Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download files #416

Merged
merged 6 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <LL@li.org>\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 <LL@li.org>\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)