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

Set up handlers to build and send email against the form fields on th… #1791

Open
wants to merge 2 commits into
base: task/update-feedback-api-to-integrate-with-feedback-page/CDD-2103
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
69 changes: 68 additions & 1 deletion feedback/email_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.core.mail import EmailMessage

import config
from feedback.email_template import build_body_for_email
from feedback.email_template import build_body_for_email, build_body_for_email_v2

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,6 +45,40 @@ def send_email(
return bool(email.send(fail_silently=fail_silently))


def send_email_v2(
*,
suggestions: dict[str, str],
subject: str = DEFAULT_FEEDBACK_EMAIL_SUBJECT,
recipient_email_address: str = DEFAULT_FEEDBACK_EMAIL_RECIPIENT_ADDRESS,
fail_silently: bool = True,
) -> bool:
"""Sends a feedback email to the `recipient_email_address`

Args:
suggestions: Dict of feedback suggestions from the user
E.g. {"question": "some question", "answer": "some answer"}
subject: The subject line to set on the email message
Defaults to "Suggestions Feedback for UKHSA data dashboard"
recipient_email_address: The recipient to send the email to
Defaults to the environment variable
`FEEDBACK_EMAIL_RECIPIENT_ADDRESS`
fail_silently: Switch to raise an exception
if the email failed and was not successfully sent.
Defaults to True

Returns:
A boolean to describe whether the email has been sent successfully

"""
email = create_email_message_v2(
suggestions=suggestions,
subject=subject,
recipient_email_address=recipient_email_address,
)

return bool(email.send(fail_silently=fail_silently))


def create_email_message(
*,
suggestions: dict[str, str],
Expand Down Expand Up @@ -76,3 +110,36 @@ def create_email_message(
body=body,
to=[recipient_email_address],
)


def create_email_message_v2(
*,
suggestions: dict[str, str],
subject: str = DEFAULT_FEEDBACK_EMAIL_SUBJECT,
recipient_email_address: str = DEFAULT_FEEDBACK_EMAIL_RECIPIENT_ADDRESS,
) -> EmailMessage:
"""Creates an `EmailMessage` object for a feedback email

Notes:
This returns an `EmailMessage` object which **has not been sent**

Args:
suggestions: Dict of feedback suggestions from the user
E.g. {"question": "some question", "answer": "some answer"}
subject: The subject line to set on the email message
Defaults to "Suggestions Feedback for UKHSA data dashboard"
recipient_email_address: The recipient to send the email to
Defaults to the environment variable
`FEEDBACK_EMAIL_RECIPIENT_ADDRESS`
Returns:
An `EmailMessage` object with the subject, body and recipient
set on the object

"""
body: str = build_body_for_email_v2(suggestions=suggestions)

return EmailMessage(
subject=subject,
body=body,
to=[recipient_email_address],
)
50 changes: 50 additions & 0 deletions feedback/email_template.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from enum import Enum

from django.db.models import Manager

from feedback.cms_interface import CMSInterface

FALLBACK_DID_YOU_FIND_EVERYTHING_ANSWER: str = "User provided no input"
DEFAULT_FORM_PAGE_MANAGER = CMSInterface().get_form_page_manager()


def build_body_for_email(*, suggestions: dict[str, str]) -> str:
Expand All @@ -22,6 +27,25 @@ def build_body_for_email(*, suggestions: dict[str, str]) -> str:
return _build_body_from_suggestions(suggestions=enriched_suggestions)


def build_body_for_email_v2(*, suggestions: dict[str, str]) -> str:
"""Builds the suggestions email body as a string to be sent to the email server

Args:
suggestions: A dict containing:
keys - Shortform questions
values - Answers provided by the user

Returns:
A continuous string which represents
the body of the feedback email

"""
enriched_suggestions: dict[str, str] = (
_enrich_suggestions_with_long_form_questions_v2(suggestions=suggestions)
)
return _build_body_from_suggestions(suggestions=enriched_suggestions)


class FeedbackQuestion(Enum):
reason = "What was your reason for visiting the dashboard today?"
did_you_find_everything = "Did you find everything you were looking for?"
Expand Down Expand Up @@ -73,6 +97,32 @@ def _enrich_suggestions_with_long_form_questions(
return long_form_suggestions


def _enrich_suggestions_with_long_form_questions_v2(
*,
suggestions: dict[str, str],
form_page_manager: Manager = DEFAULT_FORM_PAGE_MANAGER,
) -> dict[str, str]:
"""Enriches the question keys in the given `suggestions` with the long form versions

Examples:
`suggestions` is provided with the following key value pair:
`"reason": "some answer"`
Then the return key value pair would be:
`"What was your reason for visiting the dashboard today?": "some answer"`

Args:
suggestions: Dict of feedback suggestions from the user

Returns:
A dict of feedback suggestions with longform questions as the keys
and answers for the values.

"""
form_fields = form_page_manager.get_feedback_page_form_fields()

return {field.label: suggestions.get(field.clean_name) for field in form_fields}


def _build_body_from_suggestions(*, suggestions: dict[str, str]) -> str:
"""Builds the suggestions email body as a string to be sent to the email server

Expand Down
193 changes: 193 additions & 0 deletions tests/unit/feedback/test_email_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
DEFAULT_FEEDBACK_EMAIL_SUBJECT,
create_email_message,
send_email,
create_email_message_v2,
send_email_v2,
)

MODULE_PATH = "feedback.email_server"
Expand Down Expand Up @@ -123,6 +125,130 @@ def test_sets_specified_recipient_on_email_message_when_provided(self):
assert self.fake_recipient_email_address in email_message.recipients()


class TestCreateEmailMessageV2:
fake_recipient_email_address = "[email protected]"

@mock.patch(f"{MODULE_PATH}.build_body_for_email_v2")
def test_returns_email_message(
self, mocked_build_body_for_email_v2: mock.MagicMock
):
"""
Given a suggestions dict and a recipient email address
When `create_email_message()` is called
Then an instance of `EmailMessage` is returned
"""
# Given
mocked_suggestions = mock.MagicMock()
fake_subject = "Test subject"

# When
email_message: EmailMessage = create_email_message_v2(
suggestions=mocked_suggestions,
subject=fake_subject,
recipient_email_address=self.fake_recipient_email_address,
)

# Then
assert type(email_message) is EmailMessage

@mock.patch(f"{MODULE_PATH}.build_body_for_email_v2")
def test_sets_correct_body_on_email_message(
self, spy_build_body_for_email: mock.MagicMock
):
"""
Given a suggestions dict and a recipient email address
When `create_email_message()` is called
The body is set by delegating a call to `build_body_for_email()`

Patches:
`spy_build_body_for_email`: For the main assertion.
To check this is used for the `body`
of the created `EmailMessage`
"""
# Given
mocked_suggestions = mock.Mock()
fake_subject = "Test subject"

# When
email_message: EmailMessage = create_email_message_v2(
suggestions=mocked_suggestions,
subject=fake_subject,
recipient_email_address=self.fake_recipient_email_address,
)

# Then
spy_build_body_for_email.assert_called_once_with(suggestions=mocked_suggestions)
assert email_message.body == spy_build_body_for_email.return_value

@mock.patch(f"{MODULE_PATH}.build_body_for_email_v2")
def test_sets_default_subject_on_email_message_when_not_provided(
self, mocked_build_body_for_email_v2: mock.MagicMock
):
"""
Given a suggestions dict and a recipient email address
When `create_email_message()` is called
Then the default subject line is set on the email message
"""
# Given
mocked_suggestions = mock.MagicMock()

# When
email_message: EmailMessage = create_email_message_v2(
suggestions=mocked_suggestions,
recipient_email_address=self.fake_recipient_email_address,
)

# Then
assert email_message.subject == DEFAULT_FEEDBACK_EMAIL_SUBJECT

@mock.patch(f"{MODULE_PATH}.build_body_for_email_v2")
def test_sets_specified_subject_on_email_message_when_provided(
self, mocked_build_body_for_email_v2: mock.MagicMock
):
"""
Given a suggestions dict and a recipient email address
And a specified non-default subject line
When `create_email_message()` is called
Then the specified subject line is set on the email message
"""
# Given
mocked_suggestions = mock.MagicMock()
fake_non_default_subject = "Specified example subject"

# When
email_message: EmailMessage = create_email_message_v2(
suggestions=mocked_suggestions,
recipient_email_address=self.fake_recipient_email_address,
subject=fake_non_default_subject,
)

# Then
assert email_message.subject == fake_non_default_subject

@mock.patch(f"{MODULE_PATH}.build_body_for_email_v2")
def test_sets_specified_recipient_on_email_message_when_provided(
self, mocked_build_body_for_email_v2: mock.MagicMock
):
"""
Given a suggestions dict and a specified recipient email address
When `create_email_message()` is called
Then the specified recipient is set on the recipients of the email message
"""
# Given
mocked_suggestions = mock.MagicMock()
fake_non_default_subject = "Specified example subject"

# When
email_message: EmailMessage = create_email_message_v2(
suggestions=mocked_suggestions,
recipient_email_address=self.fake_recipient_email_address,
subject=fake_non_default_subject,
)

# Then
assert self.fake_recipient_email_address in email_message.recipients()


class TestSendEmail:
@mock.patch(f"{MODULE_PATH}.create_email_message")
def test_delegates_call_to_create_email_message(
Expand Down Expand Up @@ -188,3 +314,70 @@ def test_calls_send_on_email_message(
# Then
email_message = spy_create_email_message.return_value
email_message.send.assert_called_once_with(fail_silently=fail_silently)


class TestSendEmailV2:
@mock.patch(f"{MODULE_PATH}.create_email_message_v2")
def test_delegates_call_to_create_email_message(
self, spy_create_email_message: mock.MagicMock
):
"""
Given a suggestions dict, a recipient email address and a subject line
When `send_email()` is called
Then the call is delegated to `create_email_message()`

Patches:
`spy_create_email_message`: For the main assertion.
To check the email message object is initialized
with the correct arguments
"""
# Given
mocked_suggestions = mock.Mock()
mocked_recipient_email_address = mock.Mock()
mocked_subject = mock.Mock()

# When
send_email_v2(
suggestions=mocked_suggestions,
recipient_email_address=mocked_recipient_email_address,
subject=mocked_subject,
)

# Then
spy_create_email_message.assert_called_once_with(
suggestions=mocked_suggestions,
subject=mocked_subject,
recipient_email_address=mocked_recipient_email_address,
)

@mock.patch(f"{MODULE_PATH}.create_email_message_v2")
def test_calls_send_on_email_message(
self, spy_create_email_message: mock.MagicMock
):
"""
Given a suggestions dict, a recipient email address and a subject line
When `send_email()` is called
Then the `send()` is called from the returned `EmailMessage`

Patches:
`spy_create_email_message`: To capture and spy on
the returned `EmailMessage`.
And to check `send()` is called from the `EmailMessage`
"""
# Given
mocked_suggestions = mock.Mock()
mocked_recipient_email_address = mock.Mock()
mocked_subject = mock.Mock()
fail_silently = True

# When
send_email_v2(
suggestions=mocked_suggestions,
recipient_email_address=mocked_recipient_email_address,
subject=mocked_subject,
fail_silently=fail_silently,
)

# Then
email_message = spy_create_email_message.return_value
email_message.send.assert_called_once_with(fail_silently=fail_silently)
Loading
Loading