Change email function to use email address directly

This commit is contained in:
Daniel Egger 2023-08-25 11:48:50 +02:00
parent 31af4e933f
commit d83f660918
11 changed files with 179 additions and 93 deletions

Binary file not shown.

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
import os
import sys
import django
sys.path.append("../server")
os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
django.setup()
from vbv_lernwelt.notify.email.email_services import (
EmailTemplate,
send_email,
create_template_data_from_course_session_attendance_course,
)
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
def main():
print("start")
if __name__ == "__main__":
main()
csac = CourseSessionAttendanceCourse.objects.get(pk=1)
print(csac)
print(csac.trainer)
print(csac.due_date)
result = send_email(
to_emails="daniel.egger+sendgrid@gmail.com",
template=EmailTemplate.ATTENDANCE_COURSE_REMINDER,
template_data=create_template_data_from_course_session_attendance_course(csac),
template_language="de",
fail_silently=False,
)
print(result)

View File

@ -30,6 +30,7 @@ django-debug-toolbar # https://github.com/jazzband/django-debug-toolbar
django-extensions # https://github.com/django-extensions/django-extensions django-extensions # https://github.com/django-extensions/django-extensions
django-coverage-plugin # https://github.com/nedbat/django_coverage_plugin django-coverage-plugin # https://github.com/nedbat/django_coverage_plugin
pytest-django # https://github.com/pytest-dev/pytest-django pytest-django # https://github.com/pytest-dev/pytest-django
freezegun # https://github.com/spulec/freezegun
# django-watchfiles custom PR # django-watchfiles custom PR
https://github.com/q0w/django-watchfiles/archive/issue-1.zip https://github.com/q0w/django-watchfiles/archive/issue-1.zip

View File

@ -218,6 +218,8 @@ flake8==6.1.0
# flake8-isort # flake8-isort
flake8-isort==6.0.0 flake8-isort==6.0.0
# via -r requirements-dev.in # via -r requirements-dev.in
freezegun==1.2.2
# via -r requirements-dev.in
gitdb==4.0.10 gitdb==4.0.10
# via gitdb2 # via gitdb2
gitdb2==4.0.2 gitdb2==4.0.2
@ -409,6 +411,7 @@ python-dateutil==2.8.2
# -r requirements.in # -r requirements.in
# botocore # botocore
# faker # faker
# freezegun
python-dotenv==1.0.0 python-dotenv==1.0.0
# via # via
# environs # environs

View File

@ -16,7 +16,8 @@ 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 EmailTemplate, NotificationService from vbv_lernwelt.notify.email.email_services import EmailTemplate
from vbv_lernwelt.notify.services import NotificationService
def update_assignment_completion( def update_assignment_completion(

View File

@ -6,7 +6,8 @@ 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 EmailTemplate, NotificationService from vbv_lernwelt.notify.email.email_services import EmailTemplate
from vbv_lernwelt.notify.services import NotificationService
class FeedbackIntegerField(models.IntegerField): class FeedbackIntegerField(models.IntegerField):

View File

@ -0,0 +1,97 @@
from enum import Enum
import structlog
from django.conf import settings
from django.utils import timezone
from sendgrid import Mail, SendGridAPIClient
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
logger = structlog.get_logger(__name__)
DATETIME_FORMAT_SWISS_STR = "%d.%m.%Y %H:%M"
def format_swiss_datetime(dt: timezone.datetime) -> str:
return dt.astimezone(timezone.get_current_timezone()).strftime(
DATETIME_FORMAT_SWISS_STR
)
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"}
def send_email(
to_emails: str | list[str],
template: EmailTemplate,
template_data: dict,
template_language: str = "de",
fail_silently: bool = True,
) -> bool:
log = logger.bind(
recipient_emails=to_emails,
template=template.name,
template_data=template_data,
template_language=template_language,
)
try:
send_sendgrid_email(
to_emails=to_emails,
template=template,
template_data=template_data,
template_language=template_language,
)
log.info("Email sent successfully")
return True
except Exception as e:
log.error(
"Failed to send Email", exception=str(e), exc_info=True, stack_info=True
)
if not fail_silently:
raise e
return False
def send_sendgrid_email(
to_emails: str | list[str],
template: EmailTemplate,
template_data: dict,
template_language: str = "de",
) -> None:
message = Mail(
from_email="noreply@my.vbv-afa.ch",
to_emails=to_emails,
)
message.template_id = template.value.get(template_language, template.value["de"])
message.dynamic_template_data = template_data
SendGridAPIClient(settings.SENDGRID_API_KEY).send(message)
def create_template_data_from_course_session_attendance_course(
attendance_course: CourseSessionAttendanceCourse,
):
return {
"attendance_course": attendance_course.learning_content.title,
"location": attendance_course.location,
"trainer": attendance_course.trainer,
"start": format_swiss_datetime(attendance_course.due_date.start),
"end": format_swiss_datetime(attendance_course.due_date.end),
}

View File

@ -8,19 +8,18 @@ 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.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.notify.service import EmailTemplate, NotificationService from vbv_lernwelt.notify.email.email_services import (
create_template_data_from_course_session_attendance_course,
EmailTemplate,
)
from vbv_lernwelt.notify.services import NotificationService
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2) PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2)
DATETIME_FORMAT_STR = "%H:%M %d.%m.%Y"
def format_datetime(dt: timezone.datetime) -> str: def send_attendance_course_reminder_notification(
return dt.astimezone(timezone.get_current_timezone()).strftime(DATETIME_FORMAT_STR)
def send_notification(
recipient: User, attendance_course: CourseSessionAttendanceCourse recipient: User, attendance_course: CourseSessionAttendanceCourse
): ):
NotificationService.send_information_notification( NotificationService.send_information_notification(
@ -28,24 +27,20 @@ def send_notification(
verb=_("Erinnerung: Bald findet ein Präsenzkurs statt"), verb=_("Erinnerung: Bald findet ein Präsenzkurs statt"),
target_url=attendance_course.learning_content.get_frontend_url(), target_url=attendance_course.learning_content.get_frontend_url(),
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER,
template_data={ template_data=create_template_data_from_course_session_attendance_course(
"attendance_course": attendance_course.learning_content.title, attendance_course=attendance_course
"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(): def attendance_course_reminder_notification_job():
"""Checks if an attendance course is coming up and sends a reminder to the participants""" """Checks if an attendance course is coming up and sends a reminder to the participants"""
start = timezone.now() start = timezone.now()
end = timezone.now() + PRESENCE_COURSE_REMINDER_LEAD_TIME end = timezone.now() + PRESENCE_COURSE_REMINDER_LEAD_TIME
logger.info( logger.info(
"Querying for attendance courses in specified time range", "Querying for attendance courses in specified time range",
start_time=start.strftime(DATETIME_FORMAT_STR), start_time=start,
end_time=end.strftime(DATETIME_FORMAT_STR), end_time=end,
) )
attendance_courses = CourseSessionAttendanceCourse.objects.filter( attendance_courses = CourseSessionAttendanceCourse.objects.filter(
due_date__start__lte=end, due_date__start__lte=end,
@ -55,11 +50,11 @@ def check_attendance_course():
cs_id = attendance_course.course_session.id cs_id = attendance_course.course_session.id
csu = CourseSessionUser.objects.filter(course_session_id=cs_id) csu = CourseSessionUser.objects.filter(course_session_id=cs_id)
for user in csu: for user in csu:
send_notification(user.user, attendance_course) send_attendance_course_reminder_notification(user.user, attendance_course)
if not attendance_courses: if not attendance_courses:
logger.info("No attendance courses found") logger.info("No attendance courses found")
@click.command() @click.command()
def command(): def command():
check_attendance_course() attendance_course_reminder_notification_job()

View File

@ -1,60 +1,15 @@
from enum import Enum
from typing import Optional from typing import Optional
import structlog import structlog
from notifications.signals import notify from notifications.signals import notify
from sendgrid import Mail, SendGridAPIClient
from storages.utils import setting
from vbv_lernwelt.core.models import User 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 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:
"""Email service class implemented using the Sendgrid API"""
_sendgrid_client = SendGridAPIClient(setting("SENDGRID_API_KEY"))
@classmethod
def send_email(
cls,
recipient: User,
template: EmailTemplate,
template_data: dict,
) -> None:
message = Mail(
from_email="noreply@my.vbv-afa.ch",
to_emails=recipient.email,
)
message.template_id = template.value.get(
recipient.language, template.value["de"]
)
message.dynamic_template_data = template_data
cls._sendgrid_client.send(message)
class NotificationService: class NotificationService:
@classmethod @classmethod
def send_user_interaction_notification( def send_user_interaction_notification(
@ -153,7 +108,7 @@ class NotificationService:
template_name=email_template.name, template_name=email_template.name,
template_data=template_data, template_data=template_data,
): ):
log.warn("A duplicate notification was omitted from being sent") log.info("A duplicate notification was omitted from being sent")
return return
emailed = False emailed = False
@ -200,26 +155,14 @@ class NotificationService:
def _send_email( def _send_email(
recipient: User, recipient: User,
template: EmailTemplate, template: EmailTemplate,
template_data: dict | None, template_data: dict,
) -> bool: ) -> bool:
log = logger.bind( return send_email(
recipient=recipient.username, to_emails=recipient.email,
template=template.name, template=template,
template_data=template_data, template_data=template_data,
template_language=recipient.language,
) )
try:
EmailService.send_email(
recipient=recipient,
template=template,
template_data=template_data,
)
log.info("Email sent successfully")
return True
except Exception as e:
log.error(
"Failed to send Email", exception=str(e), exc_info=True, stack_info=True
)
return False
@staticmethod @staticmethod
def _is_duplicate_notification( def _is_duplicate_notification(

View File

@ -6,6 +6,7 @@ from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from freezegun import freeze_time
from vbv_lernwelt.core.constants import TEST_COURSE_SESSION_BERN_ID 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.core.create_default_users import create_default_users
@ -13,10 +14,10 @@ from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse
from vbv_lernwelt.notify.email.email_services import EmailTemplate
from vbv_lernwelt.notify.management.commands.send_attendance_course_reminders import ( from vbv_lernwelt.notify.management.commands.send_attendance_course_reminders import (
check_attendance_course, attendance_course_reminder_notification_job,
) )
from vbv_lernwelt.notify.service import EmailTemplate
@dataclass @dataclass
@ -36,7 +37,7 @@ def on_send_notification(**kwargs) -> None:
@patch( @patch(
"vbv_lernwelt.notify.service.NotificationService.send_information_notification", "vbv_lernwelt.notify.services.NotificationService.send_information_notification",
on_send_notification, on_send_notification,
) )
class TestAttendanceCourseReminders(TestCase): class TestAttendanceCourseReminders(TestCase):
@ -75,6 +76,9 @@ class TestAttendanceCourseReminders(TestCase):
location="Handelsschule BV Bern, Zimmer 122", location="Handelsschule BV Bern, Zimmer 122",
trainer="Thomas Berger", trainer="Thomas Berger",
) )
@freeze_time("2023-08-25 13:02:01")
def test_happy_day(self):
in_two_weeks = datetime.now() + timedelta(weeks=2, days=1) in_two_weeks = datetime.now() + timedelta(weeks=2, days=1)
self.csac_future.due_date.start = timezone.make_aware( self.csac_future.due_date.start = timezone.make_aware(
in_two_weeks.replace(hour=5, minute=20, second=0, microsecond=0) in_two_weeks.replace(hour=5, minute=20, second=0, microsecond=0)
@ -84,8 +88,7 @@ class TestAttendanceCourseReminders(TestCase):
) )
self.csac_future.due_date.save() self.csac_future.due_date.save()
def test_happy_day(self): attendance_course_reminder_notification_job()
check_attendance_course()
self.assertEquals(3, len(sent_notifications)) self.assertEquals(3, len(sent_notifications))
recipients = CourseSessionUser.objects.filter( recipients = CourseSessionUser.objects.filter(
course_session_id=self.csac.course_session.id course_session_id=self.csac.course_session.id
@ -94,6 +97,7 @@ class TestAttendanceCourseReminders(TestCase):
set(map(lambda n: n.recipient, sent_notifications)), set(map(lambda n: n.recipient, sent_notifications)),
set(map(lambda csu: csu.user, recipients)), set(map(lambda csu: csu.user, recipients)),
) )
for notification in sent_notifications: for notification in sent_notifications:
self.assertEquals( self.assertEquals(
_("Erinnerung: Bald findet ein Präsenzkurs statt"), _("Erinnerung: Bald findet ein Präsenzkurs statt"),
@ -116,10 +120,10 @@ class TestAttendanceCourseReminders(TestCase):
notification.template_data["trainer"], notification.template_data["trainer"],
) )
self.assertEquals( self.assertEquals(
self.csac.due_date.start.strftime("%H:%M %d.%m.%Y"), self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"),
notification.template_data["start"], notification.template_data["start"],
) )
self.assertEquals( self.assertEquals(
self.csac.due_date.end.strftime("%H:%M %d.%m.%Y"), self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"),
notification.template_data["end"], notification.template_data["end"],
) )

View File

@ -4,8 +4,9 @@ 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.email.email_services import EmailTemplate
from vbv_lernwelt.notify.models import Notification, NotificationType from vbv_lernwelt.notify.models import Notification, NotificationType
from vbv_lernwelt.notify.service import EmailTemplate, NotificationService from vbv_lernwelt.notify.services import NotificationService
class TestNotificationService(TestCase): class TestNotificationService(TestCase):