diff --git a/backend/api/helpers/check_folder_structure.py b/backend/api/helpers/check_folder_structure.py index b1bf70a0..bfac3e4b 100644 --- a/backend/api/helpers/check_folder_structure.py +++ b/backend/api/helpers/check_folder_structure.py @@ -152,6 +152,8 @@ def check_zip_structure( folder_structure, zip_file_path, restrict_extra_folders=False): + # print(f"Checking folder_structure: {folder_structure}") + # print(f"Checking zip_file_path: {zip_file_path}") """ Check the structure of a zip file. diff --git a/backend/api/models/project.py b/backend/api/models/project.py index 839907a1..0cecf456 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -1,8 +1,10 @@ from django.db import models +from datetime import timedelta from django.utils import timezone from api.models.course import Course +# TODO max submission size class Project(models.Model): """Model that represents a project.""" @@ -61,6 +63,14 @@ def deadline_passed(self): now = timezone.now() return now > self.deadline + def is_archived(self): + """Returns True if a project is archived.""" + return self.archived + + def is_visible(self): + """Returns True if a project is visible.""" + return self.visible + def toggle_visible(self): """Toggles the visibility of the project.""" self.visible = not self.visible @@ -70,3 +80,7 @@ def toggle_archived(self): """Toggles the archived status of the project.""" self.archived = not self.archived self.save() + + def increase_deadline(self, days): + self.deadline = self.deadline + timedelta(days=days) + self.save() diff --git a/backend/api/models/submission.py b/backend/api/models/submission.py index 531f07a0..174f5265 100644 --- a/backend/api/models/submission.py +++ b/backend/api/models/submission.py @@ -18,7 +18,7 @@ class Submission(models.Model): ) # Multiple submissions can be made by a group - submission_number = models.PositiveIntegerField(blank=False, null=False) + submission_number = models.PositiveIntegerField(blank=True, null=True) # Automatically set the submission time to the current time submission_time = models.DateTimeField(auto_now_add=True) @@ -49,7 +49,6 @@ class SubmissionFile(models.Model): null=False, ) - # TODO - Set the right place to save the file file = models.FileField(blank=False, null=False) diff --git a/backend/api/serializers/group_serializer.py b/backend/api/serializers/group_serializer.py index 92847e7f..35a61402 100644 --- a/backend/api/serializers/group_serializer.py +++ b/backend/api/serializers/group_serializer.py @@ -16,9 +16,14 @@ class GroupSerializer(serializers.ModelSerializer): read_only=True, ) + submissions = serializers.HyperlinkedIdentityField( + view_name="group-submissions", + read_only=True, + ) + class Meta: model = Group - fields = ["id", "project", "students", "score"] + fields = ["id", "project", "students", "score", "submissions"] def validate(self, data): # Make sure the score of the group is lower or equal to the maximum score diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index a906177c..e05397e2 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -1,5 +1,10 @@ +from django.utils.translation import gettext from rest_framework import serializers -from ..models.project import Project +from api.models.project import Project +from api.models.group import Group +from rest_framework.exceptions import ValidationError +from api.models.submission import Submission, SubmissionFile +from api.serializers.submission_serializer import SubmissionSerializer class ProjectSerializer(serializers.ModelSerializer): @@ -43,3 +48,21 @@ class Meta: class TeacherCreateGroupSerializer(serializers.Serializer): number_groups = serializers.IntegerField(min_value=1) + + +class SubmissionAddSerializer(SubmissionSerializer): + def validate(self, data): + group: Group = self.context["group"] + project: Project = group.project + + # Check if the project's deadline is not passed. + if project.deadline_passed(): + raise ValidationError(gettext("project.error.submission.past_project")) + + if not project.is_visible(): + raise ValidationError(gettext("project.error.submission.non_visible_project")) + + if project.is_archived(): + raise ValidationError(gettext("project.error.submission.archived_project")) + + return data diff --git a/backend/api/serializers/submission_serializer.py b/backend/api/serializers/submission_serializer.py index 744cd4ac..d75cd853 100644 --- a/backend/api/serializers/submission_serializer.py +++ b/backend/api/serializers/submission_serializer.py @@ -1,5 +1,7 @@ from rest_framework import serializers from ..models.submission import Submission, SubmissionFile, ExtraChecksResult +from api.helpers.check_folder_structure import check_zip_file # , parse_zip_file +from django.db.models import Max class SubmissionFileSerializer(serializers.ModelSerializer): @@ -45,3 +47,44 @@ class Meta: "structure_checks_passed", "extra_checks_results" ] + extra_kwargs = { + "submission_number": { + "required": False, + "default": 0, + } + } + + def create(self, validated_data): + # Extract files from the request + request = self.context.get('request') + files_data = request.FILES.getlist('files') + + # Get the group for the submission + group = validated_data['group'] + + # Get the project associated with the group + project = group.project + + # Get the maximum submission number for the group's project + max_submission_number = Submission.objects.filter( + group__project=project + ).aggregate(Max('submission_number'))['submission_number__max'] or 0 + + # Set the new submission number to the maximum value plus 1 + validated_data['submission_number'] = max_submission_number + 1 + + # Create the Submission instance without the files + submission = Submission.objects.create(**validated_data) + + pas: bool = True + # Create SubmissionFile instances for each file and check if none fail structure checks + for file in files_data: + SubmissionFile.objects.create(submission=submission, file=file) + status, _ = check_zip_file(submission.group.project, file.name) + if not status: + pas = False + + # Set structure_checks_passed to True + submission.structure_checks_passed = pas + submission.save() + return submission diff --git a/backend/api/views/admin_view.py b/backend/api/views/admin_view.py index 63fdab43..ad0862d0 100644 --- a/backend/api/views/admin_view.py +++ b/backend/api/views/admin_view.py @@ -3,6 +3,6 @@ from authentication.models import User -class AdminViewSet(viewsets.ModelViewSet): +class AdminViewSet(viewsets.ReadOnlyModelViewSet): queryset = User.objects.filter(is_staff=True) serializer_class = UserSerializer diff --git a/backend/api/views/group_view.py b/backend/api/views/group_view.py index 7c86b3db..632bb7ff 100644 --- a/backend/api/views/group_view.py +++ b/backend/api/views/group_view.py @@ -10,6 +10,9 @@ from api.serializers.group_serializer import GroupSerializer from api.serializers.student_serializer import StudentSerializer from api.serializers.group_serializer import StudentJoinGroupSerializer, StudentLeaveGroupSerializer +from api.serializers.project_serializer import SubmissionAddSerializer +from api.serializers.submission_serializer import SubmissionSerializer +from rest_framework.request import Request class GroupViewSet(CreateModelMixin, @@ -36,6 +39,20 @@ def students(self, request, **_): ) return Response(serializer.data) + @action(detail=True, permission_classes=[IsAdminUser]) + def submissions(self, request, **_): + """Returns a list of students for the given group""" + # This automatically fetches the group from the URL. + # It automatically gives back a 404 HTTP response in case of not found. + group = self.get_object() + submissions = group.submissions.all() + + # Serialize the student objects + serializer = SubmissionSerializer( + submissions, many=True, context={"request": request} + ) + return Response(serializer.data) + @students.mapping.post @students.mapping.put def _add_student(self, request, **_): @@ -74,3 +91,22 @@ def _remove_student(self, request, **_): return Response({ "message": gettext("group.success.student.remove"), }) + + @submissions.mapping.post + @submissions.mapping.put + def _add_submission(self, request: Request, **_): + """Add an submission to the group""" + + group: Group = self.get_object() + + # Add submission to course + serializer = SubmissionAddSerializer( + data=request.data, context={"group": group, "request": request} + ) + + if serializer.is_valid(raise_exception=True): + serializer.save(group=group) + + return Response({ + "message": gettext("group.success.submissions.add") + }) diff --git a/backend/api/views/project_view.py b/backend/api/views/project_view.py index 761496f1..4365b78c 100644 --- a/backend/api/views/project_view.py +++ b/backend/api/views/project_view.py @@ -10,6 +10,7 @@ from ..serializers.checks_serializer import StructureCheckSerializer, ExtraCheckSerializer from api.serializers.project_serializer import ProjectSerializer, TeacherCreateGroupSerializer from api.serializers.group_serializer import GroupSerializer +from api.serializers.submission_serializer import SubmissionSerializer class ProjectViewSet(CreateModelMixin, @@ -22,7 +23,7 @@ class ProjectViewSet(CreateModelMixin, serializer_class = ProjectSerializer permission_classes = [IsAdminUser | ProjectPermission] # GroupPermission has exact the same logic as for a project - @action(detail=True, methods=["get"], permission_classes=[IsAdminUser | ProjectGroupPermission]) + @action(detail=True, permission_classes=[IsAdminUser | ProjectGroupPermission]) def groups(self, request, **_): """Returns a list of groups for the given project""" # This automatically fetches the group from the URL. @@ -37,6 +38,23 @@ def groups(self, request, **_): return Response(serializer.data) + """ + @action(detail=True, permission_classes=[IsAdminUser]) + def submissions(self, request, **_): + # Returns a list of subbmisions for the given project + # This automatically fetches the group from the URL. + # It automatically gives back a 404 HTTP response in case of not found. + project = self.get_object() + submissions = project.submissions.all() + + # Serialize the group objects + serializer = SubmissionSerializer( + submissions, many=True, context={"request": request} + ) + + return Response(serializer.data) + """ + @groups.mapping.post def _create_groups(self, request, **_): """Create a number of groups for the project""" diff --git a/data/production/structures/empty.zip b/data/production/structures/empty.zip new file mode 100644 index 00000000..15cb0ecb Binary files /dev/null and b/data/production/structures/empty.zip differ