vbv/server/vbv_lernwelt/notify/services.py

227 lines
7.4 KiB
Python

from typing import Optional
import structlog
from notifications.signals import notify
from vbv_lernwelt.core.models import User
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
from vbv_lernwelt.notify.models import Notification, NotificationType
logger = structlog.get_logger(__name__)
class NotificationService:
@classmethod
def send_user_interaction_notification(
cls,
recipient: User,
verb: str,
sender: User,
course: str,
target_url: str,
email_template: EmailTemplate,
template_data: dict = None,
) -> str:
return cls._send_notification(
recipient=recipient,
verb=verb,
sender=sender,
course=course,
target_url=target_url,
notification_type=NotificationType.USER_INTERACTION,
email_template=email_template,
template_data=template_data,
)
@classmethod
def send_progress_notification(
cls,
recipient: User,
verb: str,
course: str,
target_url: str,
email_template: EmailTemplate,
template_data: dict = None,
) -> str:
return cls._send_notification(
recipient=recipient,
verb=verb,
course=course,
target_url=target_url,
notification_type=NotificationType.PROGRESS,
email_template=email_template,
template_data=template_data,
)
@classmethod
def send_information_notification(
cls,
recipient: User,
verb: str,
target_url: str,
email_template: EmailTemplate,
template_data: dict = None,
) -> str:
return cls._send_notification(
recipient=recipient,
verb=verb,
target_url=target_url,
notification_type=NotificationType.INFORMATION,
email_template=email_template,
template_data=template_data,
)
@classmethod
def _send_notification(
cls,
recipient: User,
verb: str,
notification_type: NotificationType,
email_template: EmailTemplate,
template_data: dict | None = None,
sender: User | None = None,
course: str | None = None,
target_url: str | None = None,
fail_silently: bool = True,
) -> str:
if template_data is None:
template_data = {}
notification_identifier = f"{notification_type.name}_{email_template.name}"
actor_avatar_url: Optional[str] = None
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_type=notification_type,
course=course,
target_url=target_url,
template_data=template_data,
)
emailed = False
try:
notification = NotificationService._find_duplicate_notification(
recipient=recipient,
verb=verb,
notification_type=notification_type,
target_url=target_url,
template_name=email_template.name,
template_data=template_data,
)
emailed = False
if notification and notification.emailed:
emailed = True
if cls._should_send_email(notification_type, 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,
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.target_url = target_url
sent_notification.notification_type = notification_type
sent_notification.course = course
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_type: NotificationType, recipient: User
) -> bool:
return str(notification_type) in recipient.additional_json_data.get(
"email_notification_types", []
)
@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_type: NotificationType,
template_name: str,
template_data: dict,
target_url: str | 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_type=notification_type,
target_url=target_url,
data={
"email_template": template_name,
"template_data": template_data,
},
).first()