Squash merge of code from Elia

This commit is contained in:
Elia Bieri 2023-08-25 09:39:14 +02:00 committed by Daniel Egger
parent e96c21f623
commit 56e454cc8b
15 changed files with 469 additions and 68 deletions

View File

@ -121,12 +121,12 @@ There are multiple ways on how to add new translations to Locize:
### Process one: Let Locize add missing keys automatically ### Process one: Let Locize add missing keys automatically
When running the app, it will automatically add the missing translation When running the app, it will automatically add the missing translation
keys to Locize. keys to Locize.
There you can translate them, and also add the German translation. There you can translate them, and also add the German translation.
### Process two: Add keys manually ### Process two: Add keys manually
You can add the new keys manually to the German locale file in You can add the new keys manually to the German locale file in
./client/locales/de/translation.json ./client/locales/de/translation.json
Then you can run the following command to add the keys to Locize: Then you can run the following command to add the keys to Locize:
@ -149,7 +149,6 @@ The command will add the keys and the German translation to Locize.
Bonus: Use the "i18n ally" plugin in VSCode or IntelliJ to get extract untranslated Bonus: Use the "i18n ally" plugin in VSCode or IntelliJ to get extract untranslated
texts directly from the code to the translation.json file. texts directly from the code to the translation.json file.
### "_many" plural form in French and Italian ### "_many" plural form in French and Italian
See https://github.com/i18next/i18next/issues/1691#issuecomment-968063348 See https://github.com/i18next/i18next/issues/1691#issuecomment-968063348
@ -296,7 +295,6 @@ npm run dev
If you run `npm run dev`, the codegen command will be run automatically in watch mode. If you run `npm run dev`, the codegen command will be run automatically in watch mode.
For the `ObjectTypes` on the server, please use the postfix `ObjectType` for types, For the `ObjectTypes` on the server, please use the postfix `ObjectType` for types,
like `LearningContentAttendanceCourseObjectType`. like `LearningContentAttendanceCourseObjectType`.

View File

@ -18,13 +18,28 @@ COPY ./server ${APP_HOME}
# define an alias for the specfic python version used in this file. # define an alias for the specfic python version used in this file.
FROM python:${PYTHON_VERSION} as python FROM python:${PYTHON_VERSION} as python
# Setup Supersonic (Cron scheduler for containers)
# https://github.com/aptible/supercronic
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.26/supercronic-linux-amd64 \
SUPERCRONIC=supercronic-linux-amd64 \
SUPERCRONIC_SHA1SUM=7a79496cf8ad899b99a719355d4db27422396735
RUN apt-get update && apt-get install --no-install-recommends -y curl
RUN curl -fsSLO "$SUPERCRONIC_URL" \
&& echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \
&& chmod +x "$SUPERCRONIC" \
&& mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
&& ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic
COPY ./compose/django/send_attendance_course_reminders /app/send_attendance_course_reminders
# Python build stage # Python build stage
FROM python as python-build-stage FROM python as python-build-stage
ARG BUILD_ENVIRONMENT=production ARG BUILD_ENVIRONMENT=production
# Install apt packages # Install apt packages
RUN apt-get update && apt-get install --no-install-recommends -y \ RUN apt-get install --no-install-recommends -y \
# dependencies for building Python packages # dependencies for building Python packages
build-essential \ build-essential \
# psycopg2 dependencies # psycopg2 dependencies
@ -56,7 +71,7 @@ RUN addgroup --system django \
# Install required system dependencies # Install required system dependencies
RUN apt-get update && apt-get install --no-install-recommends -y \ RUN apt-get install --no-install-recommends -y \
# psycopg2 dependencies # psycopg2 dependencies
libpq-dev \ libpq-dev \
# Translations dependencies # Translations dependencies

View File

@ -15,4 +15,9 @@ else
python /app/manage.py migrate python /app/manage.py migrate
fi fi
if [[ $IT_APP_ENVIRONMENT != dev* ]]; then
# Start periodic tasks
/usr/local/bin/supercronic -sentry-dsn "$IT_SENTRY_DSN" -split-logs /app/send_attendance_course_reminders &
fi
newrelic-admin run-program gunicorn config.asgi --bind 0.0.0.0:7555 --chdir=/app -k uvicorn.workers.UvicornWorker newrelic-admin run-program gunicorn config.asgi --bind 0.0.0.0:7555 --chdir=/app -k uvicorn.workers.UvicornWorker

View File

@ -0,0 +1 @@
@daily /usr/local/bin/python /app/manage.py send_attendance_course_reminders

View File

@ -636,7 +636,7 @@ EDONIQ_CERTIFICATE = env("IT_EDONIQ_CERTIFICATE", default="")
# Notifications # Notifications
# django-notifications # django-notifications
DJANGO_NOTIFICATIONS_CONFIG = {"SOFT_DELETE": True} DJANGO_NOTIFICATIONS_CONFIG = {"SOFT_DELETE": True, "USE_JSONFIELD": True}
NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification" NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
# sendgrid (email notifications) # sendgrid (email notifications)
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="") SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")

View File

@ -16,7 +16,7 @@ from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.utils import find_first from vbv_lernwelt.core.utils import find_first
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.notify.service import NotificationService from vbv_lernwelt.notify.service import EmailTemplate, NotificationService
def update_assignment_completion( def update_assignment_completion(
@ -144,6 +144,7 @@ def update_assignment_completion(
sender=ac.assignment_user, sender=ac.assignment_user,
course=course_session.course.title, course=course_session.course.title,
target_url=ac.get_assignment_evaluation_frontend_url(), target_url=ac.get_assignment_evaluation_frontend_url(),
email_template=EmailTemplate.CASEWORK_SUBMITTED,
) )
elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED: elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
ac.evaluation_submitted_at = timezone.now() ac.evaluation_submitted_at = timezone.now()
@ -162,6 +163,7 @@ def update_assignment_completion(
sender=evaluation_user, sender=evaluation_user,
course=course_session.course.title, course=course_session.course.title,
target_url=assignment_frontend_url, target_url=assignment_frontend_url,
email_template=EmailTemplate.CASEWORK_EVALUATED,
) )
ac.completion_status = completion_status.value ac.completion_status = completion_status.value

View File

@ -105,14 +105,14 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch", trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
) )
tuesday_in_two_weeks = ( tuesday_in_one_week = (
datetime.now() + relativedelta(weekday=TU(2)) + relativedelta(weeks=2) datetime.now() + relativedelta(weekday=TU) + relativedelta(weeks=1)
) )
csac.due_date.start = timezone.make_aware( csac.due_date.start = timezone.make_aware(
tuesday_in_two_weeks.replace(hour=8, minute=30, second=0, microsecond=0) tuesday_in_one_week.replace(hour=8, minute=30, second=0, microsecond=0)
) )
csac.due_date.end = timezone.make_aware( csac.due_date.end = timezone.make_aware(
tuesday_in_two_weeks.replace(hour=17, minute=0, second=0, microsecond=0) tuesday_in_one_week.replace(hour=17, minute=0, second=0, microsecond=0)
) )
csac.due_date.save() csac.due_date.save()

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.notify.service import NotificationService from vbv_lernwelt.notify.service import EmailTemplate, NotificationService
class FeedbackIntegerField(models.IntegerField): class FeedbackIntegerField(models.IntegerField):
@ -55,6 +55,7 @@ class FeedbackResponse(models.Model):
recipient=csu.user, recipient=csu.user,
verb=f"{_('New feedback for circle')} {self.circle.title}", verb=f"{_('New feedback for circle')} {self.circle.title}",
target_url=f"/course/{self.course_session.course.slug}/cockpit/feedback/{self.circle_id}/", target_url=f"/course/{self.course_session.course.slug}/cockpit/feedback/{self.circle_id}/",
email_template=EmailTemplate.NEW_FEEDBACK,
) )
super(FeedbackResponse, self).save(*args, **kwargs) super(FeedbackResponse, self).save(*args, **kwargs)

View File

@ -25,6 +25,7 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse, LearningContentAttendanceCourse,
LearningContentEdoniqTest, LearningContentEdoniqTest,
) )
from vbv_lernwelt.notify.models import NotificationType
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -248,7 +249,11 @@ def create_or_update_user(
if not user: if not user:
# create user # create user
user = User(sso_id=sso_id, email=email, username=email) user = User(
sso_id=sso_id,
email=email,
username=email,
)
user.email = email user.email = email
user.sso_id = user.sso_id or sso_id user.sso_id = user.sso_id or sso_id
@ -699,8 +704,9 @@ def sync_students_from_t2l(data):
def update_user_json_data(user: User, data: Dict[str, Any]): def update_user_json_data(user: User, data: Dict[str, Any]):
# Set E-Mail notification settings for new users
user.additional_json_data = user.additional_json_data | sanitize_json_data_input( user.additional_json_data = user.additional_json_data | sanitize_json_data_input(
data {**data, "email_notification_types": [str(NotificationType.INFORMATION)]}
) )

View File

@ -52,6 +52,7 @@ class CreateOrUpdateStudentTestCase(TestCase):
"Lehrvertragsnummer": "1234", "Lehrvertragsnummer": "1234",
"Tel. Privat": "079 593 83 43", "Tel. Privat": "079 593 83 43",
"Geburtsdatum": "01.01.2000", "Geburtsdatum": "01.01.2000",
"email_notification_types": ["INFORMATION"],
} }
def test_create_student(self): def test_create_student(self):

View File

@ -0,0 +1,65 @@
from datetime import timedelta
import djclick as click
import structlog
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.notify.service import EmailTemplate, NotificationService
logger = structlog.get_logger(__name__)
PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2)
DATETIME_FORMAT_STR = "%H:%M %d.%m.%Y"
def format_datetime(dt: timezone.datetime) -> str:
return dt.astimezone(timezone.get_current_timezone()).strftime(DATETIME_FORMAT_STR)
def send_notification(
recipient: User, attendance_course: CourseSessionAttendanceCourse
):
NotificationService.send_information_notification(
recipient=recipient,
verb=_("Erinnerung: Bald findet ein Präsenzkurs statt"),
target_url=attendance_course.learning_content.get_frontend_url(),
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER,
template_data={
"attendance_course": attendance_course.learning_content.title,
"location": attendance_course.location,
"trainer": attendance_course.trainer,
"start": format_datetime(attendance_course.due_date.start),
"end": format_datetime(attendance_course.due_date.end),
},
)
def check_attendance_course():
"""Checks if an attendance course is coming up and sends a reminder to the participants"""
start = timezone.now()
end = timezone.now() + PRESENCE_COURSE_REMINDER_LEAD_TIME
logger.info(
"Querying for attendance courses in specified time range",
start_time=start.strftime(DATETIME_FORMAT_STR),
end_time=end.strftime(DATETIME_FORMAT_STR),
)
attendance_courses = CourseSessionAttendanceCourse.objects.filter(
due_date__start__lte=end,
due_date__start__gte=start,
)
for attendance_course in attendance_courses:
cs_id = attendance_course.course_session.id
csu = CourseSessionUser.objects.filter(course_session_id=cs_id)
for user in csu:
send_notification(user.user, attendance_course)
if not attendance_courses:
logger.info("No attendance courses found")
@click.command()
def command():
check_attendance_course()

View File

@ -1,3 +1,4 @@
from enum import Enum
from typing import Optional from typing import Optional
import structlog import structlog
@ -11,35 +12,60 @@ from vbv_lernwelt.notify.models import Notification, NotificationType
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
class EmailTemplate(Enum):
"""Enum for the different Sendgrid email templates."""
# VBV - Erinnerung Präsenzkurse
ATTENDANCE_COURSE_REMINDER = {
"de": "d-9af079f98f524d85ac6e4166de3480da",
"it": "d-ab78ddca8a7a46b8afe50aaba3efee81",
"fr": "d-f88d9912e5484e55a879571463e4a166",
}
# VBV - Geleitete Fallarbeit abgegeben
CASEWORK_SUBMITTED = {"de": "d-599f0b35ddcd4fac99314cdf8f5446a2"}
# VBV - Geleitete Fallarbeit bewertet
CASEWORK_EVALUATED = {"de": "d-8c57fa13116b47be8eec95dfaf2aa030"}
# VBV - Neues Feedback für Circle
NEW_FEEDBACK = {"de": "d-40fb94d5149949e7b8e7ddfcf0fcfdde"}
class EmailService: class EmailService:
"""Email service class implemented using the Sendgrid API"""
_sendgrid_client = SendGridAPIClient(setting("SENDGRID_API_KEY")) _sendgrid_client = SendGridAPIClient(setting("SENDGRID_API_KEY"))
@classmethod @classmethod
def send_email(cls, recipient: User, verb: str, target_url) -> bool: def send_email(
cls,
recipient: User,
template: EmailTemplate,
template_data: dict,
) -> None:
message = Mail( message = Mail(
from_email="info@iterativ.ch", from_email="noreply@my.vbv-afa.ch",
to_emails=recipient.email, to_emails=recipient.email,
subject=f"myVBV - {verb}",
## TODO: Add HTML content.
html_content=f"{verb}: <a href='{target_url}'>Link</a>",
) )
try: message.template_id = template.value.get(
cls._sendgrid_client.send(message) recipient.language, template.value["de"]
logger.info(f"Successfully sent email to {recipient}", label="email") )
return True message.dynamic_template_data = template_data
except Exception as e: cls._sendgrid_client.send(message)
logger.error(
f"Failed to send email to {recipient}: {e}",
exc_info=True,
label="email",
)
return False
class NotificationService: class NotificationService:
@classmethod @classmethod
def send_user_interaction_notification( def send_user_interaction_notification(
cls, recipient: User, verb: str, sender: User, course: str, target_url: str cls,
recipient: User,
verb: str,
sender: User,
course: str,
target_url: str,
email_template: EmailTemplate,
template_data: dict = {},
) -> None: ) -> None:
cls._send_notification( cls._send_notification(
recipient=recipient, recipient=recipient,
@ -48,11 +74,19 @@ class NotificationService:
course=course, course=course,
target_url=target_url, target_url=target_url,
notification_type=NotificationType.USER_INTERACTION, notification_type=NotificationType.USER_INTERACTION,
email_template=email_template,
template_data=template_data,
) )
@classmethod @classmethod
def send_progress_notification( def send_progress_notification(
cls, recipient: User, verb: str, course: str, target_url: str cls,
recipient: User,
verb: str,
course: str,
target_url: str,
email_template: EmailTemplate,
template_data: dict = {},
) -> None: ) -> None:
cls._send_notification( cls._send_notification(
recipient=recipient, recipient=recipient,
@ -60,17 +94,26 @@ class NotificationService:
course=course, course=course,
target_url=target_url, target_url=target_url,
notification_type=NotificationType.PROGRESS, notification_type=NotificationType.PROGRESS,
email_template=email_template,
template_data=template_data,
) )
@classmethod @classmethod
def send_information_notification( def send_information_notification(
cls, recipient: User, verb: str, target_url: str cls,
recipient: User,
verb: str,
target_url: str,
email_template: EmailTemplate,
template_data: dict = {},
) -> None: ) -> None:
cls._send_notification( cls._send_notification(
recipient=recipient, recipient=recipient,
verb=verb, verb=verb,
target_url=target_url, target_url=target_url,
notification_type=NotificationType.INFORMATION, notification_type=NotificationType.INFORMATION,
email_template=email_template,
template_data=template_data,
) )
@classmethod @classmethod
@ -79,42 +122,66 @@ class NotificationService:
recipient: User, recipient: User,
verb: str, verb: str,
notification_type: NotificationType, notification_type: NotificationType,
sender: Optional[User] = None, email_template: EmailTemplate,
course: Optional[str] = None, template_data: dict,
target_url: Optional[str] = None, sender: User | None = None,
course: str | None = None,
target_url: str | None = None,
) -> None: ) -> None:
actor_avatar_url: Optional[str] = None actor_avatar_url: Optional[str] = None
if not sender: if not sender:
sender = User.objects.get(email="admin") sender = User.objects.get(email="admin")
else: else:
actor_avatar_url = sender.avatar_url actor_avatar_url = sender.avatar_url
emailed = False
if cls._should_send_email(notification_type, recipient):
emailed = cls._send_email(recipient, verb, target_url)
log = logger.bind( log = logger.bind(
recipient=recipient.get_full_name(),
sender=sender.get_full_name(),
verb=verb, verb=verb,
notification_type=notification_type, notification_type=notification_type,
sender=sender.get_full_name(),
recipient=recipient.get_full_name(),
course=course, course=course,
target_url=target_url, target_url=target_url,
template_data=template_data,
) )
if NotificationService._is_duplicate_notification(
recipient=recipient,
verb=verb,
notification_type=notification_type,
target_url=target_url,
template_name=email_template.name,
template_data=template_data,
):
log.warn("A duplicate notification was omitted from being sent")
return
emailed = False
if cls._should_send_email(notification_type, recipient):
emailed = cls._send_email(
recipient=recipient,
template=email_template,
template_data={
"target_url": f"https://my.vbv-afa.ch{target_url}",
**template_data,
},
)
try: try:
response = notify.send( response = notify.send(
sender=sender, sender=sender,
recipient=recipient, recipient=recipient,
verb=verb, verb=verb,
emailed=emailed,
# The metadata is saved in the 'data' member of the AbstractNotification model
email_template=email_template.name,
template_data=template_data,
) )
sent_notification: Notification = response[0][1][0] sent_notification: Notification = response[0][1][0] # 🫨
sent_notification.target_url = target_url sent_notification.target_url = target_url
sent_notification.notification_type = notification_type sent_notification.notification_type = notification_type
sent_notification.course = course sent_notification.course = course
sent_notification.actor_avatar_url = actor_avatar_url sent_notification.actor_avatar_url = actor_avatar_url
sent_notification.emailed = emailed
sent_notification.save() sent_notification.save()
except Exception as e: except Exception as e:
log.bind(exception=e) log.error("Failed to send notification", exception=str(e))
log.error("Failed to send notification")
else: else:
log.info("Notification sent successfully") log.info("Notification sent successfully")
@ -127,13 +194,49 @@ class NotificationService:
) )
@staticmethod @staticmethod
def _send_email(recipient: User, verb: str, target_url: Optional[str]) -> bool: def _send_email(
recipient: User,
template: EmailTemplate,
template_data: dict | None,
) -> bool:
log = logger.bind(
recipient=recipient.username,
template=template.name,
template_data=template_data,
)
try: try:
return EmailService.send_email( EmailService.send_email(
recipient=recipient, recipient=recipient,
verb=verb, template=template,
target_url=target_url, template_data=template_data,
) )
log.info("Email sent successfully")
return True
except Exception as e: except Exception as e:
logger.error(f"Failed to send email to {recipient}: {e}") log.error(
"Failed to send Email", exception=str(e), exc_info=True, stack_info=True
)
return False return False
@staticmethod
def _is_duplicate_notification(
recipient: User,
verb: str,
notification_type: NotificationType,
template_name: str,
template_data: dict,
target_url: str | None,
) -> bool:
"""Check if a notification with the same parameters has already been sent to the recipient.
This is to prevent duplicate notifications from being sent and to protect against potential programming errors.
"""
return Notification.objects.filter(
recipient=recipient,
verb=verb,
notification_type=notification_type,
target_url=target_url,
data={
"email_template": template_name,
"template_data": template_data,
},
).exists()

View File

@ -0,0 +1,125 @@
from dataclasses import dataclass
from datetime import datetime, timedelta
from unittest.mock import patch
from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from vbv_lernwelt.core.constants import TEST_COURSE_SESSION_BERN_ID
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse
from vbv_lernwelt.notify.management.commands.send_attendance_course_reminders import (
check_attendance_course,
)
from vbv_lernwelt.notify.service import EmailTemplate
@dataclass
class SentNotification:
recipient: User
verb: str
target_url: str
email_template: EmailTemplate
template_data: dict
sent_notifications: list[SentNotification] = []
def on_send_notification(**kwargs) -> None:
sent_notifications.append(SentNotification(**kwargs))
@patch(
"vbv_lernwelt.notify.service.NotificationService.send_information_notification",
on_send_notification,
)
class TestAttendanceCourseReminders(TestCase):
def setUp(self):
create_default_users()
create_test_course(with_sessions=True)
CourseSessionAttendanceCourse.objects.all().delete()
cs_bern = CourseSession.objects.get(
id=TEST_COURSE_SESSION_BERN_ID,
)
self.csac = CourseSessionAttendanceCourse.objects.create(
course_session=cs_bern,
learning_content=LearningContentAttendanceCourse.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
),
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
trainer="Roland Grossenbacher",
)
in_one_week = datetime.now() + timedelta(weeks=1)
self.csac.due_date.start = timezone.make_aware(
in_one_week.replace(hour=7, minute=30, second=0, microsecond=0)
)
self.csac.due_date.end = timezone.make_aware(
in_one_week.replace(hour=16, minute=15, second=0, microsecond=0)
)
self.csac.due_date.save()
# Attendance course more than two weeks in the future
self.csac_future = CourseSessionAttendanceCourse.objects.create(
course_session=cs_bern,
learning_content=LearningContentAttendanceCourse.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
),
location="Handelsschule BV Bern, Zimmer 122",
trainer="Thomas Berger",
)
in_two_weeks = datetime.now() + timedelta(weeks=2, days=1)
self.csac_future.due_date.start = timezone.make_aware(
in_two_weeks.replace(hour=5, minute=20, second=0, microsecond=0)
)
self.csac_future.due_date.end = timezone.make_aware(
in_two_weeks.replace(hour=15, minute=20, second=0, microsecond=0)
)
self.csac_future.due_date.save()
def test_happy_day(self):
check_attendance_course()
self.assertEquals(3, len(sent_notifications))
recipients = CourseSessionUser.objects.filter(
course_session_id=self.csac.course_session.id
)
self.assertEquals(
set(map(lambda n: n.recipient, sent_notifications)),
set(map(lambda csu: csu.user, recipients)),
)
for notification in sent_notifications:
self.assertEquals(
_("Erinnerung: Bald findet ein Präsenzkurs statt"),
notification.verb,
)
self.assertEquals(
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
notification.target_url,
)
self.assertEquals(
self.csac.learning_content.title,
notification.template_data["attendance_course"],
)
self.assertEquals(
self.csac.location,
notification.template_data["location"],
)
self.assertEquals(
self.csac.trainer,
notification.template_data["trainer"],
)
self.assertEquals(
self.csac.due_date.start.strftime("%H:%M %d.%m.%Y"),
notification.template_data["start"],
)
self.assertEquals(
self.csac.due_date.end.strftime("%H:%M %d.%m.%Y"),
notification.template_data["end"],
)

View File

@ -1,18 +1,25 @@
import json import json
from typing import List
from django.test import TestCase from django.test import TestCase
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.tests.factories import UserFactory from vbv_lernwelt.core.tests.factories import UserFactory
from vbv_lernwelt.notify.models import Notification, NotificationType from vbv_lernwelt.notify.models import Notification, NotificationType
from vbv_lernwelt.notify.service import NotificationService from vbv_lernwelt.notify.service import EmailTemplate, NotificationService
class TestNotificationService(TestCase): class TestNotificationService(TestCase):
def _on_send_email(self, *_, **__) -> bool:
self._emails_sent += 1
return True
def _has_sent_emails(self) -> bool:
return self._emails_sent != 0
def setUp(self) -> None: def setUp(self) -> None:
self._emails_sent = 0
self.notification_service = NotificationService self.notification_service = NotificationService
self.notification_service._send_email = lambda a, b, c: True self.notification_service._send_email = self._on_send_email
self.admin = UserFactory(username="admin", email="admin") self.admin = UserFactory(username="admin", email="admin")
self.sender_username = "Bob" self.sender_username = "Bob"
@ -36,11 +43,10 @@ class TestNotificationService(TestCase):
recipient=self.recipient, recipient=self.recipient,
verb=verb, verb=verb,
target_url=target_url, target_url=target_url,
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER,
) )
self.assertEqual(1, Notification.objects.count())
notifications: List[Notification] = Notification.objects.all() notification: Notification = Notification.objects.first()
self.assertEqual(1, len(notifications))
notification = notifications[0]
self.assertEqual(self.admin, notification.actor) self.assertEqual(self.admin, notification.actor)
self.assertEqual(verb, notification.verb) self.assertEqual(verb, notification.verb)
self.assertEqual(target_url, notification.target_url) self.assertEqual(target_url, notification.target_url)
@ -54,12 +60,14 @@ class TestNotificationService(TestCase):
target_url = "https://www.vbv.ch" target_url = "https://www.vbv.ch"
course = "Versicherungsvermittler/in" course = "Versicherungsvermittler/in"
self.notification_service.send_progress_notification( self.notification_service.send_progress_notification(
recipient=self.recipient, verb=verb, target_url=target_url, course=course recipient=self.recipient,
verb=verb,
target_url=target_url,
course=course,
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER,
) )
self.assertEqual(1, Notification.objects.count())
notifications: List[Notification] = Notification.objects.all() notification: Notification = Notification.objects.first()
self.assertEqual(1, len(notifications))
notification = notifications[0]
self.assertEqual(self.admin, notification.actor) self.assertEqual(self.admin, notification.actor)
self.assertEqual(verb, notification.verb) self.assertEqual(verb, notification.verb)
self.assertEqual(target_url, notification.target_url) self.assertEqual(target_url, notification.target_url)
@ -77,11 +85,10 @@ class TestNotificationService(TestCase):
verb=verb, verb=verb,
target_url=target_url, target_url=target_url,
course=course, course=course,
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER,
) )
self.assertEqual(1, Notification.objects.count())
notifications: List[Notification] = Notification.objects.all() notification: Notification = Notification.objects.first()
self.assertEqual(1, len(notifications))
notification = notifications[0]
self.assertEqual(self.sender, notification.actor) self.assertEqual(self.sender, notification.actor)
self.assertEqual(verb, notification.verb) self.assertEqual(verb, notification.verb)
self.assertEqual(target_url, notification.target_url) self.assertEqual(target_url, notification.target_url)
@ -90,3 +97,68 @@ class TestNotificationService(TestCase):
str(NotificationType.USER_INTERACTION), notification.notification_type str(NotificationType.USER_INTERACTION), notification.notification_type
) )
self.assertTrue(notification.emailed) self.assertTrue(notification.emailed)
def test_does_not_send_duplicate_notification(self):
verb = "Anne hat deinen Auftrag bewertet"
target_url = "https://www.vbv.ch"
course = "Versicherungsvermittler/in"
for i in range(2):
self.notification_service.send_user_interaction_notification(
sender=self.sender,
recipient=self.recipient,
verb=verb,
target_url=target_url,
course=course,
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER,
template_data={
"blah": 123,
"foo": "ich habe hunger",
},
)
self.assertEqual(1, Notification.objects.count())
notification: Notification = Notification.objects.first()
self.assertEqual(self.sender, notification.actor)
self.assertEqual(verb, notification.verb)
self.assertEqual(target_url, notification.target_url)
self.assertEqual(course, notification.course)
self.assertEqual(
str(NotificationType.USER_INTERACTION), notification.notification_type
)
self.assertTrue(notification.emailed)
def test_only_sends_email_if_enabled(self):
# Assert no mail is sent if corresponding email notification type is not enabled
self.recipient.additional_json_data["email_notification_types"] = json.dumps(
["INFORMATION"]
)
self.recipient.save()
self.notification_service.send_user_interaction_notification(
sender=self.sender,
recipient=self.recipient,
verb="should not be sent",
target_url="",
course="",
email_template=EmailTemplate.CASEWORK_EVALUATED,
template_data={},
)
self.assertEqual(1, Notification.objects.count())
self.assertFalse(self._has_sent_emails())
# Assert mail is sent if corresponding email notification type is enabled
self.recipient.additional_json_data["email_notification_types"] = json.dumps(
["USER_INTERACTION"]
)
self.recipient.save()
self.notification_service.send_user_interaction_notification(
sender=self.sender,
recipient=self.recipient,
verb="should be sent",
target_url="",
course="",
email_template=EmailTemplate.CASEWORK_EVALUATED,
template_data={},
)
self.assertEqual(2, Notification.objects.count())
self.assertTrue(self._has_sent_emails())

View File

@ -11,5 +11,12 @@
"img base64 content": "regex:data:image/png;base64,.*", "img base64 content": "regex:data:image/png;base64,.*",
"sentry url": "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352", "sentry url": "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352",
"git commit": "bdadf52b849bb5fa47854a3094f4da6fe9d54d02", "git commit": "bdadf52b849bb5fa47854a3094f4da6fe9d54d02",
"customDomainVerificationId": "A2AB57353045150ADA4488FAA8AA9DFBBEDDD311934653F55243B336C2F3358E" "customDomainVerificationId": "A2AB57353045150ADA4488FAA8AA9DFBBEDDD311934653F55243B336C2F3358E",
"SENDGRID_TEMPLATE_ATTENDANCE_COURSE_REMINDER_DE": "d-9af079f98f524d85ac6e4166de3480da",
"SENDGRID_TEMPLATE_ATTENDANCE_COURSE_REMINDER_FR": "d-f88d9912e5484e55a879571463e4a166",
"SENDGRID_TEMPLATE_ATTENDANCE_COURSE_REMINDER_IT": "d-ab78ddca8a7a46b8afe50aaba3efee81",
"SENDGRID_TEMPLATE_CASEWORK_SUBMITTED_DE": "d-599f0b35ddcd4fac99314cdf8f5446a2",
"SENDGRID_TEMPLATE_CASEWORK_EVALUATED_DE": "d-8c57fa13116b47be8eec95dfaf2aa030",
"SENDGRID_TEMPLATE_NEW_FEEDBACK_DE": "d-40fb94d5149949e7b8e7ddfcf0fcfdde",
"SUPERCRONIC_SHA1SUM": "7a79496cf8ad899b99a719355d4db27422396735"
} }