mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 09:37:03 +02:00
Implement all statuses in application admin class, implement approved email, consolidate email methods in application model class, unit test emails sends as side-effects of admin save
This commit is contained in:
parent
14f6a92f9f
commit
94d5682b24
5 changed files with 228 additions and 60 deletions
|
@ -90,16 +90,36 @@ class DomainApplicationAdmin(AuditedAdmin):
|
||||||
# Get the original application from the database
|
# Get the original application from the database
|
||||||
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
|
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
|
||||||
|
|
||||||
if (
|
if obj.status != original_obj.status:
|
||||||
obj.status != original_obj.status
|
if obj.status == models.DomainApplication.STARTED:
|
||||||
and obj.status == models.DomainApplication.INVESTIGATING
|
# No conditions
|
||||||
):
|
pass
|
||||||
# This is a transition annotated method in model which will throw an
|
if obj.status == models.DomainApplication.SUBMITTED:
|
||||||
# error if the condition is violated. To make this work, we need to
|
# This is an fsm in model which will throw an error if the
|
||||||
# call it on the original object which has the right status value,
|
# transition condition is violated, so we call it on the
|
||||||
# but pass the current object which contains the up-to-date data
|
# original object which has the right status value, and pass
|
||||||
# for the email.
|
# the updated object which contains the up-to-date data
|
||||||
original_obj.in_review(obj)
|
# for the side effects (like an email send).
|
||||||
|
original_obj.submit(updated_domain_application=obj)
|
||||||
|
if obj.status == models.DomainApplication.INVESTIGATING:
|
||||||
|
# This is an fsm in model which will throw an error if the
|
||||||
|
# transition condition is violated, so we call it on the
|
||||||
|
# original object which has the right status value, and pass
|
||||||
|
# the updated object which contains the up-to-date data
|
||||||
|
# for the side effects (like an email send).
|
||||||
|
original_obj.in_review(updated_domain_application=obj)
|
||||||
|
if obj.status == models.DomainApplication.APPROVED:
|
||||||
|
# This is an fsm in model which will throw an error if the
|
||||||
|
# transition condition is violated, so we call it on the
|
||||||
|
# original object which has the right status value, and pass
|
||||||
|
# the updated object which contains the up-to-date data
|
||||||
|
# for the side effects (like an email send).
|
||||||
|
original_obj.approve(updated_domain_application=obj)
|
||||||
|
if obj.status == models.DomainApplication.WITHDRAWN:
|
||||||
|
# This is a transition annotated method in model which will throw an
|
||||||
|
# error if the condition is violated. To make this work, we need to
|
||||||
|
# call it on the original object which has the right status value.
|
||||||
|
original_obj.withdraw()
|
||||||
|
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
|
@ -471,59 +471,34 @@ class DomainApplication(TimeStampedModel):
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _send_confirmation_email(self):
|
def _send_status_update_email(
|
||||||
"""Send a confirmation email that this application was submitted.
|
self, new_status, email_template, email_template_subject
|
||||||
|
):
|
||||||
|
"""Send a atatus update email to the submitter.
|
||||||
|
|
||||||
The email goes to the email address that the submitter gave as their
|
The email goes to the email address that the submitter gave as their
|
||||||
contact information. If there is not submitter information, then do
|
contact information. If there is not submitter information, then do
|
||||||
nothing.
|
nothing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.submitter is None or self.submitter.email is None:
|
if self.submitter is None or self.submitter.email is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Cannot send confirmation email, no submitter email address."
|
f"Cannot send {new_status} email, no submitter email address."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
send_templated_email(
|
send_templated_email(
|
||||||
"emails/submission_confirmation.txt",
|
email_template,
|
||||||
"emails/submission_confirmation_subject.txt",
|
email_template_subject,
|
||||||
self.submitter.email,
|
self.submitter.email,
|
||||||
context={"application": self},
|
context={"application": self},
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(f"The {new_status} email sent to: {self.submitter.email}")
|
||||||
f"Submission confirmation email sent to: {self.submitter.email}"
|
|
||||||
)
|
|
||||||
except EmailSendingError:
|
except EmailSendingError:
|
||||||
logger.warning("Failed to send confirmation email", exc_info=True)
|
logger.warning("Failed to send confirmation email", exc_info=True)
|
||||||
|
|
||||||
def _send_in_review_email(self):
|
|
||||||
"""Send an email that this application is now in review.
|
|
||||||
|
|
||||||
The email goes to the email address that the submitter gave as their
|
|
||||||
contact information. If there is not submitter information, then do
|
|
||||||
nothing.
|
|
||||||
"""
|
|
||||||
if self.submitter is None or self.submitter.email is None:
|
|
||||||
logger.warning(
|
|
||||||
"Cannot send status change (in review) email,"
|
|
||||||
"no submitter email address."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
send_templated_email(
|
|
||||||
"emails/status_change_in_review.txt",
|
|
||||||
"emails/status_change_in_review_subject.txt",
|
|
||||||
self.submitter.email,
|
|
||||||
context={"application": self},
|
|
||||||
)
|
|
||||||
logging.info(f"In review email sent to: {self.submitter.email}")
|
|
||||||
except EmailSendingError:
|
|
||||||
logger.warning(
|
|
||||||
"Failed to send status change (in review) email", exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@transition(field="status", source=[STARTED, WITHDRAWN], target=SUBMITTED)
|
@transition(field="status", source=[STARTED, WITHDRAWN], target=SUBMITTED)
|
||||||
def submit(self):
|
def submit(self, updated_domain_application=None):
|
||||||
"""Submit an application that is started."""
|
"""Submit an application that is started."""
|
||||||
|
|
||||||
# check our conditions here inside the `submit` method so that we
|
# check our conditions here inside the `submit` method so that we
|
||||||
|
@ -542,10 +517,40 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
# When an application is submitted, we need to send a confirmation email
|
# When an application is submitted, we need to send a confirmation email
|
||||||
# This is a side-effect of the state transition
|
# This is a side-effect of the state transition
|
||||||
self._send_confirmation_email()
|
if updated_domain_application is not None:
|
||||||
|
# A DomainApplication is being passed to this method (ie from admin)
|
||||||
|
updated_domain_application._send_status_update_email(
|
||||||
|
"submission confirmation",
|
||||||
|
"emails/submission_confirmation.txt",
|
||||||
|
"emails/submission_confirmation_subject.txt",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# views/application.py
|
||||||
|
self._send_status_update_email(
|
||||||
|
"submission confirmation",
|
||||||
|
"emails/submission_confirmation.txt",
|
||||||
|
"emails/submission_confirmation_subject.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
@transition(field="status", source=SUBMITTED, target=INVESTIGATING)
|
||||||
|
def in_review(self, updated_domain_application):
|
||||||
|
"""Investigate an application that has been submitted.
|
||||||
|
|
||||||
|
This method is called in admin.py on the original application
|
||||||
|
which has the correct status value, but is passed the changed
|
||||||
|
application which has the up-to-date data that we'll use
|
||||||
|
in the email."""
|
||||||
|
|
||||||
|
# When an application is moved to in review, we need to send a
|
||||||
|
# confirmation email. This is a side-effect of the state transition
|
||||||
|
updated_domain_application._send_status_update_email(
|
||||||
|
"application in review",
|
||||||
|
"emails/status_change_in_review.txt",
|
||||||
|
"emails/status_change_in_review_subject.txt",
|
||||||
|
)
|
||||||
|
|
||||||
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=APPROVED)
|
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=APPROVED)
|
||||||
def approve(self):
|
def approve(self, updated_domain_application=None):
|
||||||
"""Approve an application that has been submitted.
|
"""Approve an application that has been submitted.
|
||||||
|
|
||||||
This has substantial side-effects because it creates another database
|
This has substantial side-effects because it creates another database
|
||||||
|
@ -570,18 +575,21 @@ class DomainApplication(TimeStampedModel):
|
||||||
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
|
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
|
||||||
)
|
)
|
||||||
|
|
||||||
@transition(field="status", source=SUBMITTED, target=INVESTIGATING)
|
|
||||||
def in_review(self, updated_domain_application):
|
|
||||||
"""Investigate an application that has been submitted.
|
|
||||||
|
|
||||||
This method is called in admin.py on the original application
|
|
||||||
which has the correct status value, but is passed the changed
|
|
||||||
application which has the up-to-date data that we'll use
|
|
||||||
in the email."""
|
|
||||||
|
|
||||||
# When an application is moved to in review, we need to send a
|
# When an application is moved to in review, we need to send a
|
||||||
# confirmation email. This is a side-effect of the state transition
|
# confirmation email. This is a side-effect of the state transition
|
||||||
updated_domain_application._send_in_review_email()
|
if updated_domain_application is not None:
|
||||||
|
# A DomainApplication is being passed to this method (ie from admin)
|
||||||
|
updated_domain_application._send_status_update_email(
|
||||||
|
"application approved",
|
||||||
|
"emails/status_change_approved.txt",
|
||||||
|
"emails/status_change_approved_subject.txt",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._send_status_update_email(
|
||||||
|
"application approved",
|
||||||
|
"emails/status_change_approved.txt",
|
||||||
|
"emails/status_change_approved.txt",
|
||||||
|
)
|
||||||
|
|
||||||
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=WITHDRAWN)
|
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=WITHDRAWN)
|
||||||
def withdraw(self):
|
def withdraw(self):
|
||||||
|
|
40
src/registrar/templates/emails/status_change_approved.txt
Normal file
40
src/registrar/templates/emails/status_change_approved.txt
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
|
Hi {{ application.submitter.first_name }}.
|
||||||
|
|
||||||
|
Congratulations! Your .gov domain request has been approved.
|
||||||
|
|
||||||
|
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||||
|
REQUEST RECEIVED ON: {{ application.updated_at|date }}
|
||||||
|
REQUEST #: {{ application.id }}
|
||||||
|
STATUS: In review
|
||||||
|
|
||||||
|
Now that your .gov domain has been approved, there are a few more things to do before your domain can be used.
|
||||||
|
|
||||||
|
|
||||||
|
YOU MUST ADD DOMAIN NAME SERVER INFORMATION
|
||||||
|
|
||||||
|
Before your .gov domain can be used, you have to connect it to your Domain Name System (DNS) hosting service. At this time, we don’t provide DNS hosting services.
|
||||||
|
Go to the domain management page to add your domain name server information <https://registrar.get.gov/domain/{{ application.id }}/nameservers>.
|
||||||
|
|
||||||
|
Get help with adding your domain name server information <https://get.gov/help/domain-management/#manage-dns-information-for-your-domain>.
|
||||||
|
|
||||||
|
|
||||||
|
ADD DOMAIN MANAGERS, SECURITY EMAIL
|
||||||
|
|
||||||
|
We strongly recommend that you add other points of contact who will help manage your domain. We also recommend that you provide a security email. This email will allow the public to report security issues on your domain. Security emails are made public.
|
||||||
|
|
||||||
|
Go to the domain management page to add domain contacts <https://registrar.get.gov/domain/{{ application.id }}/your-contact-information> and a security email <https://registrar.get.gov/domain/{{ application.id }}/security-email>.
|
||||||
|
|
||||||
|
Get help with managing your .gov domain <https://get.gov/help/domain-management/>.
|
||||||
|
|
||||||
|
|
||||||
|
THANK YOU
|
||||||
|
|
||||||
|
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
|
||||||
|
The .gov team
|
||||||
|
Contact us: <https://get.gov/contact/>
|
||||||
|
Visit <https://get.gov>
|
||||||
|
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
||||||
|
Your .gov domain request is approved
|
|
@ -1,7 +1,7 @@
|
||||||
from django.test import TestCase, RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from registrar.admin import DomainApplicationAdmin
|
from registrar.admin import DomainApplicationAdmin
|
||||||
from registrar.models import DomainApplication, User
|
from registrar.models import DomainApplication, DomainInformation, User
|
||||||
from .common import completed_application
|
from .common import completed_application
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -15,7 +15,56 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_save_model_sends_email_on_property_change(self):
|
def test_save_model_sends_submitted_email(self):
|
||||||
|
# make sure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client_instance = mock_client.return_value
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application()
|
||||||
|
|
||||||
|
# Create a mock request
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an instance of the model admin
|
||||||
|
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
||||||
|
|
||||||
|
# Modify the application's property
|
||||||
|
application.status = DomainApplication.SUBMITTED
|
||||||
|
|
||||||
|
# Use the model admin's save_model method
|
||||||
|
model_admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
|
# Access the arguments passed to send_email
|
||||||
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
args, kwargs = call_args
|
||||||
|
|
||||||
|
# Retrieve the email details from the arguments
|
||||||
|
from_email = kwargs.get("FromEmailAddress")
|
||||||
|
to_email = kwargs["Destination"]["ToAddresses"][0]
|
||||||
|
email_content = kwargs["Content"]
|
||||||
|
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
|
||||||
|
# Assert or perform other checks on the email details
|
||||||
|
expected_string = "We received your .gov domain request."
|
||||||
|
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
|
||||||
|
self.assertEqual(to_email, EMAIL)
|
||||||
|
self.assertIn(expected_string, email_body)
|
||||||
|
|
||||||
|
# Perform assertions on the mock call itself
|
||||||
|
mock_client_instance.send_email.assert_called_once()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
application.delete()
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_save_model_sends_in_review_email(self):
|
||||||
# make sure there is no user with this email
|
# make sure there is no user with this email
|
||||||
EMAIL = "mayor@igorville.gov"
|
EMAIL = "mayor@igorville.gov"
|
||||||
User.objects.filter(email=EMAIL).delete()
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
@ -52,7 +101,7 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
|
||||||
# Assert or perform other checks on the email details
|
# Assert or perform other checks on the email details
|
||||||
expected_string = "Your .gov domain request is being reviewed"
|
expected_string = "Your .gov domain request is being reviewed."
|
||||||
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
|
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
|
||||||
self.assertEqual(to_email, EMAIL)
|
self.assertEqual(to_email, EMAIL)
|
||||||
self.assertIn(expected_string, email_body)
|
self.assertIn(expected_string, email_body)
|
||||||
|
@ -62,3 +111,53 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
application.delete()
|
application.delete()
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_save_model_sends_approved_email(self):
|
||||||
|
# make sure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client_instance = mock_client.return_value
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.INVESTIGATING)
|
||||||
|
|
||||||
|
# Create a mock request
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an instance of the model admin
|
||||||
|
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
||||||
|
|
||||||
|
# Modify the application's property
|
||||||
|
application.status = DomainApplication.APPROVED
|
||||||
|
|
||||||
|
# Use the model admin's save_model method
|
||||||
|
model_admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
|
# Access the arguments passed to send_email
|
||||||
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
args, kwargs = call_args
|
||||||
|
|
||||||
|
# Retrieve the email details from the arguments
|
||||||
|
from_email = kwargs.get("FromEmailAddress")
|
||||||
|
to_email = kwargs["Destination"]["ToAddresses"][0]
|
||||||
|
email_content = kwargs["Content"]
|
||||||
|
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
|
||||||
|
# Assert or perform other checks on the email details
|
||||||
|
expected_string = "Congratulations! Your .gov domain request has been approved."
|
||||||
|
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
|
||||||
|
self.assertEqual(to_email, EMAIL)
|
||||||
|
self.assertIn(expected_string, email_body)
|
||||||
|
|
||||||
|
# Perform assertions on the mock call itself
|
||||||
|
mock_client_instance.send_email.assert_called_once()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
DomainInformation.objects.get(id=application.pk).delete()
|
||||||
|
application.delete()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue