508 lines
19 KiB
Python
508 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import structlog
|
|
from django.core.exceptions import ValidationError
|
|
from django.core.validators import validate_email
|
|
from django.db.models import Model
|
|
from notifications.signals import notify
|
|
|
|
from vbv_lernwelt.assignment.models import AssignmentType
|
|
from vbv_lernwelt.core.models import User
|
|
from vbv_lernwelt.course.models import CourseSession
|
|
from vbv_lernwelt.course_session.models import CourseSessionAssignment
|
|
from vbv_lernwelt.notify.email.email_services import (
|
|
EmailTemplate,
|
|
create_template_data_from_course_session_attendance_course,
|
|
format_swiss_datetime,
|
|
send_email,
|
|
)
|
|
from vbv_lernwelt.notify.models import (
|
|
Notification,
|
|
NotificationCategory,
|
|
NotificationTrigger,
|
|
)
|
|
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
|
|
|
|
if TYPE_CHECKING:
|
|
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
|
from vbv_lernwelt.course_session.models import (
|
|
CourseSessionAttendanceCourse,
|
|
CourseSessionEdoniqTest,
|
|
)
|
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
|
class NotificationService:
|
|
@classmethod
|
|
def send_assignment_submitted_notification(
|
|
cls, recipient: User, sender: User, assignment_completion: AssignmentCompletion
|
|
):
|
|
if (
|
|
assignment_completion.assignment.assignment_type
|
|
== AssignmentType.PRAXIS_ASSIGNMENT.value
|
|
):
|
|
texts = {
|
|
"de": "%(sender)s hat den Praxisauftrag «%(assignment_title)s» abgegeben.",
|
|
"fr": "%(sender)s a soumis l'exercice pratique «%(assignment_title)s».",
|
|
"it": "%(sender)s ha consegnato il lavoro pratico «%(assignment_title)s».",
|
|
}
|
|
# this was the default case before the praxis assignment was introduced
|
|
else:
|
|
texts = {
|
|
"de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben.",
|
|
"fr": "%(sender)s a soumis l'étude de cas dirigée «%(assignment_title)s».",
|
|
"it": "%(sender)s ha consegnato il caso di studio guidato «%(assignment_title)s».",
|
|
}
|
|
verb = texts.get(recipient.language, "de") % {
|
|
"sender": sender.get_full_name(),
|
|
"assignment_title": assignment_completion.assignment.title,
|
|
}
|
|
|
|
return cls._send_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.USER_INTERACTION,
|
|
notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED,
|
|
sender=sender,
|
|
target_url=assignment_completion.get_assignment_evaluation_frontend_url(),
|
|
course_session=assignment_completion.course_session,
|
|
action_object=assignment_completion,
|
|
email_template=EmailTemplate.CASEWORK_SUBMITTED,
|
|
)
|
|
|
|
@classmethod
|
|
def send_self_evaluation_feedback_request_feedback_notification(
|
|
cls,
|
|
self_evaluation_feedback: SelfEvaluationFeedback,
|
|
):
|
|
"""Requester -> Provider"""
|
|
requester_user = self_evaluation_feedback.feedback_requester_user
|
|
provider_user = self_evaluation_feedback.feedback_provider_user
|
|
|
|
texts = {
|
|
"de": "%(requester)s hat eine Selbsteinschätzung mit dir geteilt",
|
|
"fr": "%(requester)s a partagé une auto-évaluation avec vous",
|
|
"it": "%(requester)s ha condiviso una valutazione personale con te",
|
|
}
|
|
|
|
verb = texts.get(provider_user.language, "de") % {
|
|
"requester": requester_user.get_full_name(),
|
|
}
|
|
|
|
return cls._send_notification(
|
|
recipient=provider_user,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.USER_INTERACTION,
|
|
notification_trigger=NotificationTrigger.SELF_EVALUATION_FEEDBACK_REQUESTED,
|
|
sender=requester_user,
|
|
target_url=self_evaluation_feedback.feedback_provider_evaluation_url,
|
|
action_object=self_evaluation_feedback,
|
|
email_template=EmailTemplate.SELF_EVALUATION_FEEDBACK_REQUESTED,
|
|
template_data={
|
|
"mentee_name": requester_user.get_full_name(),
|
|
"mentee_email": requester_user.email,
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def send_self_evaluation_feedback_received_notification(
|
|
cls,
|
|
self_evaluation_feedback: SelfEvaluationFeedback,
|
|
):
|
|
"""Provider -> Requester"""
|
|
requester_user = self_evaluation_feedback.feedback_requester_user
|
|
provider_user = self_evaluation_feedback.feedback_provider_user
|
|
|
|
texts = {
|
|
"de": "%(provider)s hat dir eine Fremdeinschätzung gegeben",
|
|
"fr": "%(provider)s vous a donné une évaluation externe",
|
|
"it": "%(provider)s ti ha dato una valutazione esterna",
|
|
}
|
|
|
|
verb = texts.get(requester_user.language, "de") % {
|
|
"provider": provider_user.get_full_name(),
|
|
}
|
|
|
|
return cls._send_notification(
|
|
recipient=requester_user,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.USER_INTERACTION,
|
|
notification_trigger=NotificationTrigger.SELF_EVALUATION_FEEDBACK_PROVIDED,
|
|
sender=provider_user,
|
|
target_url=self_evaluation_feedback.feedback_requester_results_url,
|
|
action_object=self_evaluation_feedback,
|
|
email_template=EmailTemplate.SELF_EVALUATION_FEEDBACK_PROVIDED,
|
|
template_data={
|
|
"mentor_name": provider_user.get_full_name(),
|
|
"mentor_email": provider_user.email,
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def send_assignment_evaluated_notification(
|
|
cls,
|
|
recipient: User,
|
|
sender: User,
|
|
assignment_completion: AssignmentCompletion,
|
|
target_url: str,
|
|
):
|
|
if (
|
|
assignment_completion.assignment.assignment_type
|
|
== AssignmentType.PRAXIS_ASSIGNMENT.value
|
|
):
|
|
texts = {
|
|
"de": "%(sender)s hat den Praxisauftrag «%(assignment_title)s» bewertet.",
|
|
"fr": "%(sender)s a évalué l'exercice pratique «%(assignment_title)s».",
|
|
"it": "%(sender)s ha valutato il lavoro pratico «%(assignment_title)s».",
|
|
}
|
|
# this was the default case before the praxis assignment was introduced
|
|
else:
|
|
texts = {
|
|
"de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet.",
|
|
"fr": "%(sender)s a évalué l'étude de cas dirigée «%(assignment_title)s».",
|
|
"it": "%(sender)s ha valutato il caso di studio guidato «%(assignment_title)s».",
|
|
}
|
|
verb = texts.get(recipient.language, "de") % {
|
|
"sender": sender.get_full_name(),
|
|
"assignment_title": assignment_completion.assignment.title,
|
|
}
|
|
|
|
return cls._send_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.USER_INTERACTION,
|
|
notification_trigger=NotificationTrigger.CASEWORK_EVALUATED,
|
|
sender=sender,
|
|
target_url=target_url,
|
|
course_session=assignment_completion.course_session,
|
|
action_object=assignment_completion,
|
|
email_template=EmailTemplate.CASEWORK_EVALUATED,
|
|
)
|
|
|
|
@classmethod
|
|
def send_new_feedback_notification(
|
|
cls,
|
|
recipient: User,
|
|
feedback_response: FeedbackResponse,
|
|
):
|
|
texts = {
|
|
"de": "Feedback abgeschickt für Circle «%(circle_title)s»",
|
|
"fr": "Feedback envoyé pour le cercle «%(circle_title)s»",
|
|
"it": "Feedback inviato per il cerchio «%(circle_title)s»",
|
|
}
|
|
verb = texts.get(recipient.language, "de") % {
|
|
"circle_title": feedback_response.circle.title,
|
|
}
|
|
|
|
return cls._send_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.INFORMATION,
|
|
notification_trigger=NotificationTrigger.NEW_FEEDBACK,
|
|
target_url=f"/course/{feedback_response.course_session.course.slug}/cockpit/feedback/{feedback_response.circle_id}/",
|
|
course_session=feedback_response.course_session,
|
|
action_object=feedback_response,
|
|
email_template=EmailTemplate.NEW_FEEDBACK,
|
|
)
|
|
|
|
@classmethod
|
|
def send_attendance_course_reminder_notification(
|
|
cls,
|
|
recipient: User,
|
|
attendance_course: CourseSessionAttendanceCourse,
|
|
):
|
|
texts = {
|
|
"de": "Erinnerung: Bald findet ein Präsenzkurs statt",
|
|
"fr": "Rappel: Un cours de présence aura lieu bientôt.",
|
|
"it": "Promemoria: Un corso di presenza avrà luogo presto.",
|
|
}
|
|
verb = texts.get(recipient.language, "de")
|
|
|
|
return cls._send_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.INFORMATION,
|
|
notification_trigger=NotificationTrigger.ATTENDANCE_COURSE_REMINDER,
|
|
target_url=attendance_course.learning_content.get_frontend_url(),
|
|
action_object=attendance_course,
|
|
course_session=attendance_course.course_session,
|
|
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER,
|
|
template_data=create_template_data_from_course_session_attendance_course(
|
|
attendance_course=attendance_course
|
|
),
|
|
)
|
|
|
|
@classmethod
|
|
def send_assignment_reminder_notification_member(
|
|
cls,
|
|
recipient: User,
|
|
assignment: CourseSessionAssignment,
|
|
):
|
|
texts = {
|
|
"de": "Erinnerung: Bald ist ein Abgabetermin",
|
|
"fr": "Rappel: Une date limite approche",
|
|
"it": "Promemoria: Una scadenza si avvicina",
|
|
}
|
|
|
|
templates = {
|
|
AssignmentType.CASEWORK: EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER,
|
|
AssignmentType.PREP_ASSIGNMENT: EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER,
|
|
}
|
|
|
|
verb = texts.get(recipient.language, "de")
|
|
circle = assignment.learning_content.get_parent_circle().title
|
|
due_at = assignment.submission_deadline.start
|
|
|
|
return cls._send_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.INFORMATION,
|
|
notification_trigger=NotificationTrigger.ASSIGNMENT_REMINDER,
|
|
target_url=assignment.learning_content.get_frontend_url(),
|
|
action_object=assignment,
|
|
course_session=assignment.course_session,
|
|
email_template=templates[
|
|
AssignmentType(assignment.learning_content.assignment_type)
|
|
],
|
|
template_data={
|
|
"circle": circle,
|
|
"due_date": format_swiss_datetime(due_at),
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def send_edoniq_test_reminder_notification_member(
|
|
cls,
|
|
recipient: User,
|
|
edoniq_test: CourseSessionEdoniqTest,
|
|
):
|
|
texts = {
|
|
"de": "Erinnerung: Bald ist ein Abgabetermin",
|
|
"fr": "Rappel: Une date limite approche",
|
|
"it": "Promemoria: Una scadenza si avvicina",
|
|
}
|
|
|
|
verb = texts.get(recipient.language, "de")
|
|
circle = edoniq_test.learning_content.get_parent_circle().title
|
|
due_at = edoniq_test.deadline.start
|
|
|
|
return cls._send_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.INFORMATION,
|
|
notification_trigger=NotificationTrigger.ASSIGNMENT_REMINDER,
|
|
target_url=edoniq_test.learning_content.get_frontend_url(),
|
|
action_object=edoniq_test,
|
|
course_session=edoniq_test.course_session,
|
|
email_template=EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER,
|
|
template_data={
|
|
"circle": circle,
|
|
"due_date": format_swiss_datetime(due_at),
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def send_casework_expert_evaluation_reminder(
|
|
cls,
|
|
recipient: User,
|
|
assignment: CourseSessionAssignment,
|
|
):
|
|
texts = {
|
|
"de": "Erinnerung: Bald ist ein Bewertungstermin",
|
|
"fr": "Rappel: Une date limite approche",
|
|
"it": "Promemoria: Una scadenza si avvicina",
|
|
}
|
|
|
|
verb = texts.get(recipient.language, "de")
|
|
circle = assignment.learning_content.get_parent_circle().title
|
|
due_at = assignment.evaluation_deadline.start
|
|
|
|
return cls._send_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
notification_category=NotificationCategory.INFORMATION,
|
|
notification_trigger=NotificationTrigger.CASEWORK_EXPERT_EVALUATION_REMINDER,
|
|
target_url=assignment.evaluation_deadline.url_expert,
|
|
action_object=assignment,
|
|
course_session=assignment.course_session,
|
|
email_template=EmailTemplate.EVALUATION_REMINDER_CASEWORK_EXPERT,
|
|
template_data={
|
|
"circle": circle,
|
|
"due_date": format_swiss_datetime(due_at),
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def _send_notification(
|
|
cls,
|
|
recipient: User,
|
|
verb: str,
|
|
notification_category: NotificationCategory,
|
|
notification_trigger: NotificationTrigger,
|
|
sender: User | None = None,
|
|
action_object: Model | None = None,
|
|
target_url: str | None = None,
|
|
course_session: CourseSession | None = None,
|
|
email_template: EmailTemplate | None = None,
|
|
template_data: dict | None = None,
|
|
fail_silently: bool = True,
|
|
) -> str:
|
|
if template_data is None:
|
|
template_data = {}
|
|
|
|
notification_identifier = (
|
|
f"{notification_category.name}_{notification_trigger.name}"
|
|
)
|
|
|
|
actor_avatar_url = ""
|
|
if not sender:
|
|
sender = User.objects.get(email="admin")
|
|
else:
|
|
actor_avatar_url = sender.avatar_url
|
|
log = logger.bind(
|
|
recipient=recipient.email,
|
|
sender=sender.email,
|
|
verb=verb,
|
|
notification_category=notification_category,
|
|
notification_trigger=notification_trigger,
|
|
course_session=course_session.title if course_session else "",
|
|
target_url=target_url,
|
|
template_data=template_data,
|
|
)
|
|
emailed = False
|
|
|
|
try:
|
|
validate_email(recipient.email)
|
|
except ValidationError:
|
|
log.info("Recipient email is invalid")
|
|
return f"{notification_identifier}_invalid_email"
|
|
|
|
try:
|
|
notification = NotificationService._find_duplicate_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
notification_category=notification_category,
|
|
notification_trigger=notification_trigger,
|
|
target_url=target_url,
|
|
course_session=course_session,
|
|
)
|
|
emailed = False
|
|
if notification and notification.emailed:
|
|
emailed = True
|
|
|
|
if (
|
|
email_template
|
|
and cls._should_send_email(notification_category, recipient)
|
|
and not emailed
|
|
):
|
|
log.debug("Try to send email")
|
|
try:
|
|
emailed = cls._send_email(
|
|
recipient=recipient,
|
|
template=email_template,
|
|
template_data={
|
|
"target_url": f"https://my.vbv-afa.ch{target_url}",
|
|
**template_data,
|
|
},
|
|
fail_silently=False,
|
|
)
|
|
except Exception as e:
|
|
notification_identifier += "_email_error"
|
|
if not fail_silently:
|
|
raise e
|
|
return notification_identifier
|
|
|
|
if emailed:
|
|
notification_identifier += "_emailed"
|
|
if notification:
|
|
notification.emailed = True
|
|
notification.save()
|
|
else:
|
|
log.debug("Should not send email")
|
|
|
|
if notification:
|
|
log.info("Duplicate notification was omitted from being sent")
|
|
notification_identifier += "_duplicate"
|
|
return notification_identifier
|
|
|
|
else:
|
|
response = notify.send(
|
|
sender=sender,
|
|
recipient=recipient,
|
|
verb=verb,
|
|
action_object=action_object,
|
|
emailed=emailed,
|
|
# The extra arguments are saved in the 'data' member
|
|
email_template=email_template.name if email_template else "",
|
|
template_data=template_data,
|
|
)
|
|
|
|
sent_notification: Notification = response[0][1][0] # 🫨
|
|
sent_notification.target_url = target_url
|
|
sent_notification.notification_category = notification_category
|
|
sent_notification.notification_trigger = notification_trigger
|
|
sent_notification.course_session = course_session
|
|
sent_notification.actor_avatar_url = actor_avatar_url
|
|
sent_notification.save()
|
|
log.info("Notification sent successfully", emailed=emailed)
|
|
return f"{notification_identifier}_success"
|
|
except Exception as e:
|
|
log.error(
|
|
"Failed to send notification",
|
|
exception=str(e),
|
|
exc_info=True,
|
|
stack_info=True,
|
|
emailed=emailed,
|
|
)
|
|
if not fail_silently:
|
|
raise e
|
|
return f"{notification_identifier}_error"
|
|
|
|
@staticmethod
|
|
def _should_send_email(
|
|
notification_category: NotificationCategory, recipient: User
|
|
) -> bool:
|
|
return str(notification_category) in recipient.additional_json_data.get(
|
|
"email_notification_categories", []
|
|
)
|
|
|
|
@staticmethod
|
|
def _send_email(
|
|
recipient: User,
|
|
template: EmailTemplate,
|
|
template_data: dict,
|
|
fail_silently: bool = True,
|
|
) -> bool:
|
|
return send_email(
|
|
recipient_email=recipient.email,
|
|
template=template,
|
|
template_data=template_data,
|
|
template_language=recipient.language,
|
|
fail_silently=fail_silently,
|
|
)
|
|
|
|
@staticmethod
|
|
def _find_duplicate_notification(
|
|
recipient: User,
|
|
verb: str,
|
|
notification_category: NotificationCategory,
|
|
notification_trigger: NotificationTrigger,
|
|
target_url: str | None,
|
|
course_session: CourseSession | None,
|
|
) -> Notification | None:
|
|
"""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_category=notification_category,
|
|
notification_trigger=notification_trigger,
|
|
target_url=target_url,
|
|
course_session=course_session,
|
|
).first()
|