From 3ecbecf4cccfbf2f47635a97f20817bc2450382e Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Tue, 5 Mar 2024 11:24:44 +0100 Subject: [PATCH 01/13] chore: add max_score + group_size to project --- backend/api/fixtures/projects.yaml | 2 ++ ...02_project_group_size_project_max_score.py | 23 ++++++++++++++++++ backend/api/models/project.py | 24 ++++++++++++++++--- backend/api/serializers/project_serializer.py | 2 ++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 backend/api/migrations/0002_project_group_size_project_max_score.py diff --git a/backend/api/fixtures/projects.yaml b/backend/api/fixtures/projects.yaml index 5d9692ea..8efb2064 100644 --- a/backend/api/fixtures/projects.yaml +++ b/backend/api/fixtures/projects.yaml @@ -7,5 +7,7 @@ archived: false start_date: 2024-02-26 00:00:00+00:00 deadline: 2024-02-27 00:00:00+00:00 + group_size: 3 + max_score: 20 checks: 1 course: 2 diff --git a/backend/api/migrations/0002_project_group_size_project_max_score.py b/backend/api/migrations/0002_project_group_size_project_max_score.py new file mode 100644 index 00000000..17da27d0 --- /dev/null +++ b/backend/api/migrations/0002_project_group_size_project_max_score.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.2 on 2024-03-05 10:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='group_size', + field=models.PositiveSmallIntegerField(default=1), + ), + migrations.AddField( + model_name='project', + name='max_score', + field=models.PositiveSmallIntegerField(default=100), + ), + ] diff --git a/backend/api/models/project.py b/backend/api/models/project.py index ec16ed77..eb32c77e 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -1,4 +1,4 @@ -from datetime import timedelta, datetime +from datetime import datetime from django.db import models from django.utils import timezone from api.models.checks import Checks @@ -28,6 +28,20 @@ class Project(models.Model): deadline = models.DateTimeField(blank=False, null=False) + # Max score that can be achieved in the project + max_score = models.PositiveSmallIntegerField( + blank=False, + null=False, + default=100 + ) + + # Size of the groups than can be formed + group_size = models.PositiveSmallIntegerField( + blank=False, + null=False, + default=1 + ) + # Check entity that is linked to the project checks = models.ForeignKey( Checks, @@ -48,18 +62,22 @@ class Project(models.Model): ) def deadline_approaching_in(self, days=7): + """Returns True if the deadline is approaching in the next days.""" now = timezone.now() approaching_date = now + timezone.timedelta(days=days) return now <= self.deadline <= approaching_date def deadline_passed(self): + """Returns True if the deadline has passed.""" now = timezone.now() return now > self.deadline def toggle_visible(self): - self.visible = not (self.visible) + """Toggles the visibility of the project.""" + self.visible = not self.visible self.save() def toggle_archived(self): - self.archived = not (self.archived) + """Toggles the archived status of the project.""" + self.archived = not self.archived self.save() diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index c0e36a9a..839d3d74 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -21,6 +21,8 @@ class Meta: "archived", "start_date", "deadline", + "max_score", + "group_size", "checks", "course", ] From c05cc952742e79ffb901ff2ba9ea255cdda5555e Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Tue, 5 Mar 2024 14:13:25 +0100 Subject: [PATCH 02/13] chore: add groups to project --- backend/api/serializers/project_serializer.py | 6 +++++ backend/api/views/project_view.py | 25 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index 839d3d74..4a26d9ac 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -11,6 +11,11 @@ class ProjectSerializer(serializers.ModelSerializer): many=False, read_only=True, view_name="check-detail" ) + groups = serializers.HyperlinkedIdentityField( + view_name="project-groups", + read_only=True + ) + class Meta: model = Project fields = [ @@ -25,4 +30,5 @@ class Meta: "group_size", "checks", "course", + "groups" ] diff --git a/backend/api/views/project_view.py b/backend/api/views/project_view.py index c8e6dc83..1b2c37d6 100644 --- a/backend/api/views/project_view.py +++ b/backend/api/views/project_view.py @@ -1,8 +1,31 @@ -from rest_framework import viewsets +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response from ..models.project import Project from ..serializers.project_serializer import ProjectSerializer +from ..serializers.group_serializer import GroupSerializer class ProjectViewSet(viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectSerializer + + @action(detail=True, methods=["get"]) + def groups(self, request, pk=None): + """Returns a list of groups for the given project""" + + try: + queryset = Project.objects.get(id=pk) + groups = queryset.groups.all() + + # Serialize the group objects + serializer = GroupSerializer( + groups, many=True, context={"request": request} + ) + return Response(serializer.data) + + except Project.DoesNotExist: + # Invalid project ID + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Project not found"} + ) From ecc5cb140ece5afea19b049e3f5229105bec9926 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 09:36:59 +0100 Subject: [PATCH 03/13] chore: refactor checks --- backend/api/models/checks.py | 60 +++++++++++++++++++++++--------- backend/api/models/extensions.py | 23 ++++++++++++ 2 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 backend/api/models/extensions.py diff --git a/backend/api/models/checks.py b/backend/api/models/checks.py index ef0595ba..cd2917c0 100644 --- a/backend/api/models/checks.py +++ b/backend/api/models/checks.py @@ -1,30 +1,58 @@ from django.db import models +from api.models.project import Project +from api.models.extensions import ObligatedExtension, BlockedExtension -class FileExtension(models.Model): - """Model that represents a file extension.""" +class StructureCheck(models.Model): + """Model that represents a structure check for a project. + This means that the structure of a submission is checked. + These checks are obligated to pass.""" - # ID check should be generated automatically + # ID should be generated automatically - extension = models.CharField(max_length=10, unique=True) + # Name of the structure check + name = models.CharField( + max_length=100, + blank=False, + null=False + ) - def __str__(self) -> str: - return str(self.extension) + # Link to the project + project = models.ForeignKey( + Project, + on_delete=models.CASCADE, + related_name="structure_checks" + ) + # Obligated extensions + obligated_extensions = models.ManyToManyField( + ObligatedExtension, + blank=True + ) + + # Blocked extensions + blocked_extensions = models.ManyToManyField( + BlockedExtension, + blank=True + ) -class Checks(models.Model): - """Model that represents checks for a project.""" - # ID check should be generated automatically +class ExtraCheck(models.Model): + """Model that represents an extra check for a project. + These checks are not obligated to pass.""" - dockerfile = models.FileField(blank=True, null=True) + # ID should be generated automatically - # Link to the file extensions that are allowed - allowed_file_extensions = models.ManyToManyField( - FileExtension, related_name="checks_allowed", blank=True + # Link to the project + project = models.ForeignKey( + Project, + on_delete=models.CASCADE, + related_name="extra_checks" ) - # Link to the file extensions that are forbidden - forbidden_file_extensions = models.ManyToManyField( - FileExtension, related_name="checks_forbidden", blank=True + # Run script + # TODO set upload_to + run_script = models.FileField( + blank=False, + null=False ) diff --git a/backend/api/models/extensions.py b/backend/api/models/extensions.py new file mode 100644 index 00000000..306af433 --- /dev/null +++ b/backend/api/models/extensions.py @@ -0,0 +1,23 @@ +from django.db import models + + +class BlockedExtension(models.Model): + """Model that represents a file extension that is blocked.""" + + # ID should be generated automatically + + extension = models.CharField( + max_length=10, + unique=True + ) + + +class ObligatedExtension(models.Model): + """Model that represents a file extension that is obligated.""" + + # ID should be generated automatically + + extension = models.CharField( + max_length=10, + unique=True + ) From 7d56998c3b0446f0ee42540758ae246976e7d0b1 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 09:56:49 +0100 Subject: [PATCH 04/13] chore: refactor checks to one model --- backend/api/fixtures/checks.yaml | 32 ++++++++++------ backend/api/fixtures/projects.yaml | 1 - backend/api/models/checks.py | 6 +-- backend/api/models/extension.py | 12 ++++++ backend/api/models/extensions.py | 23 ----------- backend/api/models/project.py | 10 ----- backend/api/serializers/checks_serializer.py | 40 ++++++++++++++++---- 7 files changed, 67 insertions(+), 57 deletions(-) create mode 100644 backend/api/models/extension.py delete mode 100644 backend/api/models/extensions.py diff --git a/backend/api/fixtures/checks.yaml b/backend/api/fixtures/checks.yaml index 5a9a7795..ac162764 100644 --- a/backend/api/fixtures/checks.yaml +++ b/backend/api/fixtures/checks.yaml @@ -1,26 +1,34 @@ -- model: api.checks +- model: api.structurecheck pk: 1 fields: - dockerfile: 'path/to/Dockerfile' - allowed_file_extensions: - - 1 - - 2 - forbidden_file_extensions: - - 3 - - 4 + name: '.' + project: 123456 + obligated_extensions: + - 3 + - 4 + blocked_extensions: + - 1 + - 2 + +- model: api.extracheck + pk: 1 + fields: + project: 123456 + run_script: 'scripts/run.sh' + - model: api.fileextension pk: 1 fields: - extension: 'py' + extension: 'class' - model: api.fileextension pk: 2 fields: - extension: 'js' + extension: 'png' - model: api.fileextension pk: 3 fields: - extension: 'html' + extension: 'java' - model: api.fileextension pk: 4 fields: - extension: 'php' + extension: 'py' diff --git a/backend/api/fixtures/projects.yaml b/backend/api/fixtures/projects.yaml index 8efb2064..2b7eca2b 100644 --- a/backend/api/fixtures/projects.yaml +++ b/backend/api/fixtures/projects.yaml @@ -9,5 +9,4 @@ deadline: 2024-02-27 00:00:00+00:00 group_size: 3 max_score: 20 - checks: 1 course: 2 diff --git a/backend/api/models/checks.py b/backend/api/models/checks.py index cd2917c0..848a8052 100644 --- a/backend/api/models/checks.py +++ b/backend/api/models/checks.py @@ -1,6 +1,6 @@ from django.db import models from api.models.project import Project -from api.models.extensions import ObligatedExtension, BlockedExtension +from api.models.extension import FileExtension class StructureCheck(models.Model): @@ -26,13 +26,13 @@ class StructureCheck(models.Model): # Obligated extensions obligated_extensions = models.ManyToManyField( - ObligatedExtension, + FileExtension, blank=True ) # Blocked extensions blocked_extensions = models.ManyToManyField( - BlockedExtension, + FileExtension, blank=True ) diff --git a/backend/api/models/extension.py b/backend/api/models/extension.py new file mode 100644 index 00000000..08238c0c --- /dev/null +++ b/backend/api/models/extension.py @@ -0,0 +1,12 @@ +from django.db import models + + +class FileExtension(models.Model): + """Model that represents a file extension.""" + + # ID should be generated automatically + + extension = models.CharField( + max_length=10, + unique=True + ) diff --git a/backend/api/models/extensions.py b/backend/api/models/extensions.py deleted file mode 100644 index 306af433..00000000 --- a/backend/api/models/extensions.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import models - - -class BlockedExtension(models.Model): - """Model that represents a file extension that is blocked.""" - - # ID should be generated automatically - - extension = models.CharField( - max_length=10, - unique=True - ) - - -class ObligatedExtension(models.Model): - """Model that represents a file extension that is obligated.""" - - # ID should be generated automatically - - extension = models.CharField( - max_length=10, - unique=True - ) diff --git a/backend/api/models/project.py b/backend/api/models/project.py index eb32c77e..777f28f7 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -1,7 +1,6 @@ from datetime import datetime from django.db import models from django.utils import timezone -from api.models.checks import Checks from api.models.course import Course @@ -42,15 +41,6 @@ class Project(models.Model): default=1 ) - # Check entity that is linked to the project - checks = models.ForeignKey( - Checks, - # If the checks are deleted, the project should remain - on_delete=models.SET_NULL, - blank=True, - null=True, - ) - # Course that the project belongs to course = models.ForeignKey( Course, diff --git a/backend/api/serializers/checks_serializer.py b/backend/api/serializers/checks_serializer.py index 01254ec0..4a876809 100644 --- a/backend/api/serializers/checks_serializer.py +++ b/backend/api/serializers/checks_serializer.py @@ -1,5 +1,6 @@ from rest_framework import serializers -from ..models.checks import Checks, FileExtension +from ..models.extension import FileExtension +from ..models.checks import StructureCheck, ExtraCheck class FileExtensionSerializer(serializers.ModelSerializer): @@ -8,16 +9,39 @@ class Meta: fields = ["extension"] -class ChecksSerializer(serializers.ModelSerializer): - allowed_file_extensions = FileExtensionSerializer(many=True) +class StructureCheckSerializer(serializers.ModelSerializer): - forbidden_file_extensions = FileExtensionSerializer(many=True) + project = serializers.HyperlinkedRelatedField( + view_name="project-detail", + read_only=True + ) + + obligated_extensions = FileExtensionSerializer(many=True) + + blocked_extensions = FileExtensionSerializer(many=True) + + class Meta: + model = StructureCheck + fields = [ + "id", + "name", + "project", + "obligated_extensions", + "blocked_extensions" + ] + + +class ExtraCheckSerializer(serializers.ModelSerializer): + + project = serializers.HyperlinkedRelatedField( + view_name="project-detail", + read_only=True + ) class Meta: - model = Checks + model = ExtraCheck fields = [ "id", - "dockerfile", - "allowed_file_extensions", - "forbidden_file_extensions", + "project", + "run_script" ] From 61a20f2de22a35102e7cf1377c62dd58a4ddb349 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 10:51:10 +0100 Subject: [PATCH 05/13] chore: refactor view + serializer for checks --- ...ecks_extracheck_structurecheck_and_more.py | 39 +++++++++++++++ backend/api/models/checks.py | 2 + backend/api/serializers/project_serializer.py | 11 ++-- backend/api/urls.py | 50 ++++++++++--------- backend/api/views/checks_view.py | 18 +++++-- 5 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 backend/api/migrations/0003_remove_project_checks_extracheck_structurecheck_and_more.py diff --git a/backend/api/migrations/0003_remove_project_checks_extracheck_structurecheck_and_more.py b/backend/api/migrations/0003_remove_project_checks_extracheck_structurecheck_and_more.py new file mode 100644 index 00000000..8d5426a7 --- /dev/null +++ b/backend/api/migrations/0003_remove_project_checks_extracheck_structurecheck_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.2 on 2024-03-06 09:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_project_group_size_project_max_score'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='checks', + ), + migrations.CreateModel( + name='ExtraCheck', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('run_script', models.FileField(upload_to='')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_checks', to='api.project')), + ], + ), + migrations.CreateModel( + name='StructureCheck', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('blocked_extensions', models.ManyToManyField(blank=True, related_name='blocked_extensions', to='api.fileextension')), + ('obligated_extensions', models.ManyToManyField(blank=True, related_name='obligated_extensions', to='api.fileextension')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='structure_checks', to='api.project')), + ], + ), + migrations.DeleteModel( + name='Checks', + ), + ] diff --git a/backend/api/models/checks.py b/backend/api/models/checks.py index 848a8052..0c5e20c3 100644 --- a/backend/api/models/checks.py +++ b/backend/api/models/checks.py @@ -27,12 +27,14 @@ class StructureCheck(models.Model): # Obligated extensions obligated_extensions = models.ManyToManyField( FileExtension, + related_name="obligated_extensions", blank=True ) # Blocked extensions blocked_extensions = models.ManyToManyField( FileExtension, + related_name="blocked_extensions", blank=True ) diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index 4a26d9ac..2dbe9c79 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -7,8 +7,12 @@ class ProjectSerializer(serializers.ModelSerializer): many=False, read_only=True, view_name="course-detail" ) - checks = serializers.HyperlinkedRelatedField( - many=False, read_only=True, view_name="check-detail" + structure_checks = serializers.HyperlinkedRelatedField( + many=True, read_only=True, view_name="structure_check-detail" + ) + + extra_checks = serializers.HyperlinkedRelatedField( + many=True, read_only=True, view_name="extra_check-detail" ) groups = serializers.HyperlinkedIdentityField( @@ -28,7 +32,8 @@ class Meta: "deadline", "max_score", "group_size", - "checks", + "structure_checks", + "extra_checks", "course", "groups" ] diff --git a/backend/api/urls.py b/backend/api/urls.py index 64b83a23..8f0f6079 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,30 +1,34 @@ from django.urls import include, path -from api.views import user_view -from api.views import teacher_view -from api.views import admin_view -from api.views import assistant_view -from api.views import student_view -from api.views import project_view -from api.views import group_view -from api.views import course_view -from api.views import submision_view -from api.views import checks_view -from api.views import faculty_view from rest_framework.routers import DefaultRouter +from api.views.user_view import UserViewSet +from api.views.teacher_view import TeacherViewSet +from api.views.admin_view import AdminViewSet +from api.views.assistant_view import AssistantViewSet +from api.views.student_view import StudentViewSet +from api.views.project_view import ProjectViewSet +from api.views.group_view import GroupViewSet +from api.views.course_view import CourseViewSet +from api.views.submision_view import SubmissionViewSet +from api.views.faculty_view import facultyViewSet +from api.views.checks_view import ( + ExtraCheckViewSet, FileExtensionViewSet, StructureCheckViewSet +) + router = DefaultRouter() -router.register(r"users", user_view.UserViewSet, basename="user") -router.register(r"teachers", teacher_view.TeacherViewSet, basename="teacher") -router.register(r"admins", admin_view.AdminViewSet, basename="admin") -router.register(r"assistants", assistant_view.AssistantViewSet, basename="assistant") -router.register(r"students", student_view.StudentViewSet, basename="student") -router.register(r"projects", project_view.ProjectViewSet, basename="project") -router.register(r"groups", group_view.GroupViewSet, basename="group") -router.register(r"courses", course_view.CourseViewSet, basename="course") -router.register(r"submissions", submision_view.SubmissionViewSet, basename="submission") -router.register(r"checks", checks_view.ChecksViewSet, basename="check") -router.register(r"fileExtensions", checks_view.FileExtensionViewSet, basename="fileExtension") -router.register(r"faculties", faculty_view.facultyViewSet, basename="faculty") +router.register(r"users", UserViewSet, basename="user") +router.register(r"teachers", TeacherViewSet, basename="teacher") +router.register(r"admins", AdminViewSet, basename="admin") +router.register(r"assistants", AssistantViewSet, basename="assistant") +router.register(r"students", StudentViewSet, basename="student") +router.register(r"projects", ProjectViewSet, basename="project") +router.register(r"groups", GroupViewSet, basename="group") +router.register(r"courses", CourseViewSet, basename="course") +router.register(r"submissions", SubmissionViewSet, basename="submission") +router.register(r"structure_checks", StructureCheckViewSet, basename="structure_check") +router.register(r"extra_checks", ExtraCheckViewSet, basename="extra_check") +router.register(r"fileExtensions", FileExtensionViewSet, basename="fileExtension") +router.register(r"faculties", facultyViewSet, basename="faculty") urlpatterns = [ path("", include(router.urls)), diff --git a/backend/api/views/checks_view.py b/backend/api/views/checks_view.py index 654eb1f1..eaf18757 100644 --- a/backend/api/views/checks_view.py +++ b/backend/api/views/checks_view.py @@ -1,11 +1,19 @@ from rest_framework import viewsets -from ..models.checks import Checks, FileExtension -from ..serializers.checks_serializer import ChecksSerializer, FileExtensionSerializer +from ..models.extension import FileExtension +from ..models.checks import StructureCheck, ExtraCheck +from ..serializers.checks_serializer import ( + StructureCheckSerializer, ExtraCheckSerializer, FileExtensionSerializer +) -class ChecksViewSet(viewsets.ModelViewSet): - queryset = Checks.objects.all() - serializer_class = ChecksSerializer +class StructureCheckViewSet(viewsets.ModelViewSet): + queryset = StructureCheck.objects.all() + serializer_class = StructureCheckSerializer + + +class ExtraCheckViewSet(viewsets.ModelViewSet): + queryset = ExtraCheck.objects.all() + serializer_class = ExtraCheckSerializer class FileExtensionViewSet(viewsets.ModelViewSet): From c2a5b9fff282bbfe0a85a4b6fcb234922a756fa4 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 12:36:55 +0100 Subject: [PATCH 06/13] chore: refactor checks tests --- backend/api/tests/test_checks.py | 153 +++++++++++++++++++++++----- backend/api/tests/test_project.py | 163 +++++++++++++++--------------- 2 files changed, 208 insertions(+), 108 deletions(-) diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index b47fe651..49781271 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -1,8 +1,12 @@ import json +from django.utils import timezone from django.urls import reverse from rest_framework.test import APITestCase from authentication.models import User -from api.models.checks import FileExtension, Checks +from api.models.checks import StructureCheck, ExtraCheck +from api.models.extension import FileExtension +from api.models.project import Project +from api.models.course import Course def create_fileExtension(id, extension): @@ -12,17 +16,67 @@ def create_fileExtension(id, extension): return FileExtension.objects.create(id=id, extension=extension) -def create_checks(id, allowed_file_extensions, forbidden_file_extensions): - """Create a Checks with the given arguments.""" - check = Checks.objects.create( +def create_structure_check(id, name, project, obligated_extensions, blocked_extensions): + """ + Create a StructureCheck with the given arguments. + """ + check = StructureCheck.objects.create(id=id, name=name, project=project) + + for ext in obligated_extensions: + check.obligated_extensions.add(ext) + for ext in blocked_extensions: + check.blocked_extensions.add(ext) + + return check + + +def create_extra_check(id, project, run_script): + """ + Create an ExtraCheck with the given arguments. + """ + return ExtraCheck.objects.create(id=id, project=project, run_script=run_script) + + +def create_project(id, name, description, visible, archived, days, course, max_score, group_size): + """Create a Project with the given arguments.""" + deadline = timezone.now() + timezone.timedelta(days=days) + + return Project.objects.create( id=id, + name=name, + description=description, + visible=visible, + archived=archived, + deadline=deadline, + course=course, + max_score=max_score, + group_size=group_size, ) - for ext in allowed_file_extensions: - check.allowed_file_extensions.add(ext) - for ext in forbidden_file_extensions: - check.forbidden_file_extensions.add(ext) - return check + +def create_course(id, name, academic_startyear): + """ + Create a Course with the given arguments. + """ + return Course.objects.create( + id=id, name=name, academic_startyear=academic_startyear + ) + + +def get_project(): + course = create_course(id=1, name="Course", academic_startyear=2021) + project = create_project( + id=1, + name="Project", + description="Description", + visible=True, + archived=False, + days=5, + course=course, + max_score=10, + group_size=2, + ) + return project class FileExtensionModelTests(APITestCase): @@ -33,7 +87,7 @@ def setUp(self) -> None: def test_no_fileExtension(self): """ - able to retrieve no FileExtension before publishing it. + Able to retrieve no FileExtension before publishing it. """ response_root = self.client.get(reverse("fileExtension-list"), follow=True) self.assertEqual(response_root.status_code, 200) @@ -130,7 +184,7 @@ def test_fileExtension_detail_view(self): self.assertEqual(content_json["extension"], fileExtension.extension) -class ChecksModelTests(APITestCase): +class StructureCheckModelTests(APITestCase): def setUp(self) -> None: self.client.force_authenticate( User.get_dummy_admin() @@ -140,13 +194,13 @@ def test_no_checks(self): """ Able to retrieve no Checks before publishing it. """ - response_root = self.client.get(reverse("check-list"), follow=True) + response_root = self.client.get(reverse("structure_check-list"), follow=True) self.assertEqual(response_root.status_code, 200) self.assertEqual(response_root.accepted_media_type, "application/json") content_json = json.loads(response_root.content.decode("utf-8")) self.assertEqual(content_json, []) - def test_checks_exists(self): + def test_structure_checks_exists(self): """ Able to retrieve a single Checks after creating it. """ @@ -155,14 +209,16 @@ def test_checks_exists(self): fileExtension2 = create_fileExtension(id=2, extension="png") fileExtension3 = create_fileExtension(id=3, extension="tar") fileExtension4 = create_fileExtension(id=4, extension="wfp") - checks = create_checks( - id=5, - allowed_file_extensions=[fileExtension1, fileExtension4], - forbidden_file_extensions=[fileExtension2, fileExtension3], + checks = create_structure_check( + id=1, + name=".", + project=get_project(), + obligated_extensions=[fileExtension1, fileExtension4], + blocked_extensions=[fileExtension2, fileExtension3], ) # Make a GET request to retrieve the Checks - response = self.client.get(reverse("check-list"), follow=True) + response = self.client.get(reverse("structure_check-list"), follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -180,25 +236,68 @@ def test_checks_exists(self): # Assert the file extensions of the retrieved # Checks match the created file extensions - retrieved_allowed_file_extensions = retrieved_checks["allowed_file_extensions"] + retrieved_obligated_file_extensions = retrieved_checks["obligated_extensions"] - self.assertEqual(len(retrieved_allowed_file_extensions), 2) + self.assertEqual(len(retrieved_obligated_file_extensions), 2) self.assertEqual( - retrieved_allowed_file_extensions[0]["extension"], fileExtension1.extension + retrieved_obligated_file_extensions[0]["extension"], fileExtension1.extension ) self.assertEqual( - retrieved_allowed_file_extensions[1]["extension"], fileExtension4.extension + retrieved_obligated_file_extensions[1]["extension"], fileExtension4.extension ) - retrieved_forbidden_file_extensions = retrieved_checks[ - "forbidden_file_extensions" + retrieved_blocked_file_extensions = retrieved_checks[ + "blocked_extensions" ] - self.assertEqual(len(retrieved_forbidden_file_extensions), 2) + self.assertEqual(len(retrieved_blocked_file_extensions), 2) self.assertEqual( - retrieved_forbidden_file_extensions[0]["extension"], + retrieved_blocked_file_extensions[0]["extension"], fileExtension2.extension, ) self.assertEqual( - retrieved_forbidden_file_extensions[1]["extension"], + retrieved_blocked_file_extensions[1]["extension"], fileExtension3.extension, ) + + +class ExtraCheckModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + + def test_no_checks(self): + """ + Able to retrieve no Checks before publishing it. + """ + response_root = self.client.get(reverse("extra_check-list"), follow=True) + self.assertEqual(response_root.status_code, 200) + self.assertEqual(response_root.accepted_media_type, "application/json") + content_json = json.loads(response_root.content.decode("utf-8")) + self.assertEqual(content_json, []) + + def test_extra_checks_exists(self): + """ + Able to retrieve a single Checks after creating it. + """ + checks = create_extra_check( + id=1, project=get_project(), run_script="test.sh" + ) + + # Make a GET request to retrieve the Checks + response = self.client.get(reverse("extra_check-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one Checks + self.assertEqual(len(content_json), 1) + + # Assert the details of the retrieved Checks match the created Checks + retrieved_checks = content_json[0] + self.assertEqual(int(retrieved_checks["id"]), checks.id) + self.assertEqual(retrieved_checks["run_script"], "http://testserver" + checks.run_script.url) diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index ae9f2efb..14381296 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -5,7 +5,8 @@ from authentication.models import User from api.models.project import Project from api.models.course import Course -from api.models.checks import Checks, FileExtension +from api.models.checks import StructureCheck, ExtraCheck +from api.models.extension import FileExtension def create_course(id, name, academic_startyear): @@ -24,27 +25,7 @@ def create_fileExtension(id, extension): return FileExtension.objects.create(id=id, extension=extension) -def create_checks( - id=None, allowed_file_extensions=None, forbidden_file_extensions=None -): - """Create a Checks with the given arguments.""" - if id is None and allowed_file_extensions is None: - # extra if to make line shorter - if forbidden_file_extensions is None: - return Checks.objects.create() - - check = Checks.objects.create( - id=id, - ) - - for ext in allowed_file_extensions: - check.allowed_file_extensions.add(ext) - for ext in forbidden_file_extensions: - check.forbidden_file_extensions.add(ext) - return check - - -def create_project(name, description, visible, archived, days, checks, course): +def create_project(name, description, visible, archived, days, course): """Create a Project with the given arguments.""" deadline = timezone.now() + timezone.timedelta(days=days) @@ -54,11 +35,24 @@ def create_project(name, description, visible, archived, days, checks, course): visible=visible, archived=archived, deadline=deadline, - checks=checks, course=course, ) +def create_structure_check(id, name, project, obligated_extensions, blocked_extensions): + """ + Create a StructureCheck with the given arguments. + """ + check = StructureCheck.objects.create(id=id, name=name, project=project) + + for ext in obligated_extensions: + check.obligated_extensions.add(ext) + for ext in blocked_extensions: + check.blocked_extensions.add(ext) + + return check + + class ProjectModelTests(APITestCase): def setUp(self) -> None: self.client.force_authenticate( @@ -70,14 +64,12 @@ def test_toggle_visible(self): toggle the visible state of a project. """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() past_project = create_project( name="test", description="descr", visible=True, archived=False, days=-10, - checks=checks, course=course, ) self.assertIs(past_project.visible, True) @@ -91,14 +83,12 @@ def test_toggle_archived(self): toggle the archived state of a project. """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() past_project = create_project( name="test", description="descr", visible=True, archived=True, days=-10, - checks=checks, course=course, ) @@ -114,14 +104,12 @@ def test_deadline_approaching_in_with_past_Project(self): is in the past. """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() past_project = create_project( name="test", description="descr", visible=True, archived=False, days=-10, - checks=checks, course=course, ) self.assertIs(past_project.deadline_approaching_in(), False) @@ -132,14 +120,12 @@ def test_deadline_approaching_in_with_future_Project_within_time(self): is in the timerange given. """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() future_project = create_project( name="test", description="descr", visible=True, archived=False, days=6, - checks=checks, course=course, ) self.assertIs(future_project.deadline_approaching_in(days=7), True) @@ -150,14 +136,12 @@ def test_deadline_approaching_in_with_future_Project_not_within_time(self): is out of the timerange given. """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() future_project = create_project( name="test", description="descr", visible=True, archived=False, days=8, - checks=checks, course=course, ) self.assertIs(future_project.deadline_approaching_in(days=7), False) @@ -168,14 +152,12 @@ def test_deadline_passed_with_future_Project(self): is not passed. """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() future_project = create_project( name="test", description="descr", visible=True, archived=False, days=1, - checks=checks, course=course, ) self.assertIs(future_project.deadline_passed(), False) @@ -186,14 +168,12 @@ def test_deadline_passed_with_past_Project(self): is passed. """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() past_project = create_project( name="test", description="descr", visible=True, archived=False, days=-1, - checks=checks, course=course, ) self.assertIs(past_project.deadline_passed(), True) @@ -212,14 +192,12 @@ def test_project_exists(self): """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() project = create_project( name="test project", description="test description", visible=True, archived=False, days=7, - checks=checks, course=course, ) @@ -234,10 +212,6 @@ def test_project_exists(self): retrieved_project = content_json[0] - expected_checks_url = "http://testserver" + reverse( - "check-detail", args=[str(checks.id)] - ) - expected_course_url = "http://testserver" + reverse( "course-detail", args=[str(course.id)] ) @@ -246,7 +220,6 @@ def test_project_exists(self): self.assertEqual(retrieved_project["description"], project.description) self.assertEqual(retrieved_project["visible"], project.visible) self.assertEqual(retrieved_project["archived"], project.archived) - self.assertEqual(retrieved_project["checks"], expected_checks_url) self.assertEqual(retrieved_project["course"], expected_course_url) def test_multiple_project(self): @@ -254,14 +227,12 @@ def test_multiple_project(self): Able to retrieve multiple projects after creating it. """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() project = create_project( name="test project", description="test description", visible=True, archived=False, days=7, - checks=checks, course=course, ) @@ -271,7 +242,6 @@ def test_multiple_project(self): visible=True, archived=False, days=7, - checks=checks, course=course, ) @@ -286,10 +256,6 @@ def test_multiple_project(self): retrieved_project = content_json[0] - expected_checks_url = "http://testserver" + reverse( - "check-detail", args=[str(checks.id)] - ) - expected_course_url = "http://testserver" + reverse( "course-detail", args=[str(course.id)] ) @@ -298,15 +264,10 @@ def test_multiple_project(self): self.assertEqual(retrieved_project["description"], project.description) self.assertEqual(retrieved_project["visible"], project.visible) self.assertEqual(retrieved_project["archived"], project.archived) - self.assertEqual(retrieved_project["checks"], expected_checks_url) self.assertEqual(retrieved_project["course"], expected_course_url) retrieved_project = content_json[1] - expected_checks_url = "http://testserver" + reverse( - "check-detail", args=[str(checks.id)] - ) - expected_course_url = "http://testserver" + reverse( "course-detail", args=[str(course.id)] ) @@ -315,7 +276,6 @@ def test_multiple_project(self): self.assertEqual(retrieved_project["description"], project2.description) self.assertEqual(retrieved_project["visible"], project2.visible) self.assertEqual(retrieved_project["archived"], project2.archived) - self.assertEqual(retrieved_project["checks"], expected_checks_url) self.assertEqual(retrieved_project["course"], expected_course_url) def test_project_course(self): @@ -324,14 +284,12 @@ def test_project_course(self): """ course = create_course(id=3, name="test course", academic_startyear=2024) - checks = create_checks() project = create_project( name="test project", description="test description", visible=True, archived=False, days=7, - checks=checks, course=course, ) @@ -346,15 +304,10 @@ def test_project_course(self): retrieved_project = content_json[0] - expected_checks_url = "http://testserver" + reverse( - "check-detail", args=[str(checks.id)] - ) - self.assertEqual(retrieved_project["name"], project.name) self.assertEqual(retrieved_project["description"], project.description) self.assertEqual(retrieved_project["visible"], project.visible) self.assertEqual(retrieved_project["archived"], project.archived) - self.assertEqual(retrieved_project["checks"], expected_checks_url) response = self.client.get(retrieved_project["course"], follow=True) @@ -371,9 +324,9 @@ def test_project_course(self): self.assertEqual(content_json["academic_startyear"], course.academic_startyear) self.assertEqual(content_json["description"], course.description) - def test_project_checks(self): + def test_project_structure_checks(self): """ - Able to retrieve a check of a project after creating it. + Able to retrieve a structure check of a project after creating it. """ course = create_course(id=3, name="test course", academic_startyear=2024) @@ -381,20 +334,21 @@ def test_project_checks(self): fileExtension2 = create_fileExtension(id=2, extension="png") fileExtension3 = create_fileExtension(id=3, extension="tar") fileExtension4 = create_fileExtension(id=4, extension="wfp") - checks = create_checks( - id=5, - allowed_file_extensions=[fileExtension1, fileExtension4], - forbidden_file_extensions=[fileExtension2, fileExtension3], - ) project = create_project( name="test project", description="test description", visible=True, archived=False, days=7, - checks=checks, course=course, ) + checks = create_structure_check( + id=5, + name=".", + project=project, + obligated_extensions=[fileExtension1, fileExtension4], + blocked_extensions=[fileExtension2, fileExtension3], + ) response = self.client.get(reverse("project-list"), follow=True) @@ -417,7 +371,7 @@ def test_project_checks(self): self.assertEqual(retrieved_project["archived"], project.archived) self.assertEqual(retrieved_project["course"], expected_course_url) - response = self.client.get(retrieved_project["checks"], follow=True) + response = self.client.get(retrieved_project["structure_checks"][0], follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -432,23 +386,70 @@ def test_project_checks(self): # Assert the file extensions of the retrieved # Checks match the created file extensions - retrieved_allowed_file_extensions = content_json["allowed_file_extensions"] + retrieved_obligated_extensions = content_json["obligated_extensions"] - self.assertEqual(len(retrieved_allowed_file_extensions), 2) + self.assertEqual(len(retrieved_obligated_extensions), 2) self.assertEqual( - retrieved_allowed_file_extensions[0]["extension"], fileExtension1.extension + retrieved_obligated_extensions[0]["extension"], fileExtension1.extension ) self.assertEqual( - retrieved_allowed_file_extensions[1]["extension"], fileExtension4.extension + retrieved_obligated_extensions[1]["extension"], fileExtension4.extension ) - retrieved_forbidden_file_extensions = content_json["forbidden_file_extensions"] - self.assertEqual(len(retrieved_forbidden_file_extensions), 2) + retrieved_blocked_file_extensions = content_json["blocked_extensions"] + self.assertEqual(len(retrieved_blocked_file_extensions), 2) self.assertEqual( - retrieved_forbidden_file_extensions[0]["extension"], + retrieved_blocked_file_extensions[0]["extension"], fileExtension2.extension, ) self.assertEqual( - retrieved_forbidden_file_extensions[1]["extension"], + retrieved_blocked_file_extensions[1]["extension"], fileExtension3.extension, ) + + def test_project_extra_checks(self): + """ + Able to retrieve a extra check of a project after creating it. + """ + course = create_course(id=3, name="test course", academic_startyear=2024) + project = create_project( + name="test project", + description="test description", + visible=True, + archived=False, + days=7, + course=course, + ) + checks = ExtraCheck.objects.create( + id=5, + project=project, + run_script="testscript.sh", + ) + + response = self.client.get(reverse("project-list"), follow=True) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.accepted_media_type, "application/json") + + content_json = json.loads(response.content.decode("utf-8")) + + self.assertEqual(len(content_json), 1) + + retrieved_project = content_json[0] + + response = self.client.get(retrieved_project["extra_checks"][0], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + self.assertEqual(int(content_json["id"]), checks.id) + self.assertEqual(content_json["project"], "http://testserver" + reverse( + "project-detail", args=[str(project.id)] + )) + self.assertEqual(content_json["run_script"], "http://testserver" + checks.run_script.url) From 2c8ebd9ffe3748764683c282b8ac5151bfa8d25d Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 13:21:50 +0100 Subject: [PATCH 07/13] chore: refactor submission to have status of checks after submission --- ...ructure_checks_passed_extrachecksresult.py | 28 ++++++++++++++ backend/api/models/submission.py | 38 +++++++++++++++++++ ...serializer.py => submission_serializer.py} | 16 +++++++- backend/api/views/submision_view.py | 2 +- 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 backend/api/migrations/0004_submission_structure_checks_passed_extrachecksresult.py rename backend/api/serializers/{submision_serializer.py => submission_serializer.py} (57%) diff --git a/backend/api/migrations/0004_submission_structure_checks_passed_extrachecksresult.py b/backend/api/migrations/0004_submission_structure_checks_passed_extrachecksresult.py new file mode 100644 index 00000000..5c1abdab --- /dev/null +++ b/backend/api/migrations/0004_submission_structure_checks_passed_extrachecksresult.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.2 on 2024-03-06 11:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_remove_project_checks_extracheck_structurecheck_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='structure_checks_passed', + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name='ExtraChecksResult', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('passed', models.BooleanField(default=False)), + ('extra_check', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.extracheck')), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_checks_results', to='api.submission')), + ], + ), + ] diff --git a/backend/api/models/submission.py b/backend/api/models/submission.py index 8f41018c..531f07a0 100644 --- a/backend/api/models/submission.py +++ b/backend/api/models/submission.py @@ -1,5 +1,6 @@ from django.db import models from api.models.group import Group +from api.models.checks import ExtraCheck class Submission(models.Model): @@ -22,6 +23,13 @@ class Submission(models.Model): # Automatically set the submission time to the current time submission_time = models.DateTimeField(auto_now_add=True) + # True if submission passed the structure checks + structure_checks_passed = models.BooleanField( + blank=False, + null=False, + default=False + ) + class Meta: # A group can only have one submission with a specific number unique_together = ("group", "submission_number") @@ -43,3 +51,33 @@ class SubmissionFile(models.Model): # TODO - Set the right place to save the file file = models.FileField(blank=False, null=False) + + +class ExtraChecksResult(models.Model): + """Model for the result of extra checks on a submission.""" + + # Result ID should be generated automatically + + submission = models.ForeignKey( + Submission, + on_delete=models.CASCADE, + related_name="extra_checks_results", + blank=False, + null=False, + ) + + # Link to the extra checks that were performed + extra_check = models.ForeignKey( + ExtraCheck, + on_delete=models.CASCADE, + related_name="results", + blank=False, + null=False, + ) + + # True if the submission passed the extra checks + passed = models.BooleanField( + blank=False, + null=False, + default=False + ) diff --git a/backend/api/serializers/submision_serializer.py b/backend/api/serializers/submission_serializer.py similarity index 57% rename from backend/api/serializers/submision_serializer.py rename to backend/api/serializers/submission_serializer.py index da7458b8..cf964e1c 100644 --- a/backend/api/serializers/submision_serializer.py +++ b/backend/api/serializers/submission_serializer.py @@ -15,6 +15,20 @@ class SubmissionSerializer(serializers.ModelSerializer): files = SubmissionFileSerializer(many=True, read_only=True) + extra_checks_results = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name="extra_checks-detail" + ) + class Meta: model = Submission - fields = ["id", "group", "submission_number", "submission_time", "files"] + fields = [ + "id", + "group", + "submission_number", + "submission_time", + "files", + "structure_checks_passed", + "extra_checks_results" + ] diff --git a/backend/api/views/submision_view.py b/backend/api/views/submision_view.py index 8e0de7ad..3644d619 100644 --- a/backend/api/views/submision_view.py +++ b/backend/api/views/submision_view.py @@ -1,6 +1,6 @@ from rest_framework import viewsets from ..models.submission import Submission, SubmissionFile -from ..serializers.submision_serializer import ( +from ..serializers.submission_serializer import ( SubmissionSerializer, SubmissionFileSerializer, ) From 1dcea4160e030c245deff10679ad37e293a4f214 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 13:51:10 +0100 Subject: [PATCH 08/13] chore: link extra checks to submission --- backend/api/fixtures/submissions.yaml | 16 +++++++++++- .../api/serializers/submission_serializer.py | 25 ++++++++++++++----- backend/api/urls.py | 2 +- .../{submision_view.py => submission_view.py} | 5 +--- 4 files changed, 36 insertions(+), 12 deletions(-) rename backend/api/views/{submision_view.py => submission_view.py} (77%) diff --git a/backend/api/fixtures/submissions.yaml b/backend/api/fixtures/submissions.yaml index 0b8f876d..af3627eb 100644 --- a/backend/api/fixtures/submissions.yaml +++ b/backend/api/fixtures/submissions.yaml @@ -4,13 +4,14 @@ group: 1 submission_number: 1 submission_time: '2021-01-01T00:00:00Z' + structure_checks_passed: True - model: api.submission pk: 2 fields: group: 1 submission_number: 2 submission_time: '2021-01-02T00:00:00Z' - + structure_checks_passed: True - model: api.submissionfile pk: 1 @@ -22,3 +23,16 @@ fields: submission: 2 file: 'submissions/1/2/1.txt' + +- model: api.extrachecksresult + pk: 1 + fields: + submission: 1 + extra_check: 1 + passed: False +- model: api.extrachecksresult + pk: 2 + fields: + submission: 2 + extra_check: 1 + passed: True diff --git a/backend/api/serializers/submission_serializer.py b/backend/api/serializers/submission_serializer.py index cf964e1c..00ff8312 100644 --- a/backend/api/serializers/submission_serializer.py +++ b/backend/api/serializers/submission_serializer.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from ..models.submission import Submission, SubmissionFile +from ..models.submission import Submission, SubmissionFile, ExtraChecksResult class SubmissionFileSerializer(serializers.ModelSerializer): @@ -8,18 +8,31 @@ class Meta: fields = ["file"] +class ExtraChecksResultSerializer(serializers.ModelSerializer): + + extra_check = serializers.HyperlinkedRelatedField( + many=False, + read_only=True, + view_name="extra_check-detail" + ) + + class Meta: + model = ExtraChecksResult + fields = [ + "extra_check", + "passed" + ] + + class SubmissionSerializer(serializers.ModelSerializer): + group = serializers.HyperlinkedRelatedField( many=False, read_only=True, view_name="group-detail" ) files = SubmissionFileSerializer(many=True, read_only=True) - extra_checks_results = serializers.HyperlinkedRelatedField( - many=True, - read_only=True, - view_name="extra_checks-detail" - ) + extra_checks_results = ExtraChecksResultSerializer(many=True, read_only=True) class Meta: model = Submission diff --git a/backend/api/urls.py b/backend/api/urls.py index 8f0f6079..d16f56fb 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -8,7 +8,7 @@ from api.views.project_view import ProjectViewSet from api.views.group_view import GroupViewSet from api.views.course_view import CourseViewSet -from api.views.submision_view import SubmissionViewSet +from api.views.submission_view import SubmissionViewSet from api.views.faculty_view import facultyViewSet from api.views.checks_view import ( ExtraCheckViewSet, FileExtensionViewSet, StructureCheckViewSet diff --git a/backend/api/views/submision_view.py b/backend/api/views/submission_view.py similarity index 77% rename from backend/api/views/submision_view.py rename to backend/api/views/submission_view.py index 3644d619..279f105b 100644 --- a/backend/api/views/submision_view.py +++ b/backend/api/views/submission_view.py @@ -1,9 +1,6 @@ from rest_framework import viewsets from ..models.submission import Submission, SubmissionFile -from ..serializers.submission_serializer import ( - SubmissionSerializer, - SubmissionFileSerializer, -) +from ..serializers.submission_serializer import SubmissionSerializer, SubmissionFileSerializer class SubmissionFileViewSet(viewsets.ModelViewSet): From d0882e144433d6e2fa5f370283836b3077365f51 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 14:43:27 +0100 Subject: [PATCH 09/13] chore: fix tests submission --- backend/api/tests/test_submission.py | 49 ++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/backend/api/tests/test_submission.py b/backend/api/tests/test_submission.py index fa7a4386..ac0b52ab 100644 --- a/backend/api/tests/test_submission.py +++ b/backend/api/tests/test_submission.py @@ -4,10 +4,11 @@ from django.urls import reverse from rest_framework.test import APITestCase from authentication.models import User -from api.models.submission import Submission, SubmissionFile +from api.models.submission import Submission, SubmissionFile, ExtraChecksResult from api.models.project import Project from api.models.group import Group from api.models.course import Course +from api.models.checks import ExtraCheck def create_course(name, academic_startyear, description=None, parent_course=None): @@ -38,7 +39,7 @@ def create_group(project, score): def create_submission(group, submission_number): """Create an Submission with the given arguments.""" return Submission.objects.create( - group=group, submission_number=submission_number, submission_time=timezone.now() + group=group, submission_number=submission_number, submission_time=timezone.now(), structure_checks_passed=True ) @@ -105,6 +106,7 @@ def test_submission_exists(self): int(retrieved_submission["submission_number"]), submission.submission_number ) self.assertEqual(retrieved_submission["group"], expected_group_url) + self.assertEqual(retrieved_submission["structure_checks_passed"], submission.structure_checks_passed) def test_multiple_submission_exists(self): """ @@ -246,3 +248,46 @@ def test_submission_group(self): self.assertEqual(int(content_json["id"]), group.id) self.assertEqual(content_json["project"], expected_project_url) self.assertEqual(content_json["score"], group.score) + + def test_submission_extra_checks(self): + """ + Able to retrieve extra checks of a single submission. + """ + course = create_course(name="sel2", academic_startyear=2023) + project = create_project( + name="Project 1", description="Description 1", days=7, course=course + ) + group = create_group(project=project, score=10) + submission = create_submission(group=group, submission_number=1) + extra_check = ExtraCheck.objects.create( + project=project, run_script="test.py" + ) + extra_check_result = ExtraChecksResult.objects.create( + submission=submission, extra_check=extra_check, passed=True + ) + + # Make a GET request to retrieve the submission + response = self.client.get( + reverse("submission-detail", args=[str(submission.id)]), follow=True + ) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved submission + # match the created submission + retrieved_submission = content_json + self.assertEqual(int(retrieved_submission["id"]), submission.id) + + # Extra check that is part of the project + retrieved_extra_check = content_json["extra_checks_results"][0] + + self.assertEqual( + retrieved_extra_check["passed"], extra_check_result.passed + ) From 898219c2e884704a7824f34feb93f3b0022c8d5e Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 15:11:59 +0100 Subject: [PATCH 10/13] fix: linting --- backend/api/tests/test_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index 14381296..03dfe7cd 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -410,7 +410,7 @@ def test_project_structure_checks(self): def test_project_extra_checks(self): """ Able to retrieve a extra check of a project after creating it. - """ + """ course = create_course(id=3, name="test course", academic_startyear=2024) project = create_project( name="test project", From f3593dace12776a7d1c6f58c491338112d3bd9f9 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 15:29:19 +0100 Subject: [PATCH 11/13] fix: typo --- backend/api/urls.py | 4 ++-- backend/api/views/faculty_view.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/api/urls.py b/backend/api/urls.py index d16f56fb..4aa067ca 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -9,7 +9,7 @@ from api.views.group_view import GroupViewSet from api.views.course_view import CourseViewSet from api.views.submission_view import SubmissionViewSet -from api.views.faculty_view import facultyViewSet +from api.views.faculty_view import FacultyViewSet from api.views.checks_view import ( ExtraCheckViewSet, FileExtensionViewSet, StructureCheckViewSet ) @@ -28,7 +28,7 @@ router.register(r"structure_checks", StructureCheckViewSet, basename="structure_check") router.register(r"extra_checks", ExtraCheckViewSet, basename="extra_check") router.register(r"fileExtensions", FileExtensionViewSet, basename="fileExtension") -router.register(r"faculties", facultyViewSet, basename="faculty") +router.register(r"faculties", FacultyViewSet, basename="faculty") urlpatterns = [ path("", include(router.urls)), diff --git a/backend/api/views/faculty_view.py b/backend/api/views/faculty_view.py index 92975d0f..446ac45b 100644 --- a/backend/api/views/faculty_view.py +++ b/backend/api/views/faculty_view.py @@ -3,6 +3,6 @@ from ..serializers.faculty_serializer import facultySerializer -class facultyViewSet(viewsets.ModelViewSet): +class FacultyViewSet(viewsets.ModelViewSet): queryset = Faculty.objects.all() serializer_class = facultySerializer From b59f565b82c916f8e712017cebfbcd80b4123725 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Wed, 6 Mar 2024 15:34:27 +0100 Subject: [PATCH 12/13] fix: update default score project --- .../migrations/0005_alter_project_max_score.py | 18 ++++++++++++++++++ backend/api/models/project.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 backend/api/migrations/0005_alter_project_max_score.py diff --git a/backend/api/migrations/0005_alter_project_max_score.py b/backend/api/migrations/0005_alter_project_max_score.py new file mode 100644 index 00000000..faec3a5f --- /dev/null +++ b/backend/api/migrations/0005_alter_project_max_score.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-03-06 14:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_submission_structure_checks_passed_extrachecksresult'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='max_score', + field=models.PositiveSmallIntegerField(default=20), + ), + ] diff --git a/backend/api/models/project.py b/backend/api/models/project.py index 777f28f7..88e587fc 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -31,7 +31,7 @@ class Project(models.Model): max_score = models.PositiveSmallIntegerField( blank=False, null=False, - default=100 + default=20 ) # Size of the groups than can be formed From 79caead723c817ff938bca3979e8366aab8f4459 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Thu, 7 Mar 2024 10:00:37 +0100 Subject: [PATCH 13/13] fix: - instead of _ --- backend/api/serializers/project_serializer.py | 4 ++-- backend/api/serializers/submission_serializer.py | 2 +- backend/api/tests/test_checks.py | 16 ++++++++-------- backend/api/urls.py | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index 2dbe9c79..f9c458d5 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -8,11 +8,11 @@ class ProjectSerializer(serializers.ModelSerializer): ) structure_checks = serializers.HyperlinkedRelatedField( - many=True, read_only=True, view_name="structure_check-detail" + many=True, read_only=True, view_name="structure-check-detail" ) extra_checks = serializers.HyperlinkedRelatedField( - many=True, read_only=True, view_name="extra_check-detail" + many=True, read_only=True, view_name="extra-check-detail" ) groups = serializers.HyperlinkedIdentityField( diff --git a/backend/api/serializers/submission_serializer.py b/backend/api/serializers/submission_serializer.py index 00ff8312..744cd4ac 100644 --- a/backend/api/serializers/submission_serializer.py +++ b/backend/api/serializers/submission_serializer.py @@ -13,7 +13,7 @@ class ExtraChecksResultSerializer(serializers.ModelSerializer): extra_check = serializers.HyperlinkedRelatedField( many=False, read_only=True, - view_name="extra_check-detail" + view_name="extra-check-detail" ) class Meta: diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index 49781271..b66251b8 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -89,7 +89,7 @@ def test_no_fileExtension(self): """ Able to retrieve no FileExtension before publishing it. """ - response_root = self.client.get(reverse("fileExtension-list"), follow=True) + response_root = self.client.get(reverse("file-extension-list"), follow=True) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") @@ -105,7 +105,7 @@ def test_fileExtension_exists(self): fileExtension = create_fileExtension(id=5, extension="pdf") # Make a GET request to retrieve the fileExtension - response = self.client.get(reverse("fileExtension-list"), follow=True) + response = self.client.get(reverse("file-extension-list"), follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -133,7 +133,7 @@ def test_multiple_fileExtension(self): fileExtension2 = create_fileExtension(id=2, extension="png") # Make a GET request to retrieve the fileExtension - response = self.client.get(reverse("fileExtension-list"), follow=True) + response = self.client.get(reverse("file-extension-list"), follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -167,7 +167,7 @@ def test_fileExtension_detail_view(self): # Make a GET request to retrieve the fileExtension details response = self.client.get( - reverse("fileExtension-detail", args=[str(fileExtension.id)]), follow=True + reverse("file-extension-detail", args=[str(fileExtension.id)]), follow=True ) # Check if the response was successful @@ -194,7 +194,7 @@ def test_no_checks(self): """ Able to retrieve no Checks before publishing it. """ - response_root = self.client.get(reverse("structure_check-list"), follow=True) + response_root = self.client.get(reverse("structure-check-list"), follow=True) self.assertEqual(response_root.status_code, 200) self.assertEqual(response_root.accepted_media_type, "application/json") content_json = json.loads(response_root.content.decode("utf-8")) @@ -218,7 +218,7 @@ def test_structure_checks_exists(self): ) # Make a GET request to retrieve the Checks - response = self.client.get(reverse("structure_check-list"), follow=True) + response = self.client.get(reverse("structure-check-list"), follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -270,7 +270,7 @@ def test_no_checks(self): """ Able to retrieve no Checks before publishing it. """ - response_root = self.client.get(reverse("extra_check-list"), follow=True) + response_root = self.client.get(reverse("extra-check-list"), follow=True) self.assertEqual(response_root.status_code, 200) self.assertEqual(response_root.accepted_media_type, "application/json") content_json = json.loads(response_root.content.decode("utf-8")) @@ -285,7 +285,7 @@ def test_extra_checks_exists(self): ) # Make a GET request to retrieve the Checks - response = self.client.get(reverse("extra_check-list"), follow=True) + response = self.client.get(reverse("extra-check-list"), follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) diff --git a/backend/api/urls.py b/backend/api/urls.py index 4aa067ca..094e2fce 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -25,9 +25,9 @@ router.register(r"groups", GroupViewSet, basename="group") router.register(r"courses", CourseViewSet, basename="course") router.register(r"submissions", SubmissionViewSet, basename="submission") -router.register(r"structure_checks", StructureCheckViewSet, basename="structure_check") -router.register(r"extra_checks", ExtraCheckViewSet, basename="extra_check") -router.register(r"fileExtensions", FileExtensionViewSet, basename="fileExtension") +router.register(r"structure-checks", StructureCheckViewSet, basename="structure-check") +router.register(r"extra-checks", ExtraCheckViewSet, basename="extra-check") +router.register(r"file-extensions", FileExtensionViewSet, basename="file-extension") router.register(r"faculties", FacultyViewSet, basename="faculty") urlpatterns = [