Skip to content

Commit

Permalink
Merge pull request #444 from SELab-2/notifications
Browse files Browse the repository at this point in the history
chore: send notifications
  • Loading branch information
francisvaut authored May 21, 2024
2 parents 5dff24e + 5188188 commit 13fdd3d
Show file tree
Hide file tree
Showing 13 changed files with 223 additions and 22 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
8 changes: 8 additions & 0 deletions backend/api/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from authentication.signals import user_created
from django.db.models.signals import post_delete, post_save, pre_delete
from django.dispatch import Signal, receiver
from notifications.signals import NotificationType, notification_create

# MARK: Signals

Expand Down Expand Up @@ -119,6 +120,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
17 changes: 14 additions & 3 deletions 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 @@ -12,6 +13,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 @@ -20,10 +23,18 @@ def task_docker_image_build(docker_image: DockerImage):
docker_image.state = StateEnum.READY
except (docker.errors.APIError, docker.errors.BuildError, 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
Expand Down
28 changes: 27 additions & 1 deletion backend/api/tasks/extra_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from api.models.docker import StateEnum as DockerStateEnum
from api.models.submission import ErrorMessageEnum, ExtraCheckResult, StateEnum
from celery import shared_task
from django.core.files import File
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 @@ -36,12 +36,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 @@ -114,41 +124,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
# Start by saving any logs
Expand All @@ -165,6 +183,14 @@ 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)

# 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):
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"
35 changes: 35 additions & 0 deletions backend/notifications/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,38 @@ msgid "Title: Score updated"
msgstr "New score"
msgid "Description: Score updated %(score)s"
msgstr "Your score has been updated.\nNew score: %(score)s"
# Docker Image Build Succes
msgid "Title: Docker image build success"
msgstr "Docker image successfully build"
msgid "Description: Docker image build success %(name)s"
msgstr "Your docker image, $(name)s, has successfully been build"
# Docker Image Build Error
msgid "Title: Docker image build error"
msgstr "Docker image failed to build"
msgid "Description: Docker image build error %(name)s"
msgstr "Failed to build your docker image, %(name)s"
# Extra Check Succes
msgid "Title: Extra check success"
msgstr "Passed an extra check"
msgid "Description: Extra check success %(name)s"
msgstr "Your submission passed the extra check, $(name)s"
# Extra Check Error
msgid "Title: Extra check error"
msgstr "Failed an extra check"
msgid "Description: Extra check error %(name)s"
msgstr "Your submission failed to pass the extra check, %(name)s"
# Structure Checks Succes
msgid "Title: Structure checks success"
msgstr "Passed all structure checks"
msgid "Description: Structure checks success"
msgstr "Your submission passed all structure checks"
# Structure Checks Error
msgid "Title: Structure checks error"
msgstr "Failed a structure check"
msgid "Description: Structure checks"
msgstr "Your submission failed one or more structure checks"
# Submission received
msgid "Title: Submission received"
msgstr "Received submission"
msgid "Description: Submission received"
msgstr "We have received your submission"
35 changes: 35 additions & 0 deletions backend/notifications/locale/nl/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,38 @@ msgid "Title: Score updated"
msgstr "Nieuwe score"
msgid "Description: Score updated %(score)s"
msgstr "Je score is geupdate.\nNieuwe score: %(score)s"
# Docker Image Build Succes
msgid "Title: Docker image build success"
msgstr "Docker image succesvol gebouwd"
msgid "Description: Docker image build success %(name)s"
msgstr "Jouw docker image, $(name)s, is succesvol gebouwd"
# Docker Image Build Error
msgid "Title: Docker image build error"
msgstr "Docker image is gefaald om te bouwen"
msgid "Description: Docker image build error %(name)s"
msgstr "Gefaald om jouw docker image, %(name)s, te bouwen"
# Extra Check Succes
msgid "Title: Extra check success"
msgstr "Geslaagd voor een extra check"
msgid "Description: Extra check success %(name)s"
msgstr "Jouw indiening is geslaagd voor de extra check: $(name)s"
# Extra Check Error
msgid "Title: Extra check error"
msgstr "Gefaald voor een extra check"
msgid "Description: Extra check error %(name)s"
msgstr "Jouw indiening is gefaald voor de extra check: %(name)s"
# Structure Checks Succes
msgid "Title: Structure checks success"
msgstr "Geslaagd voor de structuur checks"
msgid "Description: Structure checks success"
msgstr "Jouw indiening is geslaagd voor alle structuur checks"
# Structure Checks Error
msgid "Title: Structure checks error"
msgstr "Gefaald voor een structuur check"
msgid "Description: Structure checks"
msgstr "Jouw indiening is gefaald voor een structuur check"
# Submission received
msgid "Title: Submission received"
msgstr "Indiening ontvangen"
msgid "Description: Submission received"
msgstr "We hebben jouw indiening ontvangen"
4 changes: 2 additions & 2 deletions backend/notifications/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def get_message_dict(notification: Notification) -> Dict[str, str]:

# Call the function after 60 seconds and no more than once in that period
def schedule_send_mails():
if not cache.get("notifications_send_mails"):
if not cache.get("notifications_send_mails", False):
cache.set("notifications_send_mails", True)
_send_mails.apply_async(countdown=60)

Expand All @@ -41,7 +41,7 @@ def _send_mail(mail: mail.EmailMessage, result: List[bool]):
# TODO: Retry 3
# https://docs.celeryq.dev/en/v5.3.6/getting-started/next-steps.html#next-steps
# Send all unsent emails
@shared_task(ignore_result=True)
@shared_task()
def _send_mails():
# All notifications that need to be sent
notifications = Notification.objects.filter(is_sent=False)
Expand Down
Loading

0 comments on commit 13fdd3d

Please sign in to comment.