From e2c965a9156855eb9db22d1675ff49c67047f7d9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 24 Jan 2024 21:28:43 -0500 Subject: [PATCH 01/33] Reduce email sends to first-time transition for the applicable transitions --- src/registrar/models/domain_application.py | 73 +++++-- src/registrar/tests/test_admin.py | 227 +++++++++++---------- src/registrar/tests/test_models.py | 217 +++++++++++++++++++- 3 files changed, 382 insertions(+), 135 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 196449bfa..e1c809058 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -1,4 +1,6 @@ from __future__ import annotations +from json import JSONDecodeError +import json from typing import Union import logging @@ -12,6 +14,7 @@ from registrar.models.domain import Domain from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError from itertools import chain +from auditlog.models import LogEntry # type: ignore logger = logging.getLogger(__name__) @@ -565,6 +568,25 @@ class DomainApplication(TimeStampedModel): except Exception: return "" + def has_previously_had_a_status_of(self, status): + """Return True if this request has previously had the status of {passed param}.""" + + log_entries = LogEntry.objects.get_for_object(self) + + for entry in log_entries: + try: + changes_dict = json.loads(entry.changes) + logger.info(changes_dict) + # changes_dict will look like {'status': ['withdrawn', 'submitted']}, + # henceforth the len(changes_dict.get('status', [])) == 2 + if len(changes_dict.get("status", [])) == 2 and changes_dict.get("status", [])[1] == status: + logger.info(f"found one instance where it had a status of {status}") + return True + except JSONDecodeError: + pass + + return False + def domain_is_not_active(self): if self.approved_domain: return not self.approved_domain.is_active() @@ -633,11 +655,13 @@ class DomainApplication(TimeStampedModel): self.submission_date = timezone.now().date() self.save() - self._send_status_update_email( - "submission confirmation", - "emails/submission_confirmation.txt", - "emails/submission_confirmation_subject.txt", - ) + # Limit email notifications for this transition to the first time the request transitions to this status + if not self.has_previously_had_a_status_of("submitted"): + self._send_status_update_email( + "submission confirmation", + "emails/submission_confirmation.txt", + "emails/submission_confirmation_subject.txt", + ) @transition( field="status", @@ -713,12 +737,14 @@ class DomainApplication(TimeStampedModel): user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) - self._send_status_update_email( - "application approved", - "emails/status_change_approved.txt", - "emails/status_change_approved_subject.txt", - send_email, - ) + # Limit email notifications for this transition to the first time the request transitions to this status + if not self.has_previously_had_a_status_of("approved"): + self._send_status_update_email( + "application approved", + "emails/status_change_approved.txt", + "emails/status_change_approved_subject.txt", + send_email, + ) @transition( field="status", @@ -727,11 +753,14 @@ class DomainApplication(TimeStampedModel): ) def withdraw(self): """Withdraw an application that has been submitted.""" - self._send_status_update_email( - "withdraw", - "emails/domain_request_withdrawn.txt", - "emails/domain_request_withdrawn_subject.txt", - ) + + # Limit email notifications for this transition to the first time the request transitions to this status + if not self.has_previously_had_a_status_of("withdrawn"): + self._send_status_update_email( + "withdraw", + "emails/domain_request_withdrawn.txt", + "emails/domain_request_withdrawn_subject.txt", + ) @transition( field="status", @@ -757,11 +786,13 @@ class DomainApplication(TimeStampedModel): logger.error(err) logger.error("Can't query an approved domain while attempting a DA reject()") - self._send_status_update_email( - "action needed", - "emails/status_change_rejected.txt", - "emails/status_change_rejected_subject.txt", - ) + # Limit email notifications for this transition to the first time the request transitions to this status + if not self.has_previously_had_a_status_of("rejected"): + self._send_status_update_email( + "action needed", + "emails/status_change_rejected.txt", + "emails/status_change_rejected_subject.txt", + ) @transition( field="status", diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 7c9aa8fe4..7773cb60b 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -316,6 +316,7 @@ class TestDomainApplicationAdminForm(TestCase): ) +@boto3_mocking.patching class TestDomainApplicationAdmin(MockEppLib): def setUp(self): super().setUp() @@ -421,83 +422,143 @@ class TestDomainApplicationAdmin(MockEppLib): # Now let's make sure the long description does not exist self.assertNotContains(response, "Federal: an agency of the U.S. government") - @boto3_mocking.patching + def transition_state_and_send_email(self, application, status): + """Helper method for the email test cases.""" + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + + # Modify the application's property + application.status = status + + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) + + def assert_email_is_accurate(self, expected_string, email_index, email_address): + """Helper method for the email test cases. + email_index is the index of the email in mock_client.""" + + # Access the arguments passed to send_email + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[email_index]["kwargs"] + + # 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 + self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) + self.assertEqual(to_email, email_address) + self.assertIn(expected_string, email_body) + def test_save_model_sends_submitted_email(self): - # make sure there is no user with this email + """When transitioning to submitted the first time (and the first time only) on a domain request, + an email is sent out.""" + + # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Create a sample application - application = completed_application() - - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.SUBMITTED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) - - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[0]["kwargs"] - - # 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) + # Create a sample application + application = completed_application() + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - @boto3_mocking.patching + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_save_model_sends_approved_email(self): - # make sure there is no user with this email + """When transitioning to approved the first time (and the first time only) on a domain request, + an email is sent out.""" + + # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.APPROVED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) - - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[0]["kwargs"] - - # 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) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - @boto3_mocking.patching + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) + self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_sends_rejected_email(self): + """When transitioning to rejected the first time (and the first time only) on a domain request, + an email is sent out.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) + self.assert_email_is_accurate("Your .gov domain request has been rejected.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_sends_withdrawn_email(self): + """When transitioning to withdrawn the first time (and the first time only) on a domain request, + an email is sent out.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_save_model_sets_approved_domain(self): # make sure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -520,45 +581,6 @@ class TestDomainApplicationAdmin(MockEppLib): # Test that approved domain exists and equals requested domain self.assertEqual(application.requested_domain.name, application.approved_domain.name) - @boto3_mocking.patching - def test_save_model_sends_rejected_email(self): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.REJECTED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) - - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[0]["kwargs"] - - # 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 = "Your .gov domain request has been rejected." - self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(to_email, EMAIL) - self.assertIn(expected_string, email_body) - - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - @boto3_mocking.patching def test_save_model_sets_restricted_status_on_user(self): # make sure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -707,7 +729,6 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) - @boto3_mocking.patching def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): # Create an instance of the model application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index ef6522747..a1b22373d 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1,6 +1,6 @@ from django.test import TestCase from django.db.utils import IntegrityError -from unittest.mock import patch +from unittest.mock import MagicMock, patch from registrar.models import ( Contact, @@ -154,17 +154,34 @@ class TestDomainApplication(TestCase): application.submit() self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_has_previously_had_a_status_of_returns_true(self, mock_get_for_object): + """Set up mock LogEntry.objects.get_for_object to return a log entry with the desired status""" + + log_entry_with_status = MagicMock(changes='{"status": ["previous_status", "desired_status"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + result = self.started_application.has_previously_had_a_status_of("desired_status") + + self.assertTrue(result) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_has_previously_had_a_status_of_returns_false(self, mock_get_for_object): + """Set up mock LogEntry.objects.get_for_object to return a log entry + with a different status than the desired status""" + + log_entry_with_status = MagicMock(changes='{"status": ["previous_status", "different_status"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + result = self.started_application.has_previously_had_a_status_of("desired_status") + + self.assertFalse(result) + def test_submit_sends_email(self): """Create an application and submit it and see if email was sent.""" - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create(email="test@test.gov") - domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=user, - requested_domain=domain, - submitter=contact, - ) - application.save() + + # submitter's email is mayor@igorville.gov + application = completed_application() with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with less_console_noise(): @@ -176,7 +193,185 @@ class TestDomainApplication(TestCase): [ email for email in MockSESClient.EMAILS_SENT - if "test@test.gov" in email["kwargs"]["Destination"]["ToAddresses"] + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_submit_does_not_send_email_if_submitted_previously(self, mock_get_for_object): + """Create an application, make it so it was submitted previously, submit it, + and see that an email was not sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application() + + # Mock the logs + log_entry_with_status = MagicMock(changes='{"status": ["started", "submitted"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_approve_sends_email(self): + """Create an application and approve it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.approve() + + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_approve_does_not_send_email_if_approved_previously(self, mock_get_for_object): + """Create an application, make it so it was approved previously, approve it, + and see that an email was not sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Mock the logs + log_entry_with_status = MagicMock(changes='{"status": ["submitted", "approved"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.approve() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_withdraw_sends_email(self): + """Create an application and withdraw it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.withdraw() + + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_withdraw_does_not_send_email_if_withdrawn_previously(self, mock_get_for_object): + """Create an application, make it so it was withdrawn previously, withdraw it, + and see that an email was not sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Mock the logs + log_entry_with_status = MagicMock(changes='{"status": ["submitted", "withdrawn"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.withdraw() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_reject_sends_email(self): + """Create an application and reject it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.reject() + + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_reject_does_not_send_email_if_rejected_previously(self, mock_get_for_object): + """Create an application, make it so it was rejected previously, reject it, + and see that an email was not sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + + # Mock the logs + log_entry_with_status = MagicMock(changes='{"status": ["submitted", "rejected"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.reject() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] ] ), 0, From e04a8bde61e7f690943356b5131772998c51c560 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 24 Jan 2024 21:48:31 -0500 Subject: [PATCH 02/33] clean up --- src/registrar/models/domain_application.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index e1c809058..0a7bbcd0b 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -576,14 +576,14 @@ class DomainApplication(TimeStampedModel): for entry in log_entries: try: changes_dict = json.loads(entry.changes) - logger.info(changes_dict) # changes_dict will look like {'status': ['withdrawn', 'submitted']}, # henceforth the len(changes_dict.get('status', [])) == 2 if len(changes_dict.get("status", [])) == 2 and changes_dict.get("status", [])[1] == status: - logger.info(f"found one instance where it had a status of {status}") return True except JSONDecodeError: - pass + logger.warning( + "JSON decode error while parsing logs for domain requests in has_previously_had_a_status_of" + ) return False From a1f49329353c7a6b69cf5373203631b60a0bde1b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 26 Jan 2024 16:00:32 -0500 Subject: [PATCH 03/33] Code cleanup --- src/registrar/models/domain_application.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 0a7bbcd0b..da801ce3d 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -578,7 +578,8 @@ class DomainApplication(TimeStampedModel): changes_dict = json.loads(entry.changes) # changes_dict will look like {'status': ['withdrawn', 'submitted']}, # henceforth the len(changes_dict.get('status', [])) == 2 - if len(changes_dict.get("status", [])) == 2 and changes_dict.get("status", [])[1] == status: + status_change = changes_dict.get("status", []) + if len(status_change) == 2 and status_change[1] == status: return True except JSONDecodeError: logger.warning( @@ -656,7 +657,7 @@ class DomainApplication(TimeStampedModel): self.save() # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of("submitted"): + if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.SUBMITTED): self._send_status_update_email( "submission confirmation", "emails/submission_confirmation.txt", @@ -738,7 +739,7 @@ class DomainApplication(TimeStampedModel): ) # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of("approved"): + if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.APPROVED): self._send_status_update_email( "application approved", "emails/status_change_approved.txt", @@ -755,7 +756,7 @@ class DomainApplication(TimeStampedModel): """Withdraw an application that has been submitted.""" # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of("withdrawn"): + if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.WITHDRAWN): self._send_status_update_email( "withdraw", "emails/domain_request_withdrawn.txt", @@ -787,7 +788,7 @@ class DomainApplication(TimeStampedModel): logger.error("Can't query an approved domain while attempting a DA reject()") # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of("rejected"): + if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.REJECTED): self._send_status_update_email( "action needed", "emails/status_change_rejected.txt", From 114feafaf01bf06114ea3cee2b9eec4c1052c469 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jan 2024 14:26:11 -0500 Subject: [PATCH 04/33] Add a domain not ready FSM rule for the new transitions from approved, error handling and unit tests --- src/registrar/admin.py | 7 +- src/registrar/models/domain_application.py | 58 ++++---- src/registrar/tests/test_admin.py | 153 ++++++--------------- src/registrar/tests/test_models.py | 40 ++++++ 4 files changed, 118 insertions(+), 140 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 325081575..9a75d7d72 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -882,14 +882,11 @@ class DomainApplicationAdmin(ListHeaderAdmin): if ( obj and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED - and ( - obj.status == models.DomainApplication.ApplicationStatus.REJECTED - or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE - ) + and obj.status != models.DomainApplication.ApplicationStatus.APPROVED and not obj.domain_is_not_active() ): # If an admin tried to set an approved application to - # rejected or ineligible and the related domain is already + # another status and the related domain is already # active, shortcut the action and throw a friendly # error message. This action would still not go through # shortcut or not as the rules are duplicated on the model, diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 30def9cfc..5f61c9385 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -572,6 +572,19 @@ class DomainApplication(TimeStampedModel): return not self.approved_domain.is_active() return True + def delete_and_clean_up_domain(self, called_from): + try: + domain_state = self.approved_domain.state + # Only reject if it exists on EPP + if domain_state != Domain.State.UNKNOWN: + self.approved_domain.deletedInEpp() + self.approved_domain.save() + self.approved_domain.delete() + self.approved_domain = None + except Exception as err: + logger.error(err) + logger.error(f"Can't query an approved domain while attempting {called_from}") + def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True): """Send a status update email to the submitter. @@ -651,11 +664,19 @@ class DomainApplication(TimeStampedModel): ApplicationStatus.INELIGIBLE, ], target=ApplicationStatus.IN_REVIEW, + conditions=[domain_is_not_active], ) def in_review(self): """Investigate an application that has been submitted. - This action is logged.""" + This action is logged. + + As side effects this will delete the domain and domain_information + (will cascade) when they exist.""" + + if self.status == self.ApplicationStatus.APPROVED: + self.delete_and_clean_up_domain("in_review") + literal = DomainApplication.ApplicationStatus.IN_REVIEW # Check if the tuple exists, then grab its value in_review = literal if literal is not None else "In Review" @@ -670,11 +691,19 @@ class DomainApplication(TimeStampedModel): ApplicationStatus.INELIGIBLE, ], target=ApplicationStatus.ACTION_NEEDED, + conditions=[domain_is_not_active], ) def action_needed(self): """Send back an application that is under investigation or rejected. - This action is logged.""" + This action is logged. + + As side effects this will delete the domain and domain_information + (will cascade) when they exist.""" + + if self.status == self.ApplicationStatus.APPROVED: + self.delete_and_clean_up_domain("reject_with_prejudice") + literal = DomainApplication.ApplicationStatus.ACTION_NEEDED # Check if the tuple is setup correctly, then grab its value action_needed = literal if literal is not None else "Action Needed" @@ -746,18 +775,9 @@ class DomainApplication(TimeStampedModel): As side effects this will delete the domain and domain_information (will cascade), and send an email notification.""" + if self.status == self.ApplicationStatus.APPROVED: - try: - domain_state = self.approved_domain.state - # Only reject if it exists on EPP - if domain_state != Domain.State.UNKNOWN: - self.approved_domain.deletedInEpp() - self.approved_domain.save() - self.approved_domain.delete() - self.approved_domain = None - except Exception as err: - logger.error(err) - logger.error("Can't query an approved domain while attempting a DA reject()") + self.delete_and_clean_up_domain("reject") self._send_status_update_email( "action needed", @@ -786,17 +806,7 @@ class DomainApplication(TimeStampedModel): and domain_information (will cascade) when they exist.""" if self.status == self.ApplicationStatus.APPROVED: - try: - domain_state = self.approved_domain.state - # Only reject if it exists on EPP - if domain_state != Domain.State.UNKNOWN: - self.approved_domain.deletedInEpp() - self.approved_domain.save() - self.approved_domain.delete() - self.approved_domain = None - except Exception as err: - logger.error(err) - logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()") + self.delete_and_clean_up_domain("reject_with_prejudice") self.creator.restrict_user() diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f88e25c2f..6b0c16b5b 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -707,41 +707,13 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) - @boto3_mocking.patching - def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - application.approved_domain = domain - application.save() + def trigger_saving_approved_to_another_state(self, domain_is_active, another_state): + """Helper method that triggers domain request state changes from approved to another state, + with an associated domain that can be either active (READY) or not. - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + Used to test errors when saving a change with an active domain, also used to test side effects + when saving a change goes through.""" - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.REJECTED - self.admin.save_model(request, application, None, True) - - # Assert that the error message was called with the correct argument - messages.error.assert_called_once_with( - request, - "This action is not permitted. The domain " + "is already active.", - ) - - def test_side_effects_when_saving_approved_to_rejected(self): # Create an instance of the model application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) domain = Domain.objects.create(name=application.requested_domain.name) @@ -755,101 +727,60 @@ class TestDomainApplicationAdmin(MockEppLib): # Define a custom implementation for is_active def custom_is_active(self): - return False # Override to return False + return domain_is_active # Override to return True # Use ExitStack to combine patch contexts with ExitStack() as stack: # Patch Domain.is_active and django.contrib.messages.error simultaneously stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) stack.enter_context(patch.object(messages, "error")) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.REJECTED - self.admin.save_model(request, application, None, True) - # Assert that the error message was never called - messages.error.assert_not_called() + application.status = another_state + self.admin.save_model(request, application, None, True) - self.assertEqual(application.approved_domain, None) + # Assert that the error message was called with the correct argument + if domain_is_active: + messages.error.assert_called_once_with( + request, + "This action is not permitted. The domain " + "is already active.", + ) + else: + # Assert that the error message was never called + messages.error.assert_not_called() - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() + self.assertEqual(application.approved_domain, None) - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() + # Assert that Domain got Deleted + with self.assertRaises(Domain.DoesNotExist): + domain.refresh_from_db() + + # Assert that DomainInformation got Deleted + with self.assertRaises(DomainInformation.DoesNotExist): + domain_information.refresh_from_db() + + def test_error_when_saving_approved_to_in_review_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW) + + def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.ACTION_NEEDED) + + def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.REJECTED) def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - application.approved_domain = domain - application.save() + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.INELIGIBLE) - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + def test_side_effects_when_saving_approved_to_in_review(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.IN_REVIEW) - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True + def test_side_effects_when_saving_approved_to_action_needed(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED) - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.INELIGIBLE - self.admin.save_model(request, application, None, True) - - # Assert that the error message was called with the correct argument - messages.error.assert_called_once_with( - request, - "This action is not permitted. The domain " + "is already active.", - ) + def test_side_effects_when_saving_approved_to_rejected(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED) def test_side_effects_when_saving_approved_to_ineligible(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain) - application.approved_domain = domain - application.save() - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser - - # Define a custom implementation for is_active - def custom_is_active(self): - return False # Override to return False - - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.INELIGIBLE - self.admin.save_model(request, application, None, True) - - # Assert that the error message was never called - messages.error.assert_not_called() - - self.assertEqual(application.approved_domain, None) - - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() - - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE) def test_has_correct_filters(self): """ diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d0005cbd5..d2db53817 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -457,6 +457,46 @@ class TestDomainApplication(TestCase): with self.assertRaises(exception_type): application.reject_with_prejudice() + def test_transition_not_allowed_approved_in_review_when_domain_is_active(self): + """Create an application with status approved, create a matching domain that + is active, and call in_review against transition rules""" + + domain = Domain.objects.create(name=self.approved_application.requested_domain.name) + self.approved_application.approved_domain = domain + self.approved_application.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_application.in_review() + + def test_transition_not_allowed_approved_action_needed_when_domain_is_active(self): + """Create an application with status approved, create a matching domain that + is active, and call action_needed against transition rules""" + + domain = Domain.objects.create(name=self.approved_application.requested_domain.name) + self.approved_application.approved_domain = domain + self.approved_application.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_application.action_needed() + def test_transition_not_allowed_approved_rejected_when_domain_is_active(self): """Create an application with status approved, create a matching domain that is active, and call reject against transition rules""" From 211a4b52cf1ada9e77a6a8a54a35ffdc36b6fe3c Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jan 2024 19:16:56 -0500 Subject: [PATCH 05/33] Initial solution and loggers --- src/registrar/views/application.py | 77 ++++++++++++++++++++- src/registrar/views/utility/steps_helper.py | 15 ++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index a15f36ccc..e57a5436d 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -224,8 +224,10 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): if request.path_info == self.NEW_URL_NAME: return render(request, "application_intro.html") else: + logger.info('get calling self.steps.first') return self.goto(self.steps.first) + logger.info('get setting current step') self.steps.current = current_url context = self.get_context_data() context["forms"] = self.get_forms() @@ -254,6 +256,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): All arguments (**kwargs) are passed directly to `get_forms`. """ + logger.info('get_all_forms gettig steps in self.steps') nested = (self.get_forms(step=step, **kwargs) for step in self.steps) flattened = [form for lst in nested for form in lst] return flattened @@ -269,6 +272,8 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): and from the database if `use_db` is True (provided that record exists). An empty form will be provided if neither of those are true. """ + + logger.info('get_forms setting prefix to self.steps.current') kwargs = { "files": files, "prefix": self.steps.current, @@ -328,6 +333,66 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): DomainApplication.ApplicationStatus.ACTION_NEEDED, ] return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses) + + def db_check_for_unlocking_steps(self): + unlocked_steps = {} + + if self.application.organization_type: + unlocked_steps["organization_type"] = True + + if self.application.tribe_name: + unlocked_steps["tribal_government"] = True + + if self.application.federal_agency: + unlocked_steps["organization_federal"] = True + + if self.application.is_election_board: + unlocked_steps["organization_election"] = True + + if ( + self.application.organization_name + or self.application.address_line1 + or self.application.city + or self.application.state_territory + or self.application.zipcode + or self.application.urbanization + ): + unlocked_steps["organization_contact"] = True + + if self.application.about_your_organization: + unlocked_steps["about_your_organization"] = True + + if self.application.authorizing_official: + unlocked_steps["authorizing_official"] = True + + # Since this field is optional, test to see if the next step has been touched + if self.application.current_websites.exists() or self.application.requested_domain: + unlocked_steps["current_sites"] = True + + if self.application.requested_domain: + unlocked_steps["dotgov_domain"] = True + + if self.application.purpose: + unlocked_steps["purpose"] = True + + if self.application.submitter: + unlocked_steps["your_contact"] = True + + if self.application.other_contacts.exists() or self.application.no_other_contacts_rationale: + unlocked_steps["other_contacts"] = True + + # Since this field is optional, test to see if the next step has been touched + if self.application.anything_else or self.application.is_policy_acknowledged: + unlocked_steps["anything_else"] = True + + if self.application.is_policy_acknowledged: + unlocked_steps["requirements"] = True + + if self.application.submission_date: + unlocked_steps["review"] = True + + return unlocked_steps + def get_context_data(self): """Define context for access on all wizard pages.""" @@ -338,11 +403,17 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain) else: modal_heading = "You are about to submit an incomplete request" + + logger.info(f'get_context_data returning value for cisited equals to: {self.storage.get("step_history", [])}') + + unlocked_steps_list = list(self.db_check_for_unlocking_steps().keys()) + + return { "form_titles": self.TITLES, "steps": self.steps, # Add information about which steps should be unlocked - "visited": self.storage.get("step_history", []), + "visited": unlocked_steps_list, "is_federal": self.application.is_federal(), "modal_button": modal_button, "modal_heading": modal_heading, @@ -360,6 +431,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): return step_list def goto(self, step): + logger.info(f'goto sets self.steps.current to passed {step}') self.steps.current = step return redirect(reverse(f"{self.URL_NAMESPACE}:{step}")) @@ -368,6 +440,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): next = self.steps.next if next: self.steps.current = next + logger.info(f'goto sets self.goto_next_step.current to passed {self.steps.next}') return self.goto(next) else: raise Http404() @@ -387,6 +460,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): if button == "intro_acknowledge": if request.path_info == self.NEW_URL_NAME: del self.storage + logger.info(f'post calling goto with {self.steps.first}') return self.goto(self.steps.first) # if accessing this class directly, redirect to the first step @@ -406,6 +480,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): # return them to the page they were already on if button == "save": messages.success(request, "Your progress has been saved!") + logger.info(f'post calling goto with {self.steps.current}') return self.goto(self.steps.current) # if user opted to save progress and return, # return them to the home page diff --git a/src/registrar/views/utility/steps_helper.py b/src/registrar/views/utility/steps_helper.py index f5eca2b55..55f481263 100644 --- a/src/registrar/views/utility/steps_helper.py +++ b/src/registrar/views/utility/steps_helper.py @@ -44,28 +44,35 @@ class StepsHelper: """ def __init__(self, wizard): + logger.info(f"steps_helper __init__") self._wizard = wizard def __dir__(self): + logger.info(f"steps_helper __dir__ {self.all}") return self.all def __len__(self): + logger.info(f"steps_helper __len__ {self.count}") return self.count def __repr__(self): + logger.info(f"steps_helper __repr__ {self._wizard} {self.all}") return "" % (self._wizard, self.all) def __getitem__(self, step): + logger.info(f"steps_helper __getitem__ {self.all[step]}") return self.all[step] @property def all(self): """Returns the names of all steps.""" + logger.info(f"steps_helper all {self._wizard.get_step_list()}") return self._wizard.get_step_list() @property def count(self): """Returns the total number of steps/forms in this the wizard.""" + logger.info(f"steps_helper count {len(self.all)}") return len(self.all) @property @@ -79,12 +86,14 @@ class StepsHelper: current_url = resolve(self._wizard.request.path_info).url_name step = current_url if current_url in self.all else self.first self._wizard.storage["current_step"] = step + logger.info(f"steps_helper current getter {step}") return step @current.setter def current(self, step: str): """Sets the current step. Updates step history.""" if step in self.all: + logger.info(f"steps_helper current setter {step}") self._wizard.storage["current_step"] = step else: logger.debug("Invalid step name %s given to StepHelper" % str(step)) @@ -97,11 +106,13 @@ class StepsHelper: @property def first(self): """Returns the name of the first step.""" + logger.info(f"steps_helper first {self.all[0]}") return self.all[0] @property def last(self): """Returns the name of the last step.""" + logger.info(f"steps_helper last {self.all[-1]}") return self.all[-1] @property @@ -110,6 +121,7 @@ class StepsHelper: steps = self.all index = steps.index(self.current) + 1 if index < self.count: + logger.info(f"steps_helper next {steps[index]}") return steps[index] return None @@ -119,6 +131,7 @@ class StepsHelper: steps = self.all key = steps.index(self.current) - 1 if key >= 0: + logger.info(f"steps_helper prev {steps[key]}") return steps[key] return None @@ -127,10 +140,12 @@ class StepsHelper: """Returns the index for the current step.""" steps = self.all if self.current in steps: + logger.info(f"steps_helper index {steps.index(self)}") return steps.index(self) return None @property def history(self): """Returns the list of already visited steps.""" + logger.info(f"steps_helper history {self._wizard.storage.setdefault('step_history', [])}") return self._wizard.storage.setdefault("step_history", []) From 3fe47296a92427b25dab3ea922c5d80bb55772a4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:02:52 -0700 Subject: [PATCH 06/33] Update run.sh --- src/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run.sh b/src/run.sh index 487c54591..d8dabe0cf 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn registrar.config.wsgi -t 60 +gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 registrar.config.wsgi -t 60 From cfc9a2d1695c2c6c65920225bef4ac7a52daeb72 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:55:35 -0700 Subject: [PATCH 07/33] Test one worker --- src/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run.sh b/src/run.sh index d8dabe0cf..e7512b28d 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 registrar.config.wsgi -t 60 +gunicorn --worker-class=gevent --worker-connections=1000 --workers=1 registrar.config.wsgi -t 60 From 0653ee838512cbdaa0971deed6061c5dd1a9355b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:09:51 -0700 Subject: [PATCH 08/33] Add sleep to mimic --- src/registrar/views/domain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 313762ef1..48d537562 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -6,7 +6,7 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionDeleteView). """ import logging - +import time from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError @@ -150,6 +150,7 @@ class DomainView(DomainBaseView): context["security_email"] = None return context context["security_email"] = security_email + time.sleep(100) return context def in_editable_state(self, pk): From 1130b5db8eaec794e6b2643e24a0e083b1ad5f2b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 1 Feb 2024 17:23:09 -0500 Subject: [PATCH 09/33] unit tests --- .../templates/application_sidebar.html | 29 ++-- src/registrar/tests/test_views.py | 148 +++++++++++++++++- src/registrar/views/application.py | 108 +++++-------- src/registrar/views/utility/steps_helper.py | 15 -- 4 files changed, 200 insertions(+), 100 deletions(-) diff --git a/src/registrar/templates/application_sidebar.html b/src/registrar/templates/application_sidebar.html index 318bea366..da55a623e 100644 --- a/src/registrar/templates/application_sidebar.html +++ b/src/registrar/templates/application_sidebar.html @@ -4,24 +4,27 @@ - \ No newline at end of file + From ad6d080ad94ef7726d88d38adaa58b02cbbecc5e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 2 Feb 2024 15:11:22 -0500 Subject: [PATCH 17/33] Add non-prod banner --- src/registrar/assets/sass/_theme/_alerts.scss | 5 +- src/registrar/templates/admin/base_site.html | 79 ++++++++++++------- src/registrar/templates/base.html | 4 + .../includes/non-production-alert.html | 5 ++ .../test_environment_variables_effects.py | 29 +++++++ 5 files changed, 93 insertions(+), 29 deletions(-) create mode 100644 src/registrar/templates/includes/non-production-alert.html create mode 100644 src/registrar/tests/test_environment_variables_effects.py diff --git a/src/registrar/assets/sass/_theme/_alerts.scss b/src/registrar/assets/sass/_theme/_alerts.scss index 9ee28a357..163f243d3 100644 --- a/src/registrar/assets/sass/_theme/_alerts.scss +++ b/src/registrar/assets/sass/_theme/_alerts.scss @@ -17,5 +17,8 @@ .usa-alert__body::before { left: 1rem !important; } - } + } + .usa-alert__body.margin-left-1 { + margin-left: 0.5rem!important; + } } diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index c0884c912..f9ff23455 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -24,34 +24,57 @@ {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block extrastyle %}{{ block.super }} - + {% endblock %} -{% block branding %} -

.gov admin

-{% if user.is_anonymous %} - {% include "admin/color_theme_toggle.html" %} -{% endif %} +{% block header %} + {% if not IS_PRODUCTION %} + {% with add_body_class="margin-left-1" %} + {% include "includes/non-production-alert.html" %} + {% endwith %} + {% endif %} + + {# Djando update: this div will change to header #} + {% endblock %} -{% comment %} - This was copied from the 'userlinks' template, with a few minor changes. - You can find that here: - https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59 -{% endcomment %} -{% block userlinks %} - {% if site_url %} - {% translate 'View site' %} / - {% endif %} - {% if user.is_active and user.is_staff %} - {% url 'django-admindocs-docroot' as docsroot %} - {% if docsroot %} - {% translate 'Documentation' %} / - {% endif %} - {% endif %} - {% if user.has_usable_password %} - {% translate 'Change password' %} / - {% endif %} - {% translate 'Log out' %} - {% include "admin/color_theme_toggle.html" %} - {% endblock %} -{% block nav-global %}{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 2786cca22..c0702e78f 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -70,6 +70,10 @@ Skip to main content + {% if not IS_PRODUCTION %} + {% include "includes/non-production-alert.html" %} + {% endif %} +
diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html new file mode 100644 index 000000000..e4ce21437 --- /dev/null +++ b/src/registrar/templates/includes/non-production-alert.html @@ -0,0 +1,5 @@ +
+
+ You are not on production. +
+
diff --git a/src/registrar/tests/test_environment_variables_effects.py b/src/registrar/tests/test_environment_variables_effects.py new file mode 100644 index 000000000..03706f179 --- /dev/null +++ b/src/registrar/tests/test_environment_variables_effects.py @@ -0,0 +1,29 @@ +from django.test import Client, TestCase, override_settings +from django.contrib.auth import get_user_model + + +class MyTestCase(TestCase): + def setUp(self): + self.client = Client() + username = "test_user" + first_name = "First" + last_name = "Last" + email = "info@example.com" + self.user = get_user_model().objects.create( + username=username, first_name=first_name, last_name=last_name, email=email + ) + self.client.force_login(self.user) + + def tearDown(self): + super().tearDown() + self.user.delete() + + @override_settings(IS_PRODUCTION=True) + def test_production_environment(self): + home_page = self.client.get("/") + self.assertNotContains(home_page, "You are not on production.") + + @override_settings(IS_PRODUCTION=False) + def test_non_production_environment(self): + home_page = self.client.get("/") + self.assertContains(home_page, "You are not on production.") From fa8122b2179e85a0b1038ce2e98cb2b60678bd4e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 2 Feb 2024 15:16:38 -0500 Subject: [PATCH 18/33] Add test defs --- src/registrar/tests/test_environment_variables_effects.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/tests/test_environment_variables_effects.py b/src/registrar/tests/test_environment_variables_effects.py index 03706f179..9ef065aeb 100644 --- a/src/registrar/tests/test_environment_variables_effects.py +++ b/src/registrar/tests/test_environment_variables_effects.py @@ -20,10 +20,12 @@ class MyTestCase(TestCase): @override_settings(IS_PRODUCTION=True) def test_production_environment(self): + """No banner on prod.""" home_page = self.client.get("/") self.assertNotContains(home_page, "You are not on production.") @override_settings(IS_PRODUCTION=False) def test_non_production_environment(self): + """Banner on non-prod.""" home_page = self.client.get("/") self.assertContains(home_page, "You are not on production.") From 02d147ae114349fc1ea5295bb31a450f51ce467e Mon Sep 17 00:00:00 2001 From: rachidatecs <107004823+rachidatecs@users.noreply.github.com> Date: Mon, 5 Feb 2024 11:56:46 -0500 Subject: [PATCH 19/33] Update src/registrar/templates/includes/non-production-alert.html Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/templates/includes/non-production-alert.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index e4ce21437..4f8aaeac0 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -1,5 +1,5 @@
- You are not on production. + WARNING: You are not on production.
From 81c8fe97fad0da26faa745536753e3100e5d0956 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 5 Feb 2024 17:24:39 -0500 Subject: [PATCH 20/33] solidify the bool checks on db_check_for_unlocking_steps --- src/registrar/tests/test_views.py | 4 +-- src/registrar/utility/csv_export.py | 16 ++++++++++ src/registrar/views/application.py | 46 +++++++++++++++-------------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8c992036f..66ed27bbc 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -2431,7 +2431,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest): self.assertContains(detail_page, "link_usa-checked", count=11) else: - self.fail("Expected a redirect, but got a different response") + self.fail(f"Expected a redirect, but got a different response: {response}") def test_unlocked_steps_partial_application(self): """Test when some fields in the application are filled.""" @@ -2498,7 +2498,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest): self.assertContains(detail_page, "link_usa-checked", count=5) else: - self.fail("Expected a redirect, but got a different response") + self.fail(f"Expected a redirect, but got a different response: {response}") class TestWithDomainPermissions(TestWithUser): diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index f9608f553..0046058e6 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -92,6 +92,12 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None "Deleted": domain.deleted, } + # user_emails = [user.email for user in domain.permissions] + + # Dynamically add user emails to the FIELDS dictionary + # for i, user_email in enumerate(user_emails, start=1): + # FIELDS[f"User{i} email"] = user_email + row = [FIELDS.get(column, "") for column in columns] return row @@ -127,6 +133,16 @@ def write_body( else: logger.warning("csv_export -> Domain was none for PublicContact") + # all_user_nums = 0 + # for domain_info in all_domain_infos: + # user_num = len(domain_info.domain.permissions) + # all_user_nums.append(user_num) + + # if user_num > highest_user_nums: + # highest_user_nums = user_num + + # Build the header here passing to it highest_user_nums + # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) for page_num in paginator.page_range: diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index a6b9134d4..b71018d81 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -338,35 +338,37 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): def db_check_for_unlocking_steps(self): """Helper for get_context_data - Queries the DB for an application and returns a dict for unlocked steps.""" + Queries the DB for an application and returns a list of unlocked steps.""" history_dict = { - "organization_type": bool(self.application.organization_type), - "tribal_government": bool(self.application.tribe_name), - "organization_federal": bool(self.application.federal_type), - "organization_election": bool(self.application.is_election_board), + "organization_type": self.application.organization_type is not None, + "tribal_government": self.application.tribe_name is not None, + "organization_federal": self.application.federal_type is not None, + "organization_election": self.application.is_election_board is not None, "organization_contact": ( - bool(self.application.federal_agency) - or bool(self.application.organization_name) - or bool(self.application.address_line1) - or bool(self.application.city) - or bool(self.application.state_territory) - or bool(self.application.zipcode) - or bool(self.application.urbanization) + self.application.federal_agency is not None + or self.application.organization_name is not None + or self.application.address_line1 is not None + or self.application.city is not None + or self.application.state_territory is not None + or self.application.zipcode is not None + or self.application.urbanization is not None ), - "about_your_organization": bool(self.application.about_your_organization), - "authorizing_official": bool(self.application.authorizing_official), + "about_your_organization": self.application.about_your_organization is not None, + "authorizing_official": self.application.authorizing_official is not None, "current_sites": ( - bool(self.application.current_websites.exists()) or bool(self.application.requested_domain) + self.application.current_websites.exists() or self.application.requested_domain is not None ), - "dotgov_domain": bool(self.application.requested_domain), - "purpose": bool(self.application.purpose), - "your_contact": bool(self.application.submitter), + "dotgov_domain": self.application.requested_domain is not None, + "purpose": self.application.purpose is not None, + "your_contact": self.application.submitter is not None, "other_contacts": ( - bool(self.application.other_contacts.exists()) or bool(self.application.no_other_contacts_rationale) + self.application.other_contacts.exists() or self.application.no_other_contacts_rationale is not None ), - "anything_else": (bool(self.application.anything_else) or bool(self.application.is_policy_acknowledged)), - "requirements": bool(self.application.is_policy_acknowledged), - "review": bool(self.application.is_policy_acknowledged), + "anything_else": ( + self.application.anything_else is not None or self.application.is_policy_acknowledged is not None + ), + "requirements": self.application.is_policy_acknowledged is not None, + "review": self.application.is_policy_acknowledged is not None, } return [key for key, value in history_dict.items() if value] From 19764e81511f40fafeb17dff34181788f044fb48 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 5 Feb 2024 19:14:09 -0700 Subject: [PATCH 21/33] Added fake submitted dates to fixtures --- src/registrar/fixtures_applications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py index 92094b876..9519dfce9 100644 --- a/src/registrar/fixtures_applications.py +++ b/src/registrar/fixtures_applications.py @@ -1,3 +1,4 @@ +import datetime import logging import random from faker import Faker @@ -104,7 +105,7 @@ class DomainApplicationFixture: # Random choice of agency for selects, used as placeholders for testing. else random.choice(DomainApplication.AGENCIES) # nosec ) - + da.submission_date = fake.date() da.federal_type = ( app["federal_type"] if "federal_type" in app From 09b0ebb8922f9c0a28326b169d18a6deffcb8652 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:13:00 -0700 Subject: [PATCH 22/33] Revert "Change manifest health check" This reverts commit 2c3efb4cbce24696f069a5f4034789f0b9c11084. --- ops/manifests/manifest-za.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ops/manifests/manifest-za.yaml b/ops/manifests/manifest-za.yaml index 54cdc0262..271f49da9 100644 --- a/ops/manifests/manifest-za.yaml +++ b/ops/manifests/manifest-za.yaml @@ -11,7 +11,7 @@ applications: command: ./run.sh health-check-type: http health-check-http-endpoint: /health - health-check-invocation-timeout: 1 + health-check-invocation-timeout: 40 env: # Send stdout and stderr straight to the terminal without buffering PYTHONUNBUFFERED: yup From 175c721025a0847d205d5c0ac6d10c3948144a8e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 6 Feb 2024 12:30:11 -0700 Subject: [PATCH 23/33] linted --- src/registrar/fixtures_applications.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py index 9519dfce9..659a3040e 100644 --- a/src/registrar/fixtures_applications.py +++ b/src/registrar/fixtures_applications.py @@ -1,4 +1,3 @@ -import datetime import logging import random from faker import Faker From 18cc0e578e9b7892133c124d17c78aaaf42b541c Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 6 Feb 2024 13:22:08 -0800 Subject: [PATCH 24/33] limit changes to just gevent --- src/registrar/views/domain.py | 2 -- src/run.sh | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 48d537562..094ad86da 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -6,7 +6,6 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionDeleteView). """ import logging -import time from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError @@ -150,7 +149,6 @@ class DomainView(DomainBaseView): context["security_email"] = None return context context["security_email"] = security_email - time.sleep(100) return context def in_editable_state(self, pk): diff --git a/src/run.sh b/src/run.sh index e7512b28d..04987c154 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn --worker-class=gevent --worker-connections=1000 --workers=1 registrar.config.wsgi -t 60 +gunicorn --worker-class=gevent registrar.config.wsgi -t 60 \ No newline at end of file From 239b704a920696b9f96f89bd4a9cd9a3ae8d51d5 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 6 Feb 2024 13:40:11 -0800 Subject: [PATCH 25/33] fixed newlines --- src/registrar/views/domain.py | 1 + src/run.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 094ad86da..313762ef1 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -6,6 +6,7 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionDeleteView). """ import logging + from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError diff --git a/src/run.sh b/src/run.sh index 04987c154..1d35cd617 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn --worker-class=gevent registrar.config.wsgi -t 60 \ No newline at end of file +gunicorn --worker-class=gevent registrar.config.wsgi -t 60 From 0eac1db3ad6f2e9b6d463c496383d94893d07c04 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 11:00:21 -0500 Subject: [PATCH 26/33] Revise banner color and copy --- src/registrar/assets/sass/_theme/_uswds-theme.scss | 4 ++++ src/registrar/templates/includes/non-production-alert.html | 4 ++-- src/registrar/tests/test_environment_variables_effects.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_uswds-theme.scss b/src/registrar/assets/sass/_theme/_uswds-theme.scss index 0cdf6675e..a26f23508 100644 --- a/src/registrar/assets/sass/_theme/_uswds-theme.scss +++ b/src/registrar/assets/sass/_theme/_uswds-theme.scss @@ -116,6 +116,10 @@ in the form $setting: value, $theme-color-success-light: $dhs-green-30, $theme-color-success-lighter: $dhs-green-15, + /*--------------------------- + ## Emergency state + ----------------------------*/ + $theme-color-emergency: #FFC3F9, /*--------------------------- # Input settings diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index 4f8aaeac0..811e1f9de 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -1,5 +1,5 @@ -
+
- WARNING: You are not on production. + WARNING: You are on a test site.
diff --git a/src/registrar/tests/test_environment_variables_effects.py b/src/registrar/tests/test_environment_variables_effects.py index 9ef065aeb..3a838c2a2 100644 --- a/src/registrar/tests/test_environment_variables_effects.py +++ b/src/registrar/tests/test_environment_variables_effects.py @@ -22,10 +22,10 @@ class MyTestCase(TestCase): def test_production_environment(self): """No banner on prod.""" home_page = self.client.get("/") - self.assertNotContains(home_page, "You are not on production.") + self.assertNotContains(home_page, "You are on a test site.") @override_settings(IS_PRODUCTION=False) def test_non_production_environment(self): """Banner on non-prod.""" home_page = self.client.get("/") - self.assertContains(home_page, "You are not on production.") + self.assertContains(home_page, "You are on a test site.") From 8f1ab863ee8e22678a38ef1cf2206fd025d1c17c Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 11:44:09 -0500 Subject: [PATCH 27/33] Change warning to attention --- src/registrar/templates/includes/non-production-alert.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index 811e1f9de..8e40892bc 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -1,5 +1,5 @@
- WARNING: You are on a test site. + Attention: You are on a test site.
From 7bcb1e2e4f7bd3148dbbb14385967ea175ab138c Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 16:14:25 -0500 Subject: [PATCH 28/33] New acs: skip emails for transitions from action needed and in review to submitted --- src/registrar/models/domain_application.py | 65 +++----- src/registrar/tests/test_admin.py | 51 +++++-- src/registrar/tests/test_models.py | 165 ++++++--------------- 3 files changed, 103 insertions(+), 178 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index da801ce3d..f96ec1040 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -1,6 +1,4 @@ from __future__ import annotations -from json import JSONDecodeError -import json from typing import Union import logging @@ -14,7 +12,6 @@ from registrar.models.domain import Domain from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError from itertools import chain -from auditlog.models import LogEntry # type: ignore logger = logging.getLogger(__name__) @@ -568,26 +565,6 @@ class DomainApplication(TimeStampedModel): except Exception: return "" - def has_previously_had_a_status_of(self, status): - """Return True if this request has previously had the status of {passed param}.""" - - log_entries = LogEntry.objects.get_for_object(self) - - for entry in log_entries: - try: - changes_dict = json.loads(entry.changes) - # changes_dict will look like {'status': ['withdrawn', 'submitted']}, - # henceforth the len(changes_dict.get('status', [])) == 2 - status_change = changes_dict.get("status", []) - if len(status_change) == 2 and status_change[1] == status: - return True - except JSONDecodeError: - logger.warning( - "JSON decode error while parsing logs for domain requests in has_previously_had_a_status_of" - ) - - return False - def domain_is_not_active(self): if self.approved_domain: return not self.approved_domain.is_active() @@ -656,8 +633,8 @@ class DomainApplication(TimeStampedModel): self.submission_date = timezone.now().date() self.save() - # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.SUBMITTED): + # Limit email notifications to transitions from Started and Withdrawn + if self.status == self.ApplicationStatus.STARTED or self.status == self.ApplicationStatus.WITHDRAWN: self._send_status_update_email( "submission confirmation", "emails/submission_confirmation.txt", @@ -738,14 +715,12 @@ class DomainApplication(TimeStampedModel): user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) - # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.APPROVED): - self._send_status_update_email( - "application approved", - "emails/status_change_approved.txt", - "emails/status_change_approved_subject.txt", - send_email, - ) + self._send_status_update_email( + "application approved", + "emails/status_change_approved.txt", + "emails/status_change_approved_subject.txt", + send_email, + ) @transition( field="status", @@ -755,13 +730,11 @@ class DomainApplication(TimeStampedModel): def withdraw(self): """Withdraw an application that has been submitted.""" - # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.WITHDRAWN): - self._send_status_update_email( - "withdraw", - "emails/domain_request_withdrawn.txt", - "emails/domain_request_withdrawn_subject.txt", - ) + self._send_status_update_email( + "withdraw", + "emails/domain_request_withdrawn.txt", + "emails/domain_request_withdrawn_subject.txt", + ) @transition( field="status", @@ -787,13 +760,11 @@ class DomainApplication(TimeStampedModel): logger.error(err) logger.error("Can't query an approved domain while attempting a DA reject()") - # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.REJECTED): - self._send_status_update_email( - "action needed", - "emails/status_change_rejected.txt", - "emails/status_change_rejected_subject.txt", - ) + self._send_status_update_email( + "action needed", + "emails/status_change_rejected.txt", + "emails/status_change_rejected_subject.txt", + ) @transition( field="status", diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 7773cb60b..83f777189 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -456,8 +456,11 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertIn(expected_string, email_body) def test_save_model_sends_submitted_email(self): - """When transitioning to submitted the first time (and the first time only) on a domain request, - an email is sent out.""" + """When transitioning to submitted from started or withdrawn on a domain request, + an email is sent out. + + When transitioning to submitted from dns needed or in review on a domain request, + no email is sent out.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -466,7 +469,7 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a sample application application = completed_application() - # Test Submitted Status + # Test Submitted Status from started self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) @@ -478,13 +481,33 @@ class TestDomainApplicationAdmin(MockEppLib): ) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - # Test Submitted Status Again (No new email should be sent) + # Test Submitted Status Again (from withdrawn) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to IN_REVIEW + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test Submitted Status Again from in IN_REVIEW, no new email should be sent + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to IN_REVIEW + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to ACTION_NEEDED + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sends_approved_email(self): - """When transitioning to approved the first time (and the first time only) on a domain request, - an email is sent out.""" + """When transitioning to approved on a domain request, + an email is sent out every time.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -505,11 +528,11 @@ class TestDomainApplicationAdmin(MockEppLib): # Test Submitted Status Again (No new email should be sent) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sends_rejected_email(self): - """When transitioning to rejected the first time (and the first time only) on a domain request, - an email is sent out.""" + """When transitioning to rejected on a domain request, + an email is sent out every time.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -530,11 +553,11 @@ class TestDomainApplicationAdmin(MockEppLib): # Test Submitted Status Again (No new email should be sent) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sends_withdrawn_email(self): - """When transitioning to withdrawn the first time (and the first time only) on a domain request, - an email is sent out.""" + """When transitioning to withdrawn on a domain request, + an email is sent out every time.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -557,7 +580,7 @@ class TestDomainApplicationAdmin(MockEppLib): # Test Submitted Status Again (No new email should be sent) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sets_approved_domain(self): # make sure there is no user with this email diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index a1b22373d..66c8d04f8 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1,6 +1,6 @@ from django.test import TestCase from django.db.utils import IntegrityError -from unittest.mock import MagicMock, patch +from unittest.mock import patch from registrar.models import ( Contact, @@ -154,30 +154,7 @@ class TestDomainApplication(TestCase): application.submit() self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_has_previously_had_a_status_of_returns_true(self, mock_get_for_object): - """Set up mock LogEntry.objects.get_for_object to return a log entry with the desired status""" - - log_entry_with_status = MagicMock(changes='{"status": ["previous_status", "desired_status"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - result = self.started_application.has_previously_had_a_status_of("desired_status") - - self.assertTrue(result) - - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_has_previously_had_a_status_of_returns_false(self, mock_get_for_object): - """Set up mock LogEntry.objects.get_for_object to return a log entry - with a different status than the desired status""" - - log_entry_with_status = MagicMock(changes='{"status": ["previous_status", "different_status"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - result = self.started_application.has_previously_had_a_status_of("desired_status") - - self.assertFalse(result) - - def test_submit_sends_email(self): + def test_submit_from_started_sends_email(self): """Create an application and submit it and see if email was sent.""" # submitter's email is mayor@igorville.gov @@ -199,17 +176,55 @@ class TestDomainApplication(TestCase): 0, ) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_submit_does_not_send_email_if_submitted_previously(self, mock_get_for_object): - """Create an application, make it so it was submitted previously, submit it, - and see that an email was not sent.""" + def test_submit_from_withdrawn_sends_email(self): + """Create a withdrawn application and submit it and see if email was sent.""" # submitter's email is mayor@igorville.gov - application = completed_application() + application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN) - # Mock the logs - log_entry_with_status = MagicMock(changes='{"status": ["started", "submitted"]}') - mock_get_for_object.return_value = [log_entry_with_status] + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() + + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_submit_from_action_needed_does_not_send_email(self): + """Create a withdrawn application and submit it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_submit_from_in_review_does_not_send_email(self): + """Create a withdrawn application and submit it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with less_console_noise(): @@ -249,34 +264,6 @@ class TestDomainApplication(TestCase): 0, ) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_approve_does_not_send_email_if_approved_previously(self, mock_get_for_object): - """Create an application, make it so it was approved previously, approve it, - and see that an email was not sent.""" - - # submitter's email is mayor@igorville.gov - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Mock the logs - log_entry_with_status = MagicMock(changes='{"status": ["submitted", "approved"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.approve() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) - def test_withdraw_sends_email(self): """Create an application and withdraw it and see if email was sent.""" @@ -299,34 +286,6 @@ class TestDomainApplication(TestCase): 0, ) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_withdraw_does_not_send_email_if_withdrawn_previously(self, mock_get_for_object): - """Create an application, make it so it was withdrawn previously, withdraw it, - and see that an email was not sent.""" - - # submitter's email is mayor@igorville.gov - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Mock the logs - log_entry_with_status = MagicMock(changes='{"status": ["submitted", "withdrawn"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.withdraw() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) - def test_reject_sends_email(self): """Create an application and reject it and see if email was sent.""" @@ -349,34 +308,6 @@ class TestDomainApplication(TestCase): 0, ) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_reject_does_not_send_email_if_rejected_previously(self, mock_get_for_object): - """Create an application, make it so it was rejected previously, reject it, - and see that an email was not sent.""" - - # submitter's email is mayor@igorville.gov - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - - # Mock the logs - log_entry_with_status = MagicMock(changes='{"status": ["submitted", "rejected"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.reject() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) - def test_submit_transition_allowed(self): """ Test that calling submit from allowable statuses does raises TransitionNotAllowed. From 3318892d6827f5f50b17a1c570f8cf56ff87c1b9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 16:29:16 -0500 Subject: [PATCH 29/33] fix indent error --- src/registrar/tests/test_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d0782c359..7f866ad8b 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -167,8 +167,8 @@ class TestDomainApplication(TestCase): # submitter's email is mayor@igorville.gov application = completed_application() - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.submit() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.submit() # check to see if an email was sent self.assertGreater( From 0a942e4183fe0201338fa7f16ffee16992f6357e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 16:43:51 -0500 Subject: [PATCH 30/33] remove duplicate test def --- src/registrar/tests/test_admin.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index a5ea89072..f90b18584 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -744,13 +744,6 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) - def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - application.approved_domain = domain - application.save() - def trigger_saving_approved_to_another_state(self, domain_is_active, another_state): """Helper method that triggers domain request state changes from approved to another state, with an associated domain that can be either active (READY) or not. From 5abee70d2ea306f8fa04d305c50abc88ab972d48 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 8 Feb 2024 15:23:13 -0700 Subject: [PATCH 31/33] Update domain.py --- src/registrar/views/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 04fe1ce3a..e6af77b6c 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -555,7 +555,7 @@ class DomainYourContactInformationView(DomainFormBaseView): # Post to DB using values from the form form.save() - messages.success(self.request, "Your contact information has been updated.") + messages.success(self.request, "Your contact information for all your domains has been updated.") # superclass has the redirect return super().form_valid(form) From 7e37b93fc702f1eb5a750cf2888fdd42a7e26128 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 9 Feb 2024 11:53:19 -0500 Subject: [PATCH 32/33] Refactor model tests for less repetition --- src/registrar/models/domain_application.py | 4 +- src/registrar/tests/test_models.py | 167 +++++---------------- 2 files changed, 39 insertions(+), 132 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 0399a039e..f048bdb89 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -655,7 +655,9 @@ class DomainApplication(TimeStampedModel): self.save() # Limit email notifications to transitions from Started and Withdrawn - if self.status == self.ApplicationStatus.STARTED or self.status == self.ApplicationStatus.WITHDRAWN: + limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN] + + if self.status in limited_statuses: self._send_status_update_email( "submission confirmation", "emails/submission_confirmation.txt", diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 7f866ad8b..0cb050f41 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -161,158 +161,63 @@ class TestDomainApplication(TestCase): application.submit() self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) + def check_email_sent(self, application, msg, action, expected_count): + """Check if an email was sent after performing an action.""" + + with self.subTest(msg=msg, action=action): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Perform the specified action + action_method = getattr(application, action) + action_method() + + # Check if an email was sent + sent_emails = [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + self.assertEqual(len(sent_emails), expected_count) + def test_submit_from_started_sends_email(self): - """Create an application and submit it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application and submit it and see if email was sent." application = completed_application() - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.submit() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "submit", 1) def test_submit_from_withdrawn_sends_email(self): - """Create a withdrawn application and submit it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create a withdrawn application and submit it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.submit() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "submit", 1) def test_submit_from_action_needed_does_not_send_email(self): - """Create a withdrawn application and submit it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application with ACTION_NEEDED status and submit it, check if email was not sent." application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.submit() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "submit", 0) def test_submit_from_in_review_does_not_send_email(self): - """Create a withdrawn application and submit it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create a withdrawn application and submit it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.submit() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "submit", 0) def test_approve_sends_email(self): - """Create an application and approve it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application and approve it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.approve() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "approve", 1) def test_withdraw_sends_email(self): - """Create an application and withdraw it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application and withdraw it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.withdraw() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "withdraw", 1) def test_reject_sends_email(self): - """Create an application and reject it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application and reject it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + self.check_email_sent(application, msg, "reject", 1) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.reject() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + def test_reject_with_prejudice_does_not_send_email(self): + msg = "Create an application and reject it with prejudice and see if email was sent." + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + self.check_email_sent(application, msg, "reject_with_prejudice", 0) def test_submit_transition_allowed(self): """ From 77d88dd82f9c8b2819033a72795cb425506234ce Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:39:06 -0500 Subject: [PATCH 33/33] Change "Add user" to "Add a domain manager" (#1761) --- src/registrar/templates/domain_add_user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index d67c343a6..65290832d 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -18,7 +18,7 @@ + >Add a domain manager {% endblock %} {# domain_content #}