Skip to content

Commit

Permalink
Download files (#416)
Browse files Browse the repository at this point in the history
* chore: download submission

* feat: download log files

* chore: add translations

* chore: I hate polymorphic types

* fix: right url to download

* chore: linter
  • Loading branch information
Topvennie authored May 13, 2024
1 parent 2dde1fe commit 6f6aba3
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 40 deletions.
65 changes: 36 additions & 29 deletions backend/api/fixtures/realistic/realistic.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion backend/api/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -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."
10 changes: 9 additions & 1 deletion backend/api/locale/nl/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -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."
59 changes: 59 additions & 0 deletions backend/api/permissions/submission_permissions.py
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions backend/api/serializers/submission_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand Down Expand Up @@ -57,6 +70,22 @@ 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"]
Expand Down
6 changes: 5 additions & 1 deletion backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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-result")
router.register(r"extra-check-results", ExtraCheckResultViewSet, basename="extra-check-result")

urlpatterns = [
path("", include(router.urls)),
Expand Down
6 changes: 3 additions & 3 deletions backend/api/views/project_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
51 changes: 46 additions & 5 deletions backend/api/views/submission_view.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,52 @@
from rest_framework import viewsets
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 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.permissions import IsAdminUser
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, **__):
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=[IsAdminUser | 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)

0 comments on commit 6f6aba3

Please sign in to comment.