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

Submission logic #93

Merged
merged 9 commits into from
Mar 11, 2024
2 changes: 2 additions & 0 deletions backend/api/helpers/check_folder_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 14 additions & 0 deletions backend/api/models/project.py
Original file line number Diff line number Diff line change
@@ -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."""

Expand Down Expand Up @@ -61,6 +63,14 @@ def deadline_passed(self):
now = timezone.now()
return now > self.deadline

def is_archived(self):
BramMeir marked this conversation as resolved.
Show resolved Hide resolved
"""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
Expand All @@ -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()
3 changes: 1 addition & 2 deletions backend/api/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)


Expand Down
7 changes: 6 additions & 1 deletion backend/api/serializers/group_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion backend/api/serializers/project_serializer.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
43 changes: 43 additions & 0 deletions backend/api/serializers/submission_serializer.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion backend/api/views/admin_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions backend/api/views/group_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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, **_):
Expand Down Expand Up @@ -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")
})
20 changes: 19 additions & 1 deletion backend/api/views/project_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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"""
Expand Down
Binary file added data/production/structures/empty.zip
Binary file not shown.
Loading