Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: send notifications #444

Merged
merged 3 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/api/serializers/group_serializer.py
Topvennie marked this conversation as resolved.
Show resolved Hide resolved
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