Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DI-2715] Implementing send notification task for account app #93

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Comment on lines -49 to +50
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it make more sense to pass email insteead of pk here


@classmethod
def reset_password(cls, signature: str, new_password: str) -> None:
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% raw %}<!doctype html>
{% load static %}
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body class="">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
{% block content %}
{% endblock %}

{% block footer %}
{% endblock footer %}
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>{% endraw %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% raw %}{% extends 'accounts/notifications/base-email.html' %}

{% block content %}
<table role="presentation" class="main">
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<h1>Let’s reset your password</h1>
<p>If you want to reset your password, use the link below. This will take you to the secure page to reset your password.</p>
<p><a href="{{ reset_password_link }}" target="_blank">Reset password</a></p>
<p>If you don't want to reset your password, please ignore this message. Your password will not be reset.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
{% endblock %}{% endraw %}