Skip to content

Commit

Permalink
Merge branch 'development' into fix-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
francisvaut authored May 21, 2024
2 parents cbc203c + 13fdd3d commit 1cb519a
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 36 deletions.
1 change: 1 addition & 0 deletions backend/api/serializers/group_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from api.serializers.project_serializer import ProjectSerializer
from api.serializers.student_serializer import StudentIDSerializer
from django.utils.translation import gettext
from notifications.signals import NotificationType, notification_create
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

Expand Down
33 changes: 22 additions & 11 deletions backend/api/serializers/project_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from api.models.group import Group
from api.models.project import Project
from api.models.submission import Submission, ExtraCheckResult, StructureCheckResult, StateEnum
from api.models.checks import ExtraCheck, StructureCheck
from api.serializers.course_serializer import CourseSerializer
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.utils import timezone
Expand Down Expand Up @@ -33,6 +34,22 @@ def to_representation(self, instance: Project):
if (groups_submitted > non_empty_groups):
non_empty_groups = groups_submitted

extra_checks_count = instance.extra_checks.count()

if extra_checks_count:
passed_extra_checks_submission_ids = ExtraCheckResult.objects.filter(
submission__group__project=instance,
submission__is_valid=True,
result=StateEnum.SUCCESS
).values_list('submission__id', flat=True)

passed_extra_checks_group_ids = Submission.objects.filter(
id__in=passed_extra_checks_submission_ids
).values_list('group_id', flat=True)

unique_groups = set(passed_extra_checks_group_ids)
extra_checks_passed = len(unique_groups)

passed_structure_checks_submission_ids = StructureCheckResult.objects.filter(
submission__group__project=instance,
submission__is_valid=True,
Expand All @@ -46,18 +63,12 @@ def to_representation(self, instance: Project):
unique_groups = set(passed_structure_checks_group_ids)
structure_checks_passed = len(unique_groups)

passed_extra_checks_submission_ids = ExtraCheckResult.objects.filter(
submission__group__project=instance,
submission__is_valid=True,
result=StateEnum.SUCCESS
).values_list('submission__id', flat=True)

passed_extra_checks_group_ids = Submission.objects.filter(
id__in=passed_extra_checks_submission_ids
).values_list('group_id', flat=True)
# If there are no extra checks, we can set extra_checks_passed equal to structure_checks_passed
if not extra_checks_count:
extra_checks_passed = structure_checks_passed

unique_groups = set(passed_extra_checks_group_ids)
extra_checks_passed = len(unique_groups)
# If the extra checks succeed, the structure checks also succeed
structure_checks_passed -= extra_checks_passed

# The total number of passed extra checks combined with the number of passed structure checks
# can never exceed the total number of submissions (the seeder does not account for this restriction)
Expand Down
8 changes: 8 additions & 0 deletions backend/api/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from authentication.signals import user_created
from django.db.models.signals import post_delete, post_save
from django.dispatch import Signal, receiver
from notifications.signals import NotificationType, notification_create

# Signals

Expand Down Expand Up @@ -113,6 +114,13 @@ def hook_submission(sender, instance: Submission, created: bool, **kwargs):
run_all_checks.send(sender=Submission, submission=instance)
pass

notification_create.send(
sender=Submission,
type=NotificationType.SUBMISSION_RECEIVED,
queryset=list(instance.group.students.all()),
arguments={}
)


@receiver(post_save, sender=DockerImage)
def hook_docker_image(sender, instance: DockerImage, created: bool, **kwargs):
Expand Down
24 changes: 23 additions & 1 deletion backend/api/tasks/docker_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from api.logic.get_file_path import get_docker_image_tag
from api.models.docker import DockerImage, StateEnum
from celery import shared_task
from notifications.signals import NotificationType, notification_create
from ypovoli.settings import MEDIA_ROOT


Expand All @@ -13,6 +14,8 @@ def task_docker_image_build(docker_image: DockerImage):
docker_image.state = StateEnum.BUILDING
docker_image.save()

notification_type = NotificationType.DOCKER_IMAGE_BUILD_SUCCESS

# Build the image
try:
client = docker.from_env()
Expand All @@ -21,7 +24,26 @@ def task_docker_image_build(docker_image: DockerImage):
docker_image.state = StateEnum.READY
except (docker.errors.APIError, docker.errors.BuildError, docker.errors.APIError, TypeError):
docker_image.state = StateEnum.ERROR
# TODO: Sent notification
notification_type = NotificationType.DOCKER_IMAGE_BUILD_ERROR
finally:
# Update the state
docker_image.save()

# Update the state
docker_image.save()
# Send notification
notification_create.send(
sender=DockerImage,
type=notification_type,
queryset=[docker_image.owner],
arguments={"name": docker_image.name},
)


@shared_task
def task_docker_image_remove(docker_image: DockerImage):
try:
client = docker.from_env()
client.images.remove(get_docker_image_tag(docker_image))
except docker.errors.APIError:
pass
40 changes: 40 additions & 0 deletions backend/api/tasks/extra_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.core.files.base import ContentFile
from docker.models.containers import Container
from docker.types import LogConfig
from notifications.signals import NotificationType, notification_create
from requests.exceptions import ConnectionError


Expand All @@ -33,12 +34,22 @@ def task_extra_check_start(structure_check_result: bool, extra_check_result: Ext
extra_check_result.error_message = ErrorMessageEnum.DOCKER_IMAGE_ERROR
extra_check_result.save()

notification_create.send(
sender=ExtraCheckResult,
type=NotificationType.EXTRA_CHECK_FAIL,
queryset=list(extra_check_result.submission.group.students.all()),
arguments={"name": extra_check_result.extra_check.name},
)

return structure_check_result

# Will probably never happen but doesn't hurt to check
while extra_check_result.submission.running_checks:
sleep(1)

# Notification type
notification_type = NotificationType.EXTRA_CHECK_SUCCESS

# Lock
extra_check_result.submission.running_checks = True

Expand Down Expand Up @@ -104,41 +115,49 @@ def task_extra_check_start(structure_check_result: bool, extra_check_result: Ext
case 1:
extra_check_result.result = StateEnum.FAILED
extra_check_result.error_message = ErrorMessageEnum.CHECK_ERROR
notification_type = NotificationType.EXTRA_CHECK_FAIL

# Time limit
case 2:
extra_check_result.result = StateEnum.FAILED
extra_check_result.error_message = ErrorMessageEnum.TIME_LIMIT
notification_type = NotificationType.EXTRA_CHECK_FAIL

# Memory limit
case 3:
extra_check_result.result = StateEnum.FAILED
extra_check_result.error_message = ErrorMessageEnum.MEMORY_LIMIT
notification_type = NotificationType.EXTRA_CHECK_FAIL

# Catch all non zero exit codes
case _:
extra_check_result.result = StateEnum.FAILED
extra_check_result.error_message = ErrorMessageEnum.RUNTIME_ERROR
notification_type = NotificationType.EXTRA_CHECK_FAIL

# Docker image error
except (docker.errors.APIError, docker.errors.ImageNotFound):
extra_check_result.result = StateEnum.FAILED
extra_check_result.error_message = ErrorMessageEnum.DOCKER_IMAGE_ERROR
notification_type = NotificationType.EXTRA_CHECK_FAIL

# Runtime error
except docker.errors.ContainerError:
extra_check_result.result = StateEnum.FAILED
extra_check_result.error_message = ErrorMessageEnum.RUNTIME_ERROR
notification_type = NotificationType.EXTRA_CHECK_FAIL

# Timeout error
except ConnectionError:
extra_check_result.result = StateEnum.FAILED
extra_check_result.error_message = ErrorMessageEnum.TIME_LIMIT
notification_type = NotificationType.EXTRA_CHECK_FAIL

# Unknown error
except Exception:
extra_check_result.result = StateEnum.FAILED
extra_check_result.error_message = ErrorMessageEnum.UNKNOWN
notification_type = NotificationType.EXTRA_CHECK_FAIL

# Cleanup and data saving
finally:
Expand All @@ -155,6 +174,27 @@ def task_extra_check_start(structure_check_result: bool, extra_check_result: Ext
extra_check_result.log_file.save(submission_uuid, content=ContentFile(logs), save=False)
extra_check_result.save()

# Send notification
notification_create.send(
sender=ExtraCheckResult,
type=notification_type,
queryset=list(extra_check_result.submission.group.students.all()),
arguments={"name": extra_check_result.extra_check.name},
)

# Zip and save any possible artifacts
memory_zip = io.BytesIO()
if os.listdir(artifacts_directory):
with zipfile.ZipFile(memory_zip, 'w') as zip:
for root, _, files in os.walk(artifacts_directory):
for file in files:
zip.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), artifacts_directory))

memory_zip.seek(0)
extra_check_result.artifact.save(submission_uuid, ContentFile(memory_zip.read()), False)

extra_check_result.save()

# Remove directory
try:
shutil.rmtree(submission_directory)
Expand Down
15 changes: 15 additions & 0 deletions backend/api/tasks/structure_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from api.models.submission import (ErrorMessageEnum, StateEnum,
StructureCheckResult)
from celery import shared_task
from notifications.signals import NotificationType, notification_create


@shared_task()
Expand All @@ -19,6 +20,9 @@ def task_structure_check_start(structure_check_results: list[StructureCheckResul
# Lock
structure_check_results[0].submission.running_checks = True

# Notification type
notification_type = NotificationType.STRUCTURE_CHECK_SUCCESS

all_checks_passed = True # Boolean to check if all structure checks passed
name_ext = _get_all_name_ext(structure_check_results[0].submission.zip.path) # Dict with file name and extension

Expand All @@ -38,27 +42,38 @@ def task_structure_check_start(structure_check_results: list[StructureCheckResul
if len(extensions) == 0:
structure_check_result.result = StateEnum.FAILED
structure_check_result.error_message = ErrorMessageEnum.FILE_DIR_NOT_FOUND
notification_type = NotificationType.STRUCTURE_CHECK_FAIL

# Check if no blocked extension is present
if structure_check_result.result == StateEnum.SUCCESS:
for extension in structure_check_result.structure_check.blocked_extensions.all():
if extension.extension in extensions:
structure_check_result.result = StateEnum.FAILED
structure_check_result.error_message = ErrorMessageEnum.BLOCKED_EXTENSION
notification_type = NotificationType.STRUCTURE_CHECK_FAIL

# Check if all obligated extensions are present
if structure_check_result.result == StateEnum.SUCCESS:
for extension in structure_check_result.structure_check.obligated_extensions.all():
if extension.extension not in extensions:
structure_check_result.result = StateEnum.FAILED
structure_check_result.error_message = ErrorMessageEnum.OBLIGATED_EXTENSION_NOT_FOUND
notification_type = NotificationType.STRUCTURE_CHECK_FAIL

all_checks_passed = all_checks_passed and structure_check_result.result == StateEnum.SUCCESS
structure_check_result.save()

# Release
structure_check_results[0].submission.running_checks = False

# Send notification
notification_create.send(
sender=StructureCheckResult,
type=notification_type,
queryset=list(structure_check_results[0].submission.group.students.all()),
arguments={},
)

return all_checks_passed


Expand Down
17 changes: 17 additions & 0 deletions backend/api/views/group_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from api.serializers.submission_serializer import SubmissionSerializer
from django.utils.translation import gettext
from drf_yasg.utils import swagger_auto_schema
from notifications.signals import NotificationType, notification_create
from rest_framework.decorators import action
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
RetrieveModelMixin, UpdateModelMixin)
Expand All @@ -28,6 +29,22 @@ class GroupViewSet(CreateModelMixin,
serializer_class = GroupSerializer
permission_classes = [IsAdminUser | GroupPermission]

def update(self, request, *args, **kwargs):
old_group = self.get_object()
response = super().update(request, *args, **kwargs)
if response.status_code == 200:
new_group = self.get_object()
if "score" in request.data and old_group.score != new_group.score:
# Partial updates end up in the update function as well
notification_create.send(
sender=Group,
type=NotificationType.SCORE_UPDATED,
queryset=list(new_group.students.all()),
arguments={"score": str(new_group.score)},
)

return response

@action(detail=True, methods=["get"], permission_classes=[IsAdminUser | GroupStudentPermission])
def students(self, request, **_):
"""Returns a list of students for the given group"""
Expand Down
45 changes: 45 additions & 0 deletions backend/notifications/fixtures/realistic/realistic.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
- model: notifications.notificationtemplate
pk: 1
fields:
title_key: "Title: Score added"
description_key: "Description: Score added %(score)s"
- model: notifications.notificationtemplate
pk: 2
fields:
title_key: "Title: Score updated"
description_key: "Description: Score updated %(score)s"
- model: notifications.notificationtemplate
pk: 3
fields:
title_key: "Title: Docker image build success"
description_key: "Description: Docker image build success %(name)s"
- model: notifications.notificationtemplate
pk: 4
fields:
title_key: "Title: Docker image build error"
description_key: "Description: Docker image build error %(name)s"
- model: notifications.notificationtemplate
pk: 5
fields:
title_key: "Title: Extra check success"
description_key: "Description: Extra check success %(name)s"
- model: notifications.notificationtemplate
pk: 6
fields:
title_key: "Title: Extra check error"
description_key: "Description: Extra check error %(name)s"
- model: notifications.notificationtemplate
pk: 7
fields:
title_key: "Title: Structure checks success"
description_key: "Description: Structure checks success"
- model: notifications.notificationtemplate
pk: 8
fields:
title_key: "Title: Structure checks error"
description_key: "Description: Structure checks"
- model: notifications.notificationtemplate
pk: 9
fields:
title_key: "Title: Submission received"
description_key: "Description: Submission received"
Loading

0 comments on commit 1cb519a

Please sign in to comment.