diff --git a/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/services/notifications.py b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/services/notifications.py
new file mode 100644
index 0000000..172be14
--- /dev/null
+++ b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/services/notifications.py
@@ -0,0 +1,82 @@
+import logging
+
+from dataclasses import dataclass
+
+from django.conf import settings
+from django.core.mail import send_mail
+from django.template.loader import get_template
+from django.utils.html import strip_tags
+
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class NotificationHandlerConfig:
+ # Unique identifier of the notification.
+ # if no templates are specified, the templates will be named after this
+ kind: str
+ subject: str
+ # Name of the template to use for the html version of the email.
+ # It should be relative to the account/notifications directory.
+ template_html_filename: str | None = None
+ # Name of the template to use for the plain text version of the email.
+ # It should be relative to the account/notifications directory.
+ template_plain_filename: str | None = None
+ # Email address to use as the sender of the email.
+ from_email: str = settings.DEFAULT_FROM_EMAIL
+
+ @property
+ def template_html_name(self):
+ filename = self.template_html_filename or f"{self.kind}.html"
+ return f"accounts/notifications/{filename}"
+
+ @property
+ def template_plain_name(self):
+ filename = self.template_plain_filename or f"{self.kind}.html"
+ return f"accounts/notifications/{filename}"
+
+ @property
+ def html_only(self) -> bool:
+ return self.template_plain_name == self.template_html_name
+
+
+_HANDLERS = [
+ NotificationHandlerConfig(
+ kind="user-reset-password",
+ subject="Reset your password",
+ ),
+]
+
+HANDLERS = {handler.kind: handler for handler in _HANDLERS}
+
+
+def notification(user_email: str, kind: str, context: dict) -> None:
+ try:
+ config = HANDLERS[kind]
+ except KeyError:
+ logger.warning("Attempted to send notification of unknown kind %s", kind)
+ return
+
+ template_html = get_template(config.template_html_name)
+ template_plain = get_template(config.template_plain_name)
+
+ message_html = template_html.render(context)
+
+ if config.html_only:
+ message_plain = strip_tags(message_html)
+ else:
+ message_plain = template_plain.render(context)
+
+ res = send_mail(
+ subject=config.subject,
+ message=message_plain,
+ from_email=config.from_email,
+ recipient_list=[user_email],
+ html_message=message_html,
+ )
+
+ if res != 1:
+ logger.warning(f"Failed to send {kind} notification to user {user_email}")
+
+ return
diff --git a/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/services/password.py b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/services/password.py
index 3378243..ae04a77 100644
--- a/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/services/password.py
+++ b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/services/password.py
@@ -11,6 +11,7 @@
WrongPasswordError,
)
from {{ cookiecutter.project_slug }}.apps.accounts.models import UserAccount
+from {{ cookiecutter.project_slug }}.apps.accounts.tasks import send_notification
class PasswordService:
@@ -46,7 +47,7 @@ def send_reset_password_link(cls, email: str) -> None:
"domain_name": domain_name,
"reset_password_link": reset_password_link,
}
- cls._send_notification(user.pk, context)
+ cls._send_notification(user.email, context)
@classmethod
def reset_password(cls, signature: str, new_password: str) -> None:
@@ -60,9 +61,8 @@ def reset_password(cls, signature: str, new_password: str) -> None:
cls.change_password(user, new_password)
@staticmethod
- def _send_notification(user_pk: str, context: dict):
- # send_notification.delay(user_pk, "user-reset-password", context) # TODO: Implement notification sending
- pass
+ def _send_notification(user_email: str, context: dict):
+ send_notification.delay(user_email, "user-reset-password", context)
@staticmethod
def _generate_reset_password_signature(user: UserAccount) -> str:
diff --git a/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/tasks.py b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/tasks.py
new file mode 100644
index 0000000..17724ae
--- /dev/null
+++ b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/tasks.py
@@ -0,0 +1,14 @@
+import logging
+
+from {{ cookiecutter.project_slug }} import celery_app
+from {{ cookiecutter.project_slug }}.apps.accounts.services.notifications import notification
+
+
+logger = logging.getLogger(__name__)
+
+
+@celery_app.task
+def send_notification(user_email: str, kind: str, context: dict):
+ logger.debug(f"Sending {kind} notification to user {user_email}")
+
+ notification(user_email, kind, context)
diff --git a/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/tests/test_services/test_password_service.py b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/tests/test_services/test_password_service.py
index 77d28c5..bffd2de 100644
--- a/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/tests/test_services/test_password_service.py
+++ b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/apps/accounts/tests/test_services/test_password_service.py
@@ -111,7 +111,7 @@ def test_send_reset_password_link_success(settings, user_account, mocker):
mocked_generate_reset_password_signature.assert_called_once_with(user)
mocked_send_notification.assert_called_once_with(
- user.pk,
+ user.email,
{
"user_notification_salutation": "Dear client",
"domain_name": domain_name,
diff --git a/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/templates/accounts/notifications/base-email.html b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/templates/accounts/notifications/base-email.html
new file mode 100644
index 0000000..88bfcd9
--- /dev/null
+++ b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/templates/accounts/notifications/base-email.html
@@ -0,0 +1,25 @@
+{% raw %}
+{% load static %}
+
+
+
+
+
+
+
+
+ |
+
+
+ {% block content %}
+ {% endblock %}
+
+ {% block footer %}
+ {% endblock footer %}
+
+ |
+ |
+
+
+
+{% endraw %}
diff --git a/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/templates/accounts/notifications/user-reset-password.html b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/templates/accounts/notifications/user-reset-password.html
new file mode 100644
index 0000000..978d0b4
--- /dev/null
+++ b/{{ cookiecutter.project_slug }}/api/{{ cookiecutter.project_slug }}/templates/accounts/notifications/user-reset-password.html
@@ -0,0 +1,20 @@
+{% raw %}{% extends 'accounts/notifications/base-email.html' %}
+
+{% block content %}
+
+
+
+
+
+
+ Let’s reset your password
+ If you want to reset your password, use the link below. This will take you to the secure page to reset your password.
+ Reset password
+ If you don't want to reset your password, please ignore this message. Your password will not be reset.
+ |
+
+
+ |
+
+
+{% endblock %}{% endraw %}