From 252269569a83edd6b90e029c4c9d64a854b947ab Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 31 Mar 2023 13:32:03 -0500 Subject: [PATCH 1/4] Improve submission confirmation email and test it --- src/registrar/models/domain_application.py | 2 +- .../templates/emails/includes/contact.txt | 4 + .../submission_confirmation.subject.txt | 1 + .../emails/submission_confirmation.txt | 94 +++++++++++++++++- src/registrar/tests/test_emails.py | 97 +++++++++++++++++++ 5 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 src/registrar/templates/emails/includes/contact.txt create mode 100644 src/registrar/templates/emails/submission_confirmation.subject.txt create mode 100644 src/registrar/tests/test_emails.py diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 526e39798..729cdfe8c 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -478,7 +478,7 @@ class DomainApplication(TimeStampedModel): "emails/submission_confirmation.txt", "emails/submission_confirmation_subject.txt", self.submitter.email, - context={"id": self.id, "domain_name": self.requested_domain.name}, + context={"application": self}, ) except EmailSendingError: logger.warning("Failed to send confirmation email", exc_info=True) diff --git a/src/registrar/templates/emails/includes/contact.txt b/src/registrar/templates/emails/includes/contact.txt new file mode 100644 index 000000000..a3c3dd6eb --- /dev/null +++ b/src/registrar/templates/emails/includes/contact.txt @@ -0,0 +1,4 @@ +{{ contact.get_formatted_name }} +{% if contact.title %}{{ contact.title }}{% endif %} +{% if contact.email %}{{ contact.email }}{% endif %} +{% if contact.phone %}{{ contact.phone.as_national }}{% endif %} diff --git a/src/registrar/templates/emails/submission_confirmation.subject.txt b/src/registrar/templates/emails/submission_confirmation.subject.txt new file mode 100644 index 000000000..47e3f70fd --- /dev/null +++ b/src/registrar/templates/emails/submission_confirmation.subject.txt @@ -0,0 +1 @@ +We received your .gov domain request diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index f44c2f505..b028e1626 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -1,4 +1,92 @@ -Thank you for submitting an application for the domain name "{{ domain_name }}". +Hi {{ application.creator.first_name }}. -If you need to make changes to your application, visit -. +We received your .gov domain request. + +DOMAIN REQUESTED: {{ application.requested_domain.name }} +REQUEST RECEIVED ON: {{ application.updated_at|date }} +REQUEST #: {{ application.id }} +STATUS: Received + + +NEED TO MAKE CHANGES? + +If you need to change your request you have to first withdraw it. Once you +withdraw the request you can edit it and submit it again. Changing your request +might add to the wait time. Learn more about withdrawing your request. + + +NEXT STEPS + +- We’ll review your request. This usually takes 20 business days. + +- You can check the status of your request at any time. + + +- We’ll email you with questions or when we complete our review. + + +THANK YOU + +.Gov helps the public identify official, trusted information. Thank you for +requesting a .gov domain. + +---------------------------------------------------------------- + +SUMMARY OF YOUR DOMAIN REQUEST + +Type of organization: +{{ application.get_organization_type_display }} + +Organization name and mailing address: +{{ application.organization_name }} +{{ application.address_line1 }} +{% if application.address_line2 %}{{ application.address_line2 }}{% endif %} +{{ application.city }}, {{ application.state_territory }} +{{ application.zipcode }} +{% if application.urbanization %}{{ application.urbanization }}{% endif %} + +{% if application.type_of_work %} +Type of work: +{{ application.type_of_work }} + +{% endif %} +Authorizing official: +{% include "emails/includes/contact.txt" with contact=application.authorizing_official %} + +{% if application.current_websites.exists %} +Current website for your organization: +{% for site in application.current_websites.all %} +{{ site.website }} +{% endfor %} + +{% endif %} +.gov domain: +{{ application.requested_domain.name }} +{% for site in application.alternative_domains.all %} +{{ site.website }} +{% endfor %} + +Purpose of your domain: +{{ application.purpose }} + +Your contact information: +{% include "emails/includes/contact.txt" with contact=application.submitter %} + +{% if application.other_contacts.all %} +Other employees from your organization: +{% for other in application.other_contacts.all %} +{% include "emails/includes/contact.txt" with contact=other %} +{% endfor %} + +{% endif %} +{% if application.anything_else %} +Anything else we should know? + +{{ application.anything_else }} +{% endif %} + +---------------------------------------------------------------- + +The .gov team +Contact us: +Visit diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py new file mode 100644 index 000000000..edbf5cdf8 --- /dev/null +++ b/src/registrar/tests/test_emails.py @@ -0,0 +1,97 @@ +"""Test our email templates and sending.""" + +from unittest.mock import MagicMock + +from django.contrib.auth import get_user_model +from django.test import TestCase + +from registrar.models import Contact, Domain, Website, DomainApplication + +import boto3_mocking # type: ignore + + +class TestEmails(TestCase): + def _completed_application(self): + """A completed domain application.""" + user = get_user_model().objects.create(username="username") + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(555) 555 5555", + ) + domain, _ = Domain.objects.get_or_create(name="city.gov") + alt, _ = Website.objects.get_or_create(website="city1.gov") + current, _ = Website.objects.get_or_create(website="city.com") + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(555) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(555) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + requested_domain=domain, + submitter=you, + creator=user, + ) + application.other_contacts.add(other) + application.current_websites.add(current) + application.alternative_domains.add(alt) + + return application + + @boto3_mocking.patching + def test_submission_confirmation(self): + """Submission confirmation email works.""" + application = self._completed_application() + + mock_client_class = MagicMock() + mock_client = mock_client_class.return_value + with boto3_mocking.clients.handler_for("sesv2", mock_client_class): + application.submit() + + # check that an email was sent + self.assertTrue(mock_client.send_email.called) + + # check the call sequence for the email + args, kwargs = mock_client.send_email.call_args + self.assertIn("Content", kwargs) + self.assertIn("Simple", kwargs["Content"]) + self.assertIn("Subject", kwargs["Content"]["Simple"]) + self.assertIn("Body", kwargs["Content"]["Simple"]) + + # check for things in the email content (not an exhaustive list) + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("Type of organization:", body) + self.assertIn("Federal", body) + self.assertIn("Authorizing official:", body) + self.assertIn("Testy Tester", body) + self.assertIn(".gov domain:", body) + self.assertIn("city.gov", body) + self.assertIn("city1.gov", body) + + # check for optional things + self.assertIn("Other employees from your organization:", body) + self.assertIn("Testy2 Tester2", body) + self.assertIn("Current website for your organization:", body) + self.assertIn("city.com", body) + self.assertNotIn("Type of work:", body) From 56834e9c1636cf1719f8d916d59e9da421f61f01 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Tue, 4 Apr 2023 16:11:02 -0500 Subject: [PATCH 2/4] Remove unused subject template --- .../templates/emails/submission_confirmation.subject.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/registrar/templates/emails/submission_confirmation.subject.txt diff --git a/src/registrar/templates/emails/submission_confirmation.subject.txt b/src/registrar/templates/emails/submission_confirmation.subject.txt deleted file mode 100644 index 47e3f70fd..000000000 --- a/src/registrar/templates/emails/submission_confirmation.subject.txt +++ /dev/null @@ -1 +0,0 @@ -We received your .gov domain request From 29381418a9d5b391f3f9df1dc7bb411ce2190fbf Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Mon, 10 Apr 2023 11:09:14 -0500 Subject: [PATCH 3/4] Email template formatting and tests --- .../emails/submission_confirmation.txt | 50 ++---- src/registrar/tests/test_emails.py | 166 ++++++++++++++++-- 2 files changed, 174 insertions(+), 42 deletions(-) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index b028e1626..11bc666eb 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -1,4 +1,5 @@ -Hi {{ application.creator.first_name }}. +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +Hi {{ application.submitter.first_name }}. We received your .gov domain request. @@ -20,7 +21,7 @@ NEXT STEPS - We’ll review your request. This usually takes 20 business days. - You can check the status of your request at any time. - + - We’ll email you with questions or when we complete our review. @@ -38,55 +39,42 @@ Type of organization: {{ application.get_organization_type_display }} Organization name and mailing address: -{{ application.organization_name }} -{{ application.address_line1 }} -{% if application.address_line2 %}{{ application.address_line2 }}{% endif %} +{% spaceless %}{{ application.organization_name }} +{{ application.address_line1 }}{% if application.address_line2 %} +{{ application.address_line2 }}{% endif %} {{ application.city }}, {{ application.state_territory }} -{{ application.zipcode }} -{% if application.urbanization %}{{ application.urbanization }}{% endif %} - -{% if application.type_of_work %} +{{ application.zipcode }}{% if application.urbanization %} +{{ application.urbanization }}{% endif %}{% endspaceless %} +{% if application.type_of_work %}{# if block makes one newline if it's false #} Type of work: -{{ application.type_of_work }} - +{% spaceless %}{{ application.type_of_work }}{% endspaceless %} {% endif %} Authorizing official: -{% include "emails/includes/contact.txt" with contact=application.authorizing_official %} - -{% if application.current_websites.exists %} -Current website for your organization: -{% for site in application.current_websites.all %} +{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %} +{% if application.current_websites.exists %}{# if block makes a newline #} +Current website for your organization: {% for site in application.current_websites.all %} {{ site.website }} -{% endfor %} - -{% endif %} +{% endfor %}{% endif %} .gov domain: {{ application.requested_domain.name }} -{% for site in application.alternative_domains.all %} -{{ site.website }} +{% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %} {% endfor %} - Purpose of your domain: {{ application.purpose }} Your contact information: -{% include "emails/includes/contact.txt" with contact=application.submitter %} - +{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %} {% if application.other_contacts.all %} Other employees from your organization: {% for other in application.other_contacts.all %} -{% include "emails/includes/contact.txt" with contact=other %} -{% endfor %} - -{% endif %} -{% if application.anything_else %} +{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %} +{% endfor %}{% endif %}{% if application.anything_else %} Anything else we should know? - {{ application.anything_else }} {% endif %} - ---------------------------------------------------------------- The .gov team Contact us: Visit +{% endautoescape %} diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index edbf5cdf8..44cb565e2 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -11,7 +11,14 @@ import boto3_mocking # type: ignore class TestEmails(TestCase): - def _completed_application(self): + def _completed_application( + self, + has_other_contacts=True, + has_current_website=True, + has_alternative_gov_domain=True, + has_type_of_work=True, + has_anything_else=True, + ): """A completed domain application.""" user = get_user_model().objects.create(username="username") ao, _ = Contact.objects.get_or_create( @@ -38,14 +45,14 @@ class TestEmails(TestCase): email="testy2@town.com", phone="(555) 555 5557", ) - application, _ = DomainApplication.objects.get_or_create( + domain_application_kwargs = dict( organization_type="federal", federal_type="executive", purpose="Purpose of the site", - anything_else="No", is_policy_acknowledged=True, organization_name="Testorg", address_line1="address 1", + address_line2="address 2", state_territory="NY", zipcode="10002", authorizing_official=ao, @@ -53,27 +60,41 @@ class TestEmails(TestCase): submitter=you, creator=user, ) - application.other_contacts.add(other) - application.current_websites.add(current) - application.alternative_domains.add(alt) + if has_type_of_work: + domain_application_kwargs["type_of_work"] = "e-Government" + if has_anything_else: + domain_application_kwargs["anything_else"] = "There is more" + + application, _ = DomainApplication.objects.get_or_create( + **domain_application_kwargs + ) + + if has_other_contacts: + application.other_contacts.add(other) + if has_current_website: + application.current_websites.add(current) + if has_alternative_gov_domain: + application.alternative_domains.add(alt) return application + def setUp(self): + self.mock_client_class = MagicMock() + self.mock_client = self.mock_client_class.return_value + @boto3_mocking.patching def test_submission_confirmation(self): """Submission confirmation email works.""" application = self._completed_application() - mock_client_class = MagicMock() - mock_client = mock_client_class.return_value - with boto3_mocking.clients.handler_for("sesv2", mock_client_class): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): application.submit() # check that an email was sent - self.assertTrue(mock_client.send_email.called) + self.assertTrue(self.mock_client.send_email.called) # check the call sequence for the email - args, kwargs = mock_client.send_email.call_args + args, kwargs = self.mock_client.send_email.call_args self.assertIn("Content", kwargs) self.assertIn("Simple", kwargs["Content"]) self.assertIn("Subject", kwargs["Content"]["Simple"]) @@ -81,6 +102,7 @@ class TestEmails(TestCase): # check for things in the email content (not an exhaustive list) body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("Type of organization:", body) self.assertIn("Federal", body) self.assertIn("Authorizing official:", body) @@ -94,4 +116,126 @@ class TestEmails(TestCase): self.assertIn("Testy2 Tester2", body) self.assertIn("Current website for your organization:", body) self.assertIn("city.com", body) + self.assertIn("Type of work:", body) + self.assertIn("Anything else", body) + + @boto3_mocking.patching + def test_submission_confirmation_no_current_website_spacing(self): + """Test line spacing without current_website.""" + application = self._completed_application(has_current_website=False) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertNotIn("Current website for your organization:", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"5555\n\n.gov domain:") + + @boto3_mocking.patching + def test_submission_confirmation_current_website_spacing(self): + """Test line spacing with current_website.""" + application = self._completed_application(has_current_website=True) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("Current website for your organization:", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"5555\n\nCurrent website for") + self.assertRegex(body, r"city.com\n\n.gov domain:") + + @boto3_mocking.patching + def test_submission_confirmation_other_contacts_spacing(self): + """Test line spacing with other contacts.""" + application = self._completed_application(has_other_contacts=True) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("Other employees from your organization:", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"5556\n\nOther employees") + self.assertRegex(body, r"5557\n\nAnything else") + + @boto3_mocking.patching + def test_submission_confirmation_no_other_contacts_spacing(self): + """Test line spacing without other contacts.""" + application = self._completed_application(has_other_contacts=False) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertNotIn("Other employees from your organization:", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"5556\n\nAnything else") + + @boto3_mocking.patching + def test_submission_confirmation_alternative_govdomain_spacing(self): + """Test line spacing with alternative .gov domain.""" + application = self._completed_application(has_alternative_gov_domain=True) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("city1.gov", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"city.gov\ncity1.gov\n\nPurpose of your domain:") + + @boto3_mocking.patching + def test_submission_confirmation_no_alternative_govdomain_spacing(self): + """Test line spacing without alternative .gov domain.""" + application = self._completed_application(has_alternative_gov_domain=False) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertNotIn("city1.gov", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"city.gov\n\nPurpose of your domain:") + + @boto3_mocking.patching + def test_submission_confirmation_type_of_work_spacing(self): + """Test line spacing with type of work.""" + application = self._completed_application(has_type_of_work=True) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("Type of work:", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"10002\n\nType of work:") + + @boto3_mocking.patching + def test_submission_confirmation_no_type_of_work_spacing(self): + """Test line spacing without type of work.""" + application = self._completed_application(has_type_of_work=False) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("Type of work:", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"10002\n\nAuthorizing official:") + + @boto3_mocking.patching + def test_submission_confirmation_anything_else_spacing(self): + """Test line spacing with anything else.""" + application = self._completed_application(has_anything_else=True) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + # spacing should be right between adjacent elements + self.assertRegex(body, r"5557\n\nAnything else we should know?") + + @boto3_mocking.patching + def test_submission_confirmation_no_anything_else_spacing(self): + """Test line spacing without anything else.""" + application = self._completed_application(has_anything_else=False) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + application.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertNotIn("Anything else we should know", body) + # spacing should be right between adjacent elements + self.assertRegex(body, r"5557\n\n----") From ec64be954124df3c861ecbd3c97e0b2124a86ce8 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Mon, 10 Apr 2023 13:53:41 -0500 Subject: [PATCH 4/4] tweak spacing on current websites --- src/registrar/templates/emails/submission_confirmation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index 11bc666eb..6da239065 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -53,7 +53,7 @@ Authorizing official: {% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %} {% if application.current_websites.exists %}{# if block makes a newline #} Current website for your organization: {% for site in application.current_websites.all %} -{{ site.website }} +{% spaceless %}{{ site.website }}{% endspaceless %} {% endfor %}{% endif %} .gov domain: {{ application.requested_domain.name }}