From 74fbb0d460561d837b08c30c8b9b8332fc1e5c5f Mon Sep 17 00:00:00 2001 From: Topvennie Date: Sat, 11 May 2024 17:15:30 +0200 Subject: [PATCH 1/6] chore: download submission --- backend/api/views/project_view.py | 6 +++--- backend/api/views/submission_view.py | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/api/views/project_view.py b/backend/api/views/project_view.py index 6b2ed5d9..5f215c22 100644 --- a/backend/api/views/project_view.py +++ b/backend/api/views/project_view.py @@ -86,7 +86,7 @@ def _create_groups(self, request, **_): "message": gettext("project.success.groups.created"), }) - @action(detail=True, methods=["get"]) + @action(detail=True) def structure_checks(self, request, **_): """Returns the structure checks for the given project""" project = self.get_object() @@ -120,7 +120,7 @@ def _add_structure_check(self, request: Request, **_): return Response(serializer.data) - @action(detail=True, methods=["get"]) + @action(detail=True) def extra_checks(self, request, **_): """Returns the extra checks for the given project""" project = self.get_object() @@ -155,7 +155,7 @@ def _add_extra_check(self, request: Request, **_): "message": gettext("project.success.extra_check.add") }) - @action(detail=True, methods=["get"], permission_classes=[IsAdminUser | ProjectGroupPermission]) + @action(detail=True, permission_classes=[IsAdminUser | ProjectGroupPermission]) def submission_status(self, request, **_): """Returns the current submission status for the given project This includes: diff --git a/backend/api/views/submission_view.py b/backend/api/views/submission_view.py index 2750ebf1..e53d4408 100644 --- a/backend/api/views/submission_view.py +++ b/backend/api/views/submission_view.py @@ -1,4 +1,6 @@ +from django.http import FileResponse from rest_framework import viewsets +from rest_framework.decorators import action from rest_framework.mixins import RetrieveModelMixin from ..models.submission import Submission @@ -9,3 +11,9 @@ class SubmissionViewSet(RetrieveModelMixin, viewsets.GenericViewSet): queryset = Submission.objects.all() serializer_class = SubmissionSerializer + + @action(detail=True) + def zip(self, request, **_): + submission: Submission = self.get_object() + + return FileResponse(open(submission.zip.path, "rb"), as_attachment=True) From e9552e691723e102516ded890d08978655c955ae Mon Sep 17 00:00:00 2001 From: Topvennie Date: Sat, 11 May 2024 18:26:09 +0200 Subject: [PATCH 2/6] feat: download log files --- .../api/permissions/submission_permissions.py | 59 +++++++++++++++++++ backend/api/urls.py | 6 +- backend/api/views/submission_view.py | 44 ++++++++++++-- 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 backend/api/permissions/submission_permissions.py diff --git a/backend/api/permissions/submission_permissions.py b/backend/api/permissions/submission_permissions.py new file mode 100644 index 00000000..7bc7f642 --- /dev/null +++ b/backend/api/permissions/submission_permissions.py @@ -0,0 +1,59 @@ +from typing import cast + +from api.models.submission import (ExtraCheckResult, StructureCheckResult, + Submission) +from api.permissions.role_permissions import (is_assistant, is_student, + is_teacher) +from authentication.models import User +from rest_framework.permissions import SAFE_METHODS, BasePermission +from rest_framework.request import Request +from rest_framework.views import APIView + + +class SubmissionPermission(BasePermission): + def has_permission(self, request: Request, view: APIView) -> bool: + if request.method not in SAFE_METHODS: + return False + + user: User = cast(User, request.user) + + return user.is_staff or is_teacher(user) or is_assistant(user) + + def has_object_permission(self, request: Request, view: APIView, obj: Submission) -> bool: + if request.method not in SAFE_METHODS: + return False + + user: User = cast(User, request.user) + + if user.is_staff: + return True + + if is_teacher(user) or is_assistant(user): + return True + + return obj.group.students.filter(id=user.id).exists() + + +class StructureCheckResultPermission(SubmissionPermission): + def has_object_permission(self, request: Request, view: APIView, obj: StructureCheckResult) -> bool: + return super().has_object_permission(request, view, obj.submission) + + +class ExtraCheckResultPermission(SubmissionPermission): + def has_object_permission(self, request: Request, view: APIView, obj: ExtraCheckResult) -> bool: + return super().has_object_permission(request, view, obj.submission) + + +class ExtraCheckResultLogPermission(ExtraCheckResultPermission): + def has_object_permission(self, request: Request, view: APIView, obj: ExtraCheckResult) -> bool: + result = super().has_object_permission(request, view, obj) + + if not result: + return False + + user: User = cast(User, request.user) + + if is_student(user): + return obj.extra_check.show_log + + return True diff --git a/backend/api/urls.py b/backend/api/urls.py index 74c4da89..8887847d 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -8,7 +8,9 @@ from api.views.group_view import GroupViewSet from api.views.project_view import ProjectViewSet from api.views.student_view import StudentViewSet -from api.views.submission_view import SubmissionViewSet +from api.views.submission_view import (ExtraCheckResultViewSet, + StructureCheckResultViewSet, + SubmissionViewSet) from api.views.teacher_view import TeacherViewSet from api.views.user_view import UserViewSet from django.urls import include, path @@ -29,6 +31,8 @@ router.register(r"file-extensions", FileExtensionViewSet, basename="file-extension") router.register(r"faculties", FacultyViewSet, basename="faculty") router.register(r"docker-images", DockerImageViewSet, basename="docker-image") +router.register(r"structure-check-results", StructureCheckResultViewSet, basename="structure-check-results") +router.register(r"extra-check-results", ExtraCheckResultViewSet, basename="extra-check-results") urlpatterns = [ path("", include(router.urls)), diff --git a/backend/api/views/submission_view.py b/backend/api/views/submission_view.py index e53d4408..1e373f76 100644 --- a/backend/api/views/submission_view.py +++ b/backend/api/views/submission_view.py @@ -1,19 +1,51 @@ +from api.models.submission import (ExtraCheckResult, StructureCheckResult, + Submission) +from api.permissions.submission_permissions import ( + ExtraCheckResultLogPermission, ExtraCheckResultPermission, + StructureCheckResultPermission, SubmissionPermission) +from api.serializers.submission_serializer import ( + ExtraCheckResultSerializer, StructureCheckResultSerializer, + SubmissionSerializer) from django.http import FileResponse -from rest_framework import viewsets +from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.mixins import RetrieveModelMixin - -from ..models.submission import Submission -from ..serializers.submission_serializer import SubmissionSerializer +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet # TODO: Permission to ask for logs -class SubmissionViewSet(RetrieveModelMixin, viewsets.GenericViewSet): +class SubmissionViewSet(RetrieveModelMixin, GenericViewSet): queryset = Submission.objects.all() serializer_class = SubmissionSerializer + permission_classes = [SubmissionPermission] @action(detail=True) - def zip(self, request, **_): + def zip(self, request, **__): submission: Submission = self.get_object() + if not submission.zip: + return Response({"message": _("submission.download.zip")}, status=404) + return FileResponse(open(submission.zip.path, "rb"), as_attachment=True) + + +class StructureCheckResultViewSet(RetrieveModelMixin, GenericViewSet): + queryset = StructureCheckResult.objects.all() + serializer_class = StructureCheckResultSerializer + permission_classes = [StructureCheckResultPermission] + + +class ExtraCheckResultViewSet(RetrieveModelMixin, GenericViewSet): + queryset = ExtraCheckResult.objects.all() + serializer_class = ExtraCheckResultSerializer + permission_classes = [ExtraCheckResultPermission] + + @action(detail=True, permission_classes=[ExtraCheckResultLogPermission]) + def log(self, request, **__): + extra_check_result: ExtraCheckResult = self.get_object() + + if not extra_check_result.log_file: + return Response({"message": _("extra_check_result.download.log")}, status=404) + + return FileResponse(open(extra_check_result.log_file.path, "rb"), as_attachment=True) From 1c9fce324e8c21e0eb4b08b45b7a339b71bc1bbd Mon Sep 17 00:00:00 2001 From: Topvennie Date: Sat, 11 May 2024 18:28:32 +0200 Subject: [PATCH 3/6] chore: add translations --- backend/api/locale/en/LC_MESSAGES/django.po | 10 +++++++++- backend/api/locale/nl/LC_MESSAGES/django.po | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/api/locale/en/LC_MESSAGES/django.po b/backend/api/locale/en/LC_MESSAGES/django.po index 64cb99d3..dbe86326 100755 --- a/backend/api/locale/en/LC_MESSAGES/django.po +++ b/backend/api/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-05-11 15:01+0200\n" +"POT-Creation-Date: 2024-05-11 18:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -279,3 +279,11 @@ msgstr "The student was successfully added." #: views/student_view.py:45 msgid "students.success.destroy" msgstr "The student was successfully destroyed." + +#: views/submission_view.py:28 +msgid "submission.download.zip" +msgstr "No zip file available." + +#: views/submission_view.py:49 +msgid "extra_check_result.download.log" +msgstr "No log file available." diff --git a/backend/api/locale/nl/LC_MESSAGES/django.po b/backend/api/locale/nl/LC_MESSAGES/django.po index dba33e24..b89c1041 100755 --- a/backend/api/locale/nl/LC_MESSAGES/django.po +++ b/backend/api/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-05-11 15:01+0200\n" +"POT-Creation-Date: 2024-05-11 18:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -280,3 +280,11 @@ msgstr "De student is successvol toegevoegd." #: views/student_view.py:45 msgid "students.success.destroy" msgstr "De student is successvol verwijderd." + +#: views/submission_view.py:28 +msgid "submission.download.zip" +msgstr "Geen zip bestand beschikbaar." + +#: views/submission_view.py:49 +msgid "extra_check_result.download.log" +msgstr "Geen log bestand beschikbaar." From b9471cd0809b15d791c2e64bba42052fce2c32b9 Mon Sep 17 00:00:00 2001 From: Topvennie Date: Mon, 13 May 2024 18:12:02 +0200 Subject: [PATCH 4/6] chore: I hate polymorphic types --- backend/api/fixtures/realistic/realistic.yaml | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/backend/api/fixtures/realistic/realistic.yaml b/backend/api/fixtures/realistic/realistic.yaml index a1e89c6a..84cf1c2a 100644 --- a/backend/api/fixtures/realistic/realistic.yaml +++ b/backend/api/fixtures/realistic/realistic.yaml @@ -1,20 +1,3 @@ -# MARK: Polymorphic shit -- model: contenttypes.contenttype - pk: 16 - fields: - app_label: api - model: checkresult -- model: contenttypes.contenttype - pk: 17 - fields: - app_label: api - model: structurecheckresult -- model: contenttypes.contenttype - pk: 18 - fields: - app_label: api - model: extracheckresult - # MARK: Courses - model: api.course pk: 0 @@ -344,84 +327,108 @@ - model: api.checkresult pk: 1 fields: - polymorphic_ctype: 17 + polymorphic_ctype: + - api + - structurecheckresult submission: 1 result: SUCCESS error_message: null - model: api.checkresult pk: 2 fields: - polymorphic_ctype: 18 + polymorphic_ctype: + - api + - extracheckresult submission: 1 result: SUCCESS error_message: null - model: api.checkresult pk: 3 fields: - polymorphic_ctype: 18 + polymorphic_ctype: + - api + - extracheckresult submission: 1 result: SUCCESS error_message: null - model: api.checkresult pk: 4 fields: - polymorphic_ctype: 17 + polymorphic_ctype: + - api + - structurecheckresult submission: 2 result: SUCCESS error_message: null - model: api.checkresult pk: 5 fields: - polymorphic_ctype: 18 + polymorphic_ctype: + - api + - extracheckresult submission: 2 result: SUCCESS error_message: null - model: api.checkresult pk: 6 fields: - polymorphic_ctype: 18 + polymorphic_ctype: + - api + - extracheckresult submission: 2 result: SUCCESS error_message: null - model: api.checkresult pk: 7 fields: - polymorphic_ctype: 17 + polymorphic_ctype: + - api + - structurecheckresult submission: 3 result: FAILED error_message: OBLIGATED_EXTENSION_NOT_FOUND - model: api.checkresult pk: 8 fields: - polymorphic_ctype: 18 + polymorphic_ctype: + - api + - extracheckresult submission: 3 result: FAILED error_message: FAILED_STRUCTURE_CHECK - model: api.checkresult pk: 9 fields: - polymorphic_ctype: 18 + polymorphic_ctype: + - api + - extracheckresult submission: 3 result: FAILED error_message: FAILED_STRUCTURE_CHECK - model: api.checkresult pk: 10 fields: - polymorphic_ctype: 17 + polymorphic_ctype: + - api + - structurecheckresult submission: 4 result: SUCCESS error_message: null - model: api.checkresult pk: 11 fields: - polymorphic_ctype: 18 + polymorphic_ctype: + - api + - extracheckresult submission: 4 result: FAILED error_message: CHECK_ERROR - model: api.checkresult pk: 12 fields: - polymorphic_ctype: 18 + polymorphic_ctype: + - api + - extracheckresult submission: 4 result: SUCCESS error_message: null From 64aecd8a21f22a2ab89042e524670bd6e29f6b8c Mon Sep 17 00:00:00 2001 From: Topvennie Date: Mon, 13 May 2024 19:14:39 +0200 Subject: [PATCH 5/6] fix: right url to download --- .../api/serializers/submission_serializer.py | 27 +++++++++++++++++++ backend/api/urls.py | 4 +-- backend/api/views/submission_view.py | 3 ++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/backend/api/serializers/submission_serializer.py b/backend/api/serializers/submission_serializer.py index 3d4c7d3b..769b77bf 100644 --- a/backend/api/serializers/submission_serializer.py +++ b/backend/api/serializers/submission_serializer.py @@ -7,6 +7,8 @@ StructureCheckResult, Submission) from django.core.files import File from django.db.models import Max +from django.http import HttpRequest +from django.urls import reverse from django.utils.translation import gettext as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -30,6 +32,17 @@ class Meta: model = ExtraCheckResult exclude = ["polymorphic_ctype"] + def to_representation(self, instance: ExtraCheckResult) -> dict | None: + request: HttpRequest | None = self.context.get('request') + if request is not None: + representation: dict = super().to_representation(instance) + representation["log_file"] = request.build_absolute_uri( + reverse("extra-check-result-detail", args=[str(instance.id)]) + "log/" + ) + return representation + + return None + class CheckResultPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { @@ -57,6 +70,20 @@ class Meta: } } + def to_representation(self, instance: Submission) -> dict | None: + request: HttpRequest | None = self.context.get('request') + if request is not None: + representation: dict = super().to_representation(instance) + representation['zip'] = request.build_absolute_uri(reverse("submission-detail", args=[str(instance.id)]) + "zip/") + return representation + + return None + + def get_zip(self, obj): + return self.context["request"].build_absolute_uri( + reverse("submission-detail", args=[str(obj.id)]) + "zip/" + ) + def validate(self, attrs): group: Group = self.context["group"] diff --git a/backend/api/urls.py b/backend/api/urls.py index 8887847d..85c661a6 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -31,8 +31,8 @@ router.register(r"file-extensions", FileExtensionViewSet, basename="file-extension") router.register(r"faculties", FacultyViewSet, basename="faculty") router.register(r"docker-images", DockerImageViewSet, basename="docker-image") -router.register(r"structure-check-results", StructureCheckResultViewSet, basename="structure-check-results") -router.register(r"extra-check-results", ExtraCheckResultViewSet, basename="extra-check-results") +router.register(r"structure-check-results", StructureCheckResultViewSet, basename="structure-check-result") +router.register(r"extra-check-results", ExtraCheckResultViewSet, basename="extra-check-result") urlpatterns = [ path("", include(router.urls)), diff --git a/backend/api/views/submission_view.py b/backend/api/views/submission_view.py index 1e373f76..9010b64e 100644 --- a/backend/api/views/submission_view.py +++ b/backend/api/views/submission_view.py @@ -10,6 +10,7 @@ from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.mixins import RetrieveModelMixin +from rest_framework.permissions import IsAdminUser from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -41,7 +42,7 @@ class ExtraCheckResultViewSet(RetrieveModelMixin, GenericViewSet): serializer_class = ExtraCheckResultSerializer permission_classes = [ExtraCheckResultPermission] - @action(detail=True, permission_classes=[ExtraCheckResultLogPermission]) + @action(detail=True, permission_classes=[IsAdminUser | ExtraCheckResultLogPermission]) def log(self, request, **__): extra_check_result: ExtraCheckResult = self.get_object() From 69cd12ba989258586f5517a952e09222780e0736 Mon Sep 17 00:00:00 2001 From: Topvennie Date: Mon, 13 May 2024 19:16:19 +0200 Subject: [PATCH 6/6] chore: linter --- backend/api/serializers/submission_serializer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/api/serializers/submission_serializer.py b/backend/api/serializers/submission_serializer.py index 769b77bf..db2d505c 100644 --- a/backend/api/serializers/submission_serializer.py +++ b/backend/api/serializers/submission_serializer.py @@ -74,7 +74,9 @@ def to_representation(self, instance: Submission) -> dict | None: request: HttpRequest | None = self.context.get('request') if request is not None: representation: dict = super().to_representation(instance) - representation['zip'] = request.build_absolute_uri(reverse("submission-detail", args=[str(instance.id)]) + "zip/") + representation['zip'] = request.build_absolute_uri( + reverse("submission-detail", args=[str(instance.id)]) + "zip/" + ) return representation return None