vbv/server/vbv_lernwelt/notify/services.py

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()