From ab1aaba051ceadba6b2e370fd88871b1201effa2 Mon Sep 17 00:00:00 2001 From: Anirudh Prabhakaran <76864239+anirudhprabhakaran3@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:48:43 +0530 Subject: [PATCH] Add skyward expedition submission form (#136) * Add submission upload capability * Added submissions dashboard for admin --- corpus/Dockerfile | 6 +- corpus/corpus/validators.py | 57 ++++++++++++++++++ corpus/requirements.txt | 17 +++++- corpus/skyward_expedition/admin.py | 2 + corpus/skyward_expedition/forms.py | 7 +++ .../migrations/0004_submission.py | 59 +++++++++++++++++++ corpus/skyward_expedition/models.py | 19 ++++++ corpus/skyward_expedition/urls.py | 6 ++ corpus/skyward_expedition/views.py | 41 +++++++++++++ .../skyward_expedition/admin/index.html | 3 + .../admin/submissions_dashboard.html | 38 ++++++++++++ corpus/templates/skyward_expedition/base.html | 3 +- .../skyward_expedition/create_submission.html | 42 +++++++++++++ .../skyward_expedition/dashboard.html | 3 + 14 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 corpus/corpus/validators.py create mode 100644 corpus/skyward_expedition/migrations/0004_submission.py create mode 100644 corpus/templates/skyward_expedition/admin/submissions_dashboard.html create mode 100644 corpus/templates/skyward_expedition/create_submission.html diff --git a/corpus/Dockerfile b/corpus/Dockerfile index 3d858ee9..ff559497 100644 --- a/corpus/Dockerfile +++ b/corpus/Dockerfile @@ -6,7 +6,9 @@ WORKDIR /corpus ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 -RUN apt-get update && apt-get install -y gcc libpq-dev curl +RUN apt-get update && apt-get install -y gcc libpq-dev curl libmagic1 +RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - +RUN apt-get install -y nodejs COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip @@ -16,7 +18,5 @@ COPY . . RUN chmod +x ./start.sh RUN chmod +x ./start_dev.sh -RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - -RUN apt-get install -y nodejs RUN npm i RUN npm run tailwind-prod diff --git a/corpus/corpus/validators.py b/corpus/corpus/validators.py new file mode 100644 index 00000000..14294cc0 --- /dev/null +++ b/corpus/corpus/validators.py @@ -0,0 +1,57 @@ +import magic +from django.core.exceptions import ValidationError +from django.template.defaultfilters import filesizeformat +from django.utils.deconstruct import deconstructible + + +@deconstructible +class FileValidator(object): + error_messages = { + "max_size": ( + "Ensure this file size is not greater than %(max_size)s." + "Your file size is %(size)s" + ), + "min_size": ( + "Ensure this file size is not less than %(min_size)s." + "Your file size is %(size)s." + ), + "content_type": "Files of type %(content_type)s are not supported.", + } + + def __init__(self, max_size=None, min_size=None, content_types=()): + self.max_size = max_size + self.min_size = min_size + self.content_types = content_types + + def __call__(self, data): + if self.max_size is not None and data.size > self.max_size: + params = { + "max_size": filesizeformat(self.max_size), + "size": filesizeformat(data.size), + } + raise ValidationError(self.error_messages["max_size"], "max_size", params) + + if self.min_size is not None and data.size < self.min_size: + params = { + "min_size": filesizeformat(self.min_size), + "size": filesizeformat(data.size), + } + raise ValidationError(self.error_messages["min_size"], "min_size", params) + + if self.content_types: + content_type = magic.from_buffer(data.read(), mime=True) + data.seek(0) + + if content_type not in self.content_types: + params = {"content_type": content_type} + raise ValidationError( + self.error_messages["content_type"], "content_type", params + ) + + def __eq__(self, other): + return ( + isinstance(other, FileValidator) + and self.max_size == other.max_size + and self.min_size == other.min_size + and self.content_types == other.content_types + ) diff --git a/corpus/requirements.txt b/corpus/requirements.txt index cdf94bd6..413a85da 100644 --- a/corpus/requirements.txt +++ b/corpus/requirements.txt @@ -1,8 +1,21 @@ asgiref==3.7.2 -Django==4.2.10 +cfgv==3.4.0 +distlib==0.3.8 +Django==4.2.7 +filelock==3.13.1 gunicorn==21.2.0 +identify==2.5.34 +nodeenv==1.8.0 packaging==23.1 -Pillow==10.2.0 +Pillow==10.0.1 +pip==24.0 +platformdirs==4.2.0 +pre-commit==3.6.1 psycopg2-binary==2.9.9 +python-magic==0.4.27 +PyYAML==6.0.1 +setuptools==68.2.2 sqlparse==0.4.4 typing_extensions==4.8.0 +virtualenv==20.25.0 +wheel==0.41.2 diff --git a/corpus/skyward_expedition/admin.py b/corpus/skyward_expedition/admin.py index 9062aff9..af33104d 100644 --- a/corpus/skyward_expedition/admin.py +++ b/corpus/skyward_expedition/admin.py @@ -2,6 +2,7 @@ from skyward_expedition.models import Announcement from skyward_expedition.models import Invite from skyward_expedition.models import SEUser +from skyward_expedition.models import Submission from skyward_expedition.models import Team @@ -18,3 +19,4 @@ class SEUserAdmin(admin.ModelAdmin): admin.site.register(Team) admin.site.register(Invite) admin.site.register(Announcement) +admin.site.register(Submission) diff --git a/corpus/skyward_expedition/forms.py b/corpus/skyward_expedition/forms.py index 0ee71fd0..747faece 100644 --- a/corpus/skyward_expedition/forms.py +++ b/corpus/skyward_expedition/forms.py @@ -2,6 +2,7 @@ from skyward_expedition.models import Announcement from skyward_expedition.models import Invite from skyward_expedition.models import SEUser +from skyward_expedition.models import Submission from skyward_expedition.models import Team from corpus.forms import CorpusModelForm @@ -67,3 +68,9 @@ def clean(self): "Both URL Link and corresponding text are required." ) return data + + +class SubmissionForm(CorpusModelForm): + class Meta: + model = Submission + fields = ["file"] diff --git a/corpus/skyward_expedition/migrations/0004_submission.py b/corpus/skyward_expedition/migrations/0004_submission.py new file mode 100644 index 00000000..734003dc --- /dev/null +++ b/corpus/skyward_expedition/migrations/0004_submission.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.7 on 2024-03-04 04:10 +import django.db.models.deletion +from django.db import migrations +from django.db import models + +import corpus.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "skyward_expedition", + "0003_seuser_degree_seuser_ieee_member_seuser_ieee_number_and_more", + ), + ] + + operations = [ + migrations.CreateModel( + name="Submission", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "file", + models.FileField( + upload_to="skyward_expedition/submissions", + validators=[ + corpus.validators.FileValidator( + content_types=( + "application/zip", + "application/x-rar-compressed", + "application/octet-stream", + ), + max_size=10485760, + ) + ], + verbose_name="Submission File", + ), + ), + ( + "team", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="skyward_expedition.team", + ), + ), + ], + ), + ] diff --git a/corpus/skyward_expedition/models.py b/corpus/skyward_expedition/models.py index e7ca4671..cd1581d7 100644 --- a/corpus/skyward_expedition/models.py +++ b/corpus/skyward_expedition/models.py @@ -2,6 +2,16 @@ from django.db import models from corpus.utils import send_email +from corpus.validators import FileValidator + +file_validator = FileValidator( + max_size=1024 * 1024 * 10, + content_types=( + "application/zip", + "application/x-rar-compressed", + "application/octet-stream", + ), +) # Create your models here. @@ -70,3 +80,12 @@ def send_email(self, mail_option): {"announcement": self}, bcc=email_ids, ) + + +class Submission(models.Model): + team = models.ForeignKey(Team, on_delete=models.CASCADE, blank=True, null=True) + file = models.FileField( + verbose_name="Submission File", + upload_to="skyward_expedition/submissions", + validators=[file_validator], + ) diff --git a/corpus/skyward_expedition/urls.py b/corpus/skyward_expedition/urls.py index 01bbe372..0c15a00e 100644 --- a/corpus/skyward_expedition/urls.py +++ b/corpus/skyward_expedition/urls.py @@ -19,6 +19,7 @@ views.delete_invite, name="skyward_expedition_delete_invite", ), + path("submission/", views.submission, name="skyward_expedition_create_submission"), path("admin/", views.admin, name="skyward_expedition_admin"), path( "admin/members/", @@ -53,4 +54,9 @@ views.delete_announcement, name="skyward_expedition_delete_announcement", ), + path( + "admin/submissions/", + views.submissions_dashboard, + name="skyward_expedition_submissions_dashboard", + ), ] diff --git a/corpus/skyward_expedition/views.py b/corpus/skyward_expedition/views.py index 6bf48b1f..3110e78b 100644 --- a/corpus/skyward_expedition/views.py +++ b/corpus/skyward_expedition/views.py @@ -10,10 +10,12 @@ from skyward_expedition.forms import AnnouncementForm from skyward_expedition.forms import InviteForm from skyward_expedition.forms import SEForm +from skyward_expedition.forms import SubmissionForm from skyward_expedition.forms import TeamCreationForm from skyward_expedition.models import Announcement from skyward_expedition.models import Invite from skyward_expedition.models import SEUser +from skyward_expedition.models import Submission from skyward_expedition.models import Team from corpus.decorators import ensure_group_membership @@ -254,6 +256,37 @@ def delete_invite(request, pk): return redirect("skyward_expedition_dashboard") +@login_required +@module_enabled(module_name="skyward_expedition") +def submission(request): + team = SEUser.objects.get(user=request.user).team + + try: + prev_submission = Submission.objects.get(team=team) + except Submission.DoesNotExist: + prev_submission = None + + if prev_submission: + form = SubmissionForm(instance=prev_submission) + else: + form = SubmissionForm() + + if request.method == "POST": + print(request.POST) + print(request.FILES) + form = SubmissionForm(request.POST, request.FILES) + if form.is_valid(): + submission = form.save(commit=False) + submission.team = team + submission.save() + messages.success(request, "Submission made successfully!") + return redirect("skyward_expedition_dashboard") + + args = {"form": form} + + return render(request, "skyward_expedition/create_submission.html", args) + + @login_required @ensure_group_membership(group_names=["skyward_expedition_admin"]) def admin(request): @@ -360,3 +393,11 @@ def delete_announcement(request, announcement_id): messages.success(request, "Announcement deleted!") return redirect("skyward_expedition_announcements_dashboard") + + +@login_required +@ensure_group_membership(group_names=["skyward_expedition_admin"]) +def submissions_dashboard(request): + submissions = Submission.objects.all() + args = {"submissions": submissions} + return render(request, "skyward_expedition/admin/submissions_dashboard.html", args) diff --git a/corpus/templates/skyward_expedition/admin/index.html b/corpus/templates/skyward_expedition/admin/index.html index b9f55e87..69195175 100644 --- a/corpus/templates/skyward_expedition/admin/index.html +++ b/corpus/templates/skyward_expedition/admin/index.html @@ -16,6 +16,9 @@
Team | +Submission | +
{{ submission.team.team_name }} | ++ {{ submission.file }} + | +