From 40cc30a98849863e5e981b86ec84facd996d121f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 11:17:51 -0500 Subject: [PATCH 01/29] django admin updates for domain and application lists --- src/registrar/admin.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c5f5be276..21f966da3 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -767,6 +767,12 @@ class DomainApplicationAdmin(ListHeaderAdmin): "requested_domain", "status", "organization_type", + "federal_agency", + "federal_type", + "organization_name", + "is_election_board", + "city", + "state_territory", "created_at", "submitter", "investigator", @@ -1038,6 +1044,12 @@ class DomainAdmin(ListHeaderAdmin): list_display = [ "name", "organization_type", + "federal_agency", + "federal_type", + "organization_name", + "is_election_board", + "city", + "state_territory", "state", "expiration_date", "created_at", @@ -1061,6 +1073,24 @@ class DomainAdmin(ListHeaderAdmin): organization_type.admin_order_field = "domain_info__organization_type" # type: ignore + def federal_agency(self, obj): + return obj.domain_info.federal_agency if obj.domain_info else None + + def federal_type(self, obj): + return obj.domain_info.federal_type if obj.domain_info else None + + def organization_name(self, obj): + return obj.domain_info.organization_name if obj.domain_info else None + + def is_election_board(self, obj): + return obj.domain_info.is_election_board if obj.domain_info else None + + def city(self, obj): + return obj.domain_info.city if obj.domain_info else None + + def state_territory(self, obj): + return obj.domain_info.state_territory if obj.domain_info else None + # Filters list_filter = ["domain_info__organization_type", "state"] From e138458105f5c2f20443c410d4951d88da65dd16 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 11:24:22 -0500 Subject: [PATCH 02/29] fixed sorting --- src/registrar/admin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 21f966da3..2f1a63966 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1075,21 +1075,27 @@ class DomainAdmin(ListHeaderAdmin): def federal_agency(self, obj): return obj.domain_info.federal_agency if obj.domain_info else None + federal_agency.admin_order_field = "domain_info__federal_agency" def federal_type(self, obj): return obj.domain_info.federal_type if obj.domain_info else None + federal_type.admin_order_field = "domain_info__federal_type" def organization_name(self, obj): return obj.domain_info.organization_name if obj.domain_info else None + organization_name.admin_order_field = "domain_info__organization_name" def is_election_board(self, obj): return obj.domain_info.is_election_board if obj.domain_info else None + is_election_board.admin_order_field = "domain_info__is_election_board" def city(self, obj): return obj.domain_info.city if obj.domain_info else None + city.admin_order_field = "domain_info__city" def state_territory(self, obj): return obj.domain_info.state_territory if obj.domain_info else None + state_territory.admin_order_field = "domain_info__state_territory" # Filters list_filter = ["domain_info__organization_type", "state"] From bd418c3dca32d26aa602a97539ec62b92a662da7 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 11:32:09 -0500 Subject: [PATCH 03/29] formatted for linting --- src/registrar/admin.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 2f1a63966..bac361c6a 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1075,28 +1075,34 @@ class DomainAdmin(ListHeaderAdmin): def federal_agency(self, obj): return obj.domain_info.federal_agency if obj.domain_info else None - federal_agency.admin_order_field = "domain_info__federal_agency" + + federal_agency.admin_order_field = "domain_info__federal_agency" # type: ignore def federal_type(self, obj): return obj.domain_info.federal_type if obj.domain_info else None - federal_type.admin_order_field = "domain_info__federal_type" - + + federal_type.admin_order_field = "domain_info__federal_type" # type: ignore + def organization_name(self, obj): return obj.domain_info.organization_name if obj.domain_info else None - organization_name.admin_order_field = "domain_info__organization_name" + + organization_name.admin_order_field = "domain_info__organization_name" # type: ignore def is_election_board(self, obj): return obj.domain_info.is_election_board if obj.domain_info else None - is_election_board.admin_order_field = "domain_info__is_election_board" + + is_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore def city(self, obj): return obj.domain_info.city if obj.domain_info else None - city.admin_order_field = "domain_info__city" - + + city.admin_order_field = "domain_info__city" # type: ignore + def state_territory(self, obj): return obj.domain_info.state_territory if obj.domain_info else None - state_territory.admin_order_field = "domain_info__state_territory" - + + state_territory.admin_order_field = "domain_info__state_territory" # type: ignore + # Filters list_filter = ["domain_info__organization_type", "state"] From 4a3a72cfdd38945acf5c899be6384fff2240c40c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 12:41:15 -0500 Subject: [PATCH 04/29] fixed tests and cleaned up noise in existing tests --- src/registrar/tests/test_admin.py | 1779 +++++++++++++++-------------- 1 file changed, 911 insertions(+), 868 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f90b18584..1b133a052 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -68,9 +68,9 @@ class TestDomainAdmin(MockEppLib): response = self.client.get("/admin/registrar/domain/") - # There are 3 template references to Federal (3) plus one reference in the table + # There are 4 template references to Federal (4) plus four references in the table # for our actual application - self.assertContains(response, "Federal", count=4) + self.assertContains(response, "Federal", count=8) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist @@ -270,40 +270,43 @@ class TestDomainApplicationAdminForm(TestCase): self.application = completed_application() def test_form_choices(self): - # Create a form instance with the test application - form = DomainApplicationAdminForm(instance=self.application) + with less_console_noise(): + # Create a form instance with the test application + form = DomainApplicationAdminForm(instance=self.application) - # Verify that the form choices match the available transitions for started - expected_choices = [("started", "Started"), ("submitted", "Submitted")] - self.assertEqual(form.fields["status"].widget.choices, expected_choices) + # Verify that the form choices match the available transitions for started + expected_choices = [("started", "Started"), ("submitted", "Submitted")] + self.assertEqual(form.fields["status"].widget.choices, expected_choices) def test_form_choices_when_no_instance(self): - # Create a form instance without an instance - form = DomainApplicationAdminForm() + with less_console_noise(): + # Create a form instance without an instance + form = DomainApplicationAdminForm() - # Verify that the form choices show all choices when no instance is provided; - # this is necessary to show all choices when creating a new domain - # application in django admin; - # note that FSM ensures that no domain application exists with invalid status, - # so don't need to test for invalid status - self.assertEqual( - form.fields["status"].widget.choices, - DomainApplication._meta.get_field("status").choices, - ) + # Verify that the form choices show all choices when no instance is provided; + # this is necessary to show all choices when creating a new domain + # application in django admin; + # note that FSM ensures that no domain application exists with invalid status, + # so don't need to test for invalid status + self.assertEqual( + form.fields["status"].widget.choices, + DomainApplication._meta.get_field("status").choices, + ) def test_form_choices_when_ineligible(self): - # Create a form instance with a domain application with ineligible status - ineligible_application = DomainApplication(status="ineligible") + with less_console_noise(): + # Create a form instance with a domain application with ineligible status + ineligible_application = DomainApplication(status="ineligible") - # Attempt to create a form with the ineligible application - # The form should not raise an error, but choices should be the - # full list of possible choices - form = DomainApplicationAdminForm(instance=ineligible_application) + # Attempt to create a form with the ineligible application + # The form should not raise an error, but choices should be the + # full list of possible choices + form = DomainApplicationAdminForm(instance=ineligible_application) - self.assertEqual( - form.fields["status"].widget.choices, - DomainApplication._meta.get_field("status").choices, - ) + self.assertEqual( + form.fields["status"].widget.choices, + DomainApplication._meta.get_field("status").choices, + ) @boto3_mocking.patching @@ -327,90 +330,94 @@ class TestDomainApplicationAdmin(MockEppLib): def test_domain_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") + multiple_unalphabetical_domain_objects("application") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("requested_domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("requested_domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",)) def test_submitter_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") + multiple_unalphabetical_domain_objects("application") - additional_application = generic_domain_object("application", "Xylophone") - new_user = User.objects.filter(username=additional_application.investigator.username).get() - new_user.first_name = "Xylophonic" - new_user.save() + additional_application = generic_domain_object("application", "Xylophone") + new_user = User.objects.filter(username=additional_application.investigator.username).get() + new_user.first_name = "Xylophonic" + new_user.save() - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "5", - ( - "submitter__first_name", - "submitter__last_name", - ), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "11", + ( + "submitter__first_name", + "submitter__last_name", + ), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted( - "-5", - ( - "-submitter__first_name", - "-submitter__last_name", - ), - ) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted( + "-11", + ( + "-submitter__first_name", + "-submitter__last_name", + ), + ) def test_investigator_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") - additional_application = generic_domain_object("application", "Xylophone") - new_user = User.objects.filter(username=additional_application.investigator.username).get() - new_user.first_name = "Xylophonic" - new_user.save() + multiple_unalphabetical_domain_objects("application") + additional_application = generic_domain_object("application", "Xylophone") + new_user = User.objects.filter(username=additional_application.investigator.username).get() + new_user.first_name = "Xylophonic" + new_user.save() - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "6", - ( - "investigator__first_name", - "investigator__last_name", - ), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "6", + ( + "investigator__first_name", + "investigator__last_name", + ), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted( - "-6", - ( - "-investigator__first_name", - "-investigator__last_name", - ), - ) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted( + "-6", + ( + "-investigator__first_name", + "-investigator__last_name", + ), + ) def test_short_org_name_in_applications_list(self): """ Make sure the short name is displaying in admin on the list page """ - self.client.force_login(self.superuser) - completed_application() - response = self.client.get("/admin/registrar/domainapplication/") - # There are 3 template references to Federal (3) plus one reference in the table - # for our actual application - self.assertContains(response, "Federal", count=4) - # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) - # Now let's make sure the long description does not exist - self.assertNotContains(response, "Federal: an agency of the U.S. government") + with less_console_noise(): + self.client.force_login(self.superuser) + completed_application() + response = self.client.get("/admin/registrar/domainapplication/") + # There are 4 template references to Federal (4) plus two references in the table + # for our actual application + self.assertContains(response, "Federal", count=6) + # This may be a bit more robust + self.assertContains(response, 'Federal', count=1) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") def transition_state_and_send_email(self, application, status): """Helper method for the email test cases.""" @@ -430,20 +437,21 @@ class TestDomainApplicationAdmin(MockEppLib): """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"] + with less_console_noise(): + # 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"] + # 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) + # 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): """When transitioning to submitted from started or withdrawn on a domain request, @@ -452,297 +460,303 @@ class TestDomainApplicationAdmin(MockEppLib): 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" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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() + # Create a sample application + application = completed_application() - # 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) + # 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) - # 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 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 (from withdrawn) - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Test Submitted Status Again (from withdrawn) + 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 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) + # 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 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) + # 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) + # 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 on a domain request, an email is sent out every time.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # 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) + # 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) - # 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 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), 3) + # 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), 3) def test_save_model_sends_rejected_email(self): """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" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # 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 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 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), 3) + # 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), 3) def test_save_model_sends_withdrawn_email(self): """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" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # 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 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 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), 3) + # 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), 3) def test_save_model_sets_approved_domain(self): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # make sure 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) + # 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)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): # 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) - # Test that approved domain exists and equals requested domain - self.assertEqual(application.requested_domain.name, application.approved_domain.name) + # Test that approved domain exists and equals requested domain + self.assertEqual(application.requested_domain.name, application.approved_domain.name) def test_save_model_sets_restricted_status_on_user(self): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # make sure 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) + # 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)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): # Modify the application's property application.status = DomainApplication.ApplicationStatus.INELIGIBLE # Use the model admin's save_model method self.admin.save_model(request, application, form=None, change=True) - # Test that approved domain exists and equals requested domain - self.assertEqual(application.creator.status, "restricted") + # Test that approved domain exists and equals requested domain + self.assertEqual(application.creator.status, "restricted") def test_readonly_when_restricted_creator(self): - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with less_console_noise(): + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): application.creator.status = User.RESTRICTED application.creator.save() - request = self.factory.get("/") - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request, application) - - expected_fields = [ - "id", - "created_at", - "updated_at", - "status", - "creator", - "investigator", - "organization_type", - "federally_recognized_tribe", - "state_recognized_tribe", - "tribe_name", - "federal_agency", - "federal_type", - "is_election_board", - "organization_name", - "address_line1", - "address_line2", - "city", - "state_territory", - "zipcode", - "urbanization", - "about_your_organization", - "authorizing_official", - "approved_domain", - "requested_domain", - "submitter", - "purpose", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - "submission_date", - "notes", - "current_websites", - "other_contacts", - "alternative_domains", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_analyst(self): - request = self.factory.get("/") # Use the correct method and path - request.user = self.staffuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [ - "creator", - "about_your_organization", - "requested_domain", - "approved_domain", - "alternative_domains", - "purpose", - "submitter", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_superuser(self): - request = self.factory.get("/") # Use the correct method and path - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [] - - self.assertEqual(readonly_fields, expected_fields) - - def test_saving_when_restricted_creator(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.creator.status = User.RESTRICTED - application.creator.save() - - # Create a request object with a superuser - request = self.factory.get("/") - request.user = self.superuser - - with patch("django.contrib.messages.error") as mock_error: - # Simulate saving the model - self.admin.save_model(request, application, None, False) - - # Assert that the error message was called with the correct argument - mock_error.assert_called_once_with( - request, - "This action is not permitted for applications with a restricted creator.", - ) - - # Assert that the status has not changed - self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) - - def test_change_view_with_restricted_creator(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.creator.status = User.RESTRICTED - application.creator.save() - - with patch("django.contrib.messages.warning") as mock_warning: - # Create a request object with a superuser - request = self.factory.get("/admin/your_app/domainapplication/{}/change/".format(application.pk)) + request = self.factory.get("/") request.user = self.superuser - self.admin.display_restricted_warning(request, application) + readonly_fields = self.admin.get_readonly_fields(request, application) - # Assert that the error message was called with the correct argument - mock_warning.assert_called_once_with( - request, - "Cannot edit an application with a restricted creator.", - ) + expected_fields = [ + "id", + "created_at", + "updated_at", + "status", + "creator", + "investigator", + "organization_type", + "federally_recognized_tribe", + "state_recognized_tribe", + "tribe_name", + "federal_agency", + "federal_type", + "is_election_board", + "organization_name", + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + "urbanization", + "about_your_organization", + "authorizing_official", + "approved_domain", + "requested_domain", + "submitter", + "purpose", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + "submission_date", + "notes", + "current_websites", + "other_contacts", + "alternative_domains", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_analyst(self): + with less_console_noise(): + request = self.factory.get("/") # Use the correct method and path + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "creator", + "about_your_organization", + "requested_domain", + "approved_domain", + "alternative_domains", + "purpose", + "submitter", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_superuser(self): + with less_console_noise(): + request = self.factory.get("/") # Use the correct method and path + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [] + + self.assertEqual(readonly_fields, expected_fields) + + def test_saving_when_restricted_creator(self): + with less_console_noise(): + # Create an instance of the model + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.creator.status = User.RESTRICTED + application.creator.save() + + # Create a request object with a superuser + request = self.factory.get("/") + request.user = self.superuser + + with patch("django.contrib.messages.error") as mock_error: + # Simulate saving the model + self.admin.save_model(request, application, None, False) + + # Assert that the error message was called with the correct argument + mock_error.assert_called_once_with( + request, + "This action is not permitted for applications with a restricted creator.", + ) + + # Assert that the status has not changed + self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) + + def test_change_view_with_restricted_creator(self): + with less_console_noise(): + # Create an instance of the model + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.creator.status = User.RESTRICTED + application.creator.save() + + with patch("django.contrib.messages.warning") as mock_warning: + # Create a request object with a superuser + request = self.factory.get("/admin/your_app/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser + + self.admin.display_restricted_warning(request, application) + + # Assert that the error message was called with the correct argument + mock_warning.assert_called_once_with( + request, + "Cannot edit an application with a restricted creator.", + ) 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, @@ -751,49 +765,50 @@ class TestDomainApplicationAdmin(MockEppLib): Used to test errors when saving a change with an active domain, also used to test side effects when saving a change goes through.""" - # 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() + with less_console_noise(): + # 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 + # 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 domain_is_active # Override to return True + # Define a custom implementation for is_active + def custom_is_active(self): + 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")) + # 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")) - application.status = another_state - self.admin.save_model(request, application, None, True) + application.status = another_state + self.admin.save_model(request, application, None, True) - # 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 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() - self.assertEqual(application.approved_domain, None) + self.assertEqual(application.approved_domain, None) - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.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() + # 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) @@ -826,14 +841,15 @@ class TestDomainApplicationAdmin(MockEppLib): It retrieves the current list of filters from DomainApplicationAdmin and checks that it matches the expected list of filters. """ - request = self.factory.get("/") - request.user = self.superuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.superuser - # Grab the current list of table filters - readonly_fields = self.admin.get_list_filter(request) - expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) + # Grab the current list of table filters + readonly_fields = self.admin.get_list_filter(request) + expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_table_sorted_alphabetically(self): """ @@ -845,23 +861,24 @@ class TestDomainApplicationAdmin(MockEppLib): that it matches the expected queryset, which is sorted alphabetically by the 'requested_domain__name' field. """ - # Creates a list of DomainApplications in scrambled order - multiple_unalphabetical_domain_objects("application") + with less_console_noise(): + # Creates a list of DomainApplications in scrambled order + multiple_unalphabetical_domain_objects("application") - request = self.factory.get("/") - request.user = self.superuser + request = self.factory.get("/") + request.user = self.superuser - # Get the expected list of alphabetically sorted DomainApplications - expected_order = DomainApplication.objects.order_by("requested_domain__name") + # Get the expected list of alphabetically sorted DomainApplications + expected_order = DomainApplication.objects.order_by("requested_domain__name") - # Get the returned queryset - queryset = self.admin.get_queryset(request) + # Get the returned queryset + queryset = self.admin.get_queryset(request) - # Check the order - self.assertEqual( - list(queryset), - list(expected_order), - ) + # Check the order + self.assertEqual( + list(queryset), + list(expected_order), + ) def test_displays_investigator_filter(self): """ @@ -877,37 +894,38 @@ class TestDomainApplicationAdmin(MockEppLib): the filter displays correctly, when the filter isn't filtering correctly. """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainapplication/", - { - "investigator__id__exact": investigator_user.id, - }, - follow=True, - ) - - # Then, test if the filter actually exists - self.assertIn("filters", response.context) - - # Assert the content of filters and search_query - filters = response.context["filters"] - - self.assertEqual( - filters, - [ + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domainapplication/", { - "parameter_name": "investigator", - "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", + "investigator__id__exact": investigator_user.id, }, - ], - ) + follow=True, + ) + + # Then, test if the filter actually exists + self.assertIn("filters", response.context) + + # Assert the content of filters and search_query + filters = response.context["filters"] + + self.assertEqual( + filters, + [ + { + "parameter_name": "investigator", + "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", + }, + ], + ) def test_investigator_dropdown_displays_only_staff(self): """ @@ -920,68 +938,70 @@ class TestDomainApplicationAdmin(MockEppLib): It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin and checks that it matches the expected queryset, which only includes staff users. """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - # Create a mock DomainApplication object, with a user that is not staff - application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy") - investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() - investigator_user_2.is_staff = False - investigator_user_2.save() + # Create a mock DomainApplication object, with a user that is not staff + application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy") + investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() + investigator_user_2.is_staff = False + investigator_user_2.save() - p = "userpass" - self.client.login(username="staffuser", password=p) + p = "userpass" + self.client.login(username="staffuser", password=p) - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Get the actual field from the model's meta information - investigator_field = DomainApplication._meta.get_field("investigator") + # Get the actual field from the model's meta information + investigator_field = DomainApplication._meta.get_field("investigator") - # We should only be displaying staff users, in alphabetical order - expected_dropdown = list(User.objects.filter(is_staff=True)) - current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset) + # We should only be displaying staff users, in alphabetical order + expected_dropdown = list(User.objects.filter(is_staff=True)) + current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset) - self.assertEqual(expected_dropdown, current_dropdown) + self.assertEqual(expected_dropdown, current_dropdown) - # Non staff users should not be in the list - self.assertNotIn(application_2, current_dropdown) + # Non staff users should not be in the list + self.assertNotIn(application_2, current_dropdown) def test_investigator_list_is_alphabetically_sorted(self): """ This test verifies that filter list for the 'investigator' is displayed alphabetically """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - application_2: DomainApplication = generic_domain_object("application", "AGuy") - investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() - investigator_user_2.first_name = "AGuy" - investigator_user_2.is_staff = True - investigator_user_2.save() + application_2: DomainApplication = generic_domain_object("application", "AGuy") + investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() + investigator_user_2.first_name = "AGuy" + investigator_user_2.is_staff = True + investigator_user_2.save() - application_3: DomainApplication = generic_domain_object("application", "FinalGuy") - investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get() - investigator_user_3.first_name = "FinalGuy" - investigator_user_3.is_staff = True - investigator_user_3.save() + application_3: DomainApplication = generic_domain_object("application", "FinalGuy") + investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get() + investigator_user_3.first_name = "FinalGuy" + investigator_user_3.is_staff = True + investigator_user_3.save() - p = "userpass" - self.client.login(username="staffuser", password=p) - request = RequestFactory().get("/") + p = "userpass" + self.client.login(username="staffuser", password=p) + request = RequestFactory().get("/") - expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")) + expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")) - # Get the actual sorted list of investigators from the lookups method - actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] + # Get the actual sorted list of investigators from the lookups method + actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] - self.assertEqual(expected_list, actual_list) + self.assertEqual(expected_list, actual_list) def tearDown(self): super().tearDown() @@ -1010,28 +1030,29 @@ class DomainInvitationAdminTest(TestCase): def test_get_filters(self): """Ensures that our filters are displaying correctly""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domaininvitation/", - {}, - follow=True, - ) + response = self.client.get( + "/admin/registrar/domaininvitation/", + {}, + follow=True, + ) - # Assert that the filters are added - self.assertContains(response, "invited", count=2) - self.assertContains(response, "Invited", count=2) - self.assertContains(response, "retrieved", count=2) - self.assertContains(response, "Retrieved", count=2) + # Assert that the filters are added + self.assertContains(response, "invited", count=2) + self.assertContains(response, "Invited", count=2) + self.assertContains(response, "retrieved", count=2) + self.assertContains(response, "Retrieved", count=2) - # Check for the HTML context specificially - invited_html = 'Invited' - retrieved_html = 'Retrieved' + # Check for the HTML context specificially + invited_html = 'Invited' + retrieved_html = 'Retrieved' - self.assertContains(response, invited_html, count=1) - self.assertContains(response, retrieved_html, count=1) + self.assertContains(response, invited_html, count=1) + self.assertContains(response, retrieved_html, count=1) class TestDomainInformationAdmin(TestCase): @@ -1088,49 +1109,52 @@ class TestDomainInformationAdmin(TestCase): def test_readonly_fields_for_analyst(self): """Ensures that analysts have their permissions setup correctly""" - request = self.factory.get("/") - request.user = self.staffuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.staffuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [ - "creator", - "type_of_work", - "more_organization_information", - "domain", - "domain_application", - "submitter", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - ] + expected_fields = [ + "creator", + "type_of_work", + "more_organization_information", + "domain", + "domain_application", + "submitter", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + ] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_domain_sortable(self): """Tests if DomainInformation sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-domain__name",)) def test_submitter_sortable(self): """Tests if DomainInformation sorts by submitter correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "4", - ("submitter__first_name", "submitter__last_name"), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "4", + ("submitter__first_name", "submitter__last_name"), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name")) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name")) class UserDomainRoleAdminTest(TestCase): @@ -1157,109 +1181,113 @@ class UserDomainRoleAdminTest(TestCase): def test_domain_sortable(self): """Tests if the UserDomainrole sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" + ) - # Create a list of UserDomainRoles that are in random order - mocks_to_create = ["jkl.gov", "ghi.gov", "abc.gov", "def.gov"] - for name in mocks_to_create: - fake_domain = Domain.objects.create(name=name) - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Create a list of UserDomainRoles that are in random order + mocks_to_create = ["jkl.gov", "ghi.gov", "abc.gov", "def.gov"] + for name in mocks_to_create: + fake_domain = Domain.objects.create(name=name) + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("2", ("domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("2", ("domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-2", ("-domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-2", ("-domain__name",)) def test_user_sortable(self): """Tests if the UserDomainrole sorts by user correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - mock_data_generator = AuditedAdminMockData() + mock_data_generator = AuditedAdminMockData() - fake_domain = Domain.objects.create(name="igorville.gov") - # Create a list of UserDomainRoles that are in random order - mocks_to_create = ["jkl", "ghi", "abc", "def"] - for name in mocks_to_create: - # Creates a fake "User" object - fake_user = mock_data_generator.dummy_user(name, "user") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + fake_domain = Domain.objects.create(name="igorville.gov") + # Create a list of UserDomainRoles that are in random order + mocks_to_create = ["jkl", "ghi", "abc", "def"] + for name in mocks_to_create: + # Creates a fake "User" object + fake_user = mock_data_generator.dummy_user(name, "user") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("user__first_name", "user__last_name")) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("user__first_name", "user__last_name")) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-user__first_name", "-user__last_name")) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-user__first_name", "-user__last_name")) def test_email_not_in_search(self): """Tests the search bar in Django Admin for UserDomainRoleAdmin. Should return no results for an invalid email.""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" - ) - fake_domain = Domain.objects.create(name="test123") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Make the request using the Client class - # which handles CSRF - # Follow=True handles the redirect - response = self.client.get( - "/admin/registrar/userdomainrole/", - { - "q": "testmail@igorville.com", - }, - follow=True, - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" + ) + fake_domain = Domain.objects.create(name="test123") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Make the request using the Client class + # which handles CSRF + # Follow=True handles the redirect + response = self.client.get( + "/admin/registrar/userdomainrole/", + { + "q": "testmail@igorville.com", + }, + follow=True, + ) - # Assert that the query is added to the extra_context - self.assertIn("search_query", response.context) - # Assert the content of filters and search_query - search_query = response.context["search_query"] - self.assertEqual(search_query, "testmail@igorville.com") + # Assert that the query is added to the extra_context + self.assertIn("search_query", response.context) + # Assert the content of filters and search_query + search_query = response.context["search_query"] + self.assertEqual(search_query, "testmail@igorville.com") - # We only need to check for the end of the HTML string - self.assertNotContains(response, "Stewart Jones AntarcticPolarBears@example.com") + # We only need to check for the end of the HTML string + self.assertNotContains(response, "Stewart Jones AntarcticPolarBears@example.com") def test_email_in_search(self): """Tests the search bar in Django Admin for UserDomainRoleAdmin. Should return results for an valid email.""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Joe", last_name="Jones", email="AntarcticPolarBears@example.com" - ) - fake_domain = Domain.objects.create(name="fake") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Make the request using the Client class - # which handles CSRF - # Follow=True handles the redirect - response = self.client.get( - "/admin/registrar/userdomainrole/", - { - "q": "AntarcticPolarBears@example.com", - }, - follow=True, - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Joe", last_name="Jones", email="AntarcticPolarBears@example.com" + ) + fake_domain = Domain.objects.create(name="fake") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Make the request using the Client class + # which handles CSRF + # Follow=True handles the redirect + response = self.client.get( + "/admin/registrar/userdomainrole/", + { + "q": "AntarcticPolarBears@example.com", + }, + follow=True, + ) - # Assert that the query is added to the extra_context - self.assertIn("search_query", response.context) + # Assert that the query is added to the extra_context + self.assertIn("search_query", response.context) - search_query = response.context["search_query"] - self.assertEqual(search_query, "AntarcticPolarBears@example.com") + search_query = response.context["search_query"] + self.assertEqual(search_query, "AntarcticPolarBears@example.com") - # We only need to check for the end of the HTML string - self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com", count=1) + # We only need to check for the end of the HTML string + self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com", count=1) class ListHeaderAdminTest(TestCase): @@ -1341,39 +1369,42 @@ class MyUserAdminTest(TestCase): self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site) def test_list_display_without_username(self): - request = self.client.request().wsgi_request - request.user = create_user() + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_user() - list_display = self.admin.get_list_display(request) - expected_list_display = [ - "email", - "first_name", - "last_name", - "group", - "status", - ] + list_display = self.admin.get_list_display(request) + expected_list_display = [ + "email", + "first_name", + "last_name", + "group", + "status", + ] - self.assertEqual(list_display, expected_list_display) - self.assertNotIn("username", list_display) + self.assertEqual(list_display, expected_list_display) + self.assertNotIn("username", list_display) def test_get_fieldsets_superuser(self): - request = self.client.request().wsgi_request - request.user = create_superuser() - fieldsets = self.admin.get_fieldsets(request) - expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request) - self.assertEqual(fieldsets, expected_fieldsets) + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_superuser() + fieldsets = self.admin.get_fieldsets(request) + expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request) + self.assertEqual(fieldsets, expected_fieldsets) def test_get_fieldsets_cisa_analyst(self): - request = self.client.request().wsgi_request - request.user = create_user() - fieldsets = self.admin.get_fieldsets(request) - expected_fieldsets = ( - (None, {"fields": ("password", "status")}), - ("Personal Info", {"fields": ("first_name", "last_name", "email")}), - ("Permissions", {"fields": ("is_active", "groups")}), - ("Important dates", {"fields": ("last_login", "date_joined")}), - ) - self.assertEqual(fieldsets, expected_fieldsets) + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_user() + fieldsets = self.admin.get_fieldsets(request) + expected_fieldsets = ( + (None, {"fields": ("password", "status")}), + ("Personal Info", {"fields": ("first_name", "last_name", "email")}), + ("Permissions", {"fields": ("is_active", "groups")}), + ("Important dates", {"fields": ("last_login", "date_joined")}), + ) + self.assertEqual(fieldsets, expected_fieldsets) def tearDown(self): User.objects.all().delete() @@ -1386,172 +1417,176 @@ class AuditedAdminTest(TestCase): self.client = Client(HTTP_HOST="localhost:8080") def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names): - formatted_sort_fields = [] - for obj in obj_names: - formatted_sort_fields.append("{}__{}".format(field_name, obj)) + with less_console_noise(): + formatted_sort_fields = [] + for obj in obj_names: + formatted_sort_fields.append("{}__{}".format(field_name, obj)) - ordered_list = list( - obj_to_sort.get_queryset(request).order_by(*formatted_sort_fields).values_list(*formatted_sort_fields) - ) + ordered_list = list( + obj_to_sort.get_queryset(request).order_by(*formatted_sort_fields).values_list(*formatted_sort_fields) + ) - return ordered_list + return ordered_list def test_alphabetically_sorted_fk_fields_domain_application(self): - tested_fields = [ - DomainApplication.authorizing_official.field, - DomainApplication.submitter.field, - DomainApplication.investigator.field, - DomainApplication.creator.field, - DomainApplication.requested_domain.field, - ] + with less_console_noise(): + tested_fields = [ + DomainApplication.authorizing_official.field, + DomainApplication.submitter.field, + DomainApplication.investigator.field, + DomainApplication.creator.field, + DomainApplication.requested_domain.field, + ] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("application") + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("application") - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)) - model_admin = AuditedAdmin(DomainApplication, self.site) + model_admin = AuditedAdmin(DomainApplication, self.site) - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - isNamefield: bool = field == DomainApplication.requested_domain.field - if isNamefield: - sorted_fields = ["name"] - else: - sorted_fields = ["first_name", "last_name"] - # We want both of these to be lists, as it is richer test wise. - - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - if not isNamefield: - first = contact.first_name - last = contact.last_name + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + isNamefield: bool = field == DomainApplication.requested_domain.field + if isNamefield: + sorted_fields = ["name"] else: + sorted_fields = ["first_name", "last_name"] + # We want both of these to be lists, as it is richer test wise. + + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: + if not isNamefield: + first = contact.first_name + last = contact.last_name + else: + first = contact.name + last = None + + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name), + ) + + def test_alphabetically_sorted_fk_fields_domain_information(self): + with less_console_noise(): + tested_fields = [ + DomainInformation.authorizing_official.field, + DomainInformation.submitter.field, + # DomainInformation.creator.field, + (DomainInformation.domain.field, ["name"]), + (DomainInformation.domain_application.field, ["requested_domain__name"]), + ] + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("information") + + # Create a mock request + request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) + + model_admin = AuditedAdmin(DomainInformation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + isOtherOrderfield: bool = isinstance(field, tuple) + field_obj = None + if isOtherOrderfield: + sorted_fields = field[1] + field_obj = field[0] + else: + sorted_fields = ["first_name", "last_name"] + field_obj = field + # We want both of these to be lists, as it is richer test wise. + desired_order = self.order_by_desired_field_helper(model_admin, request, field_obj.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field_obj, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for obj in current_sort_order: + last = None + if not isOtherOrderfield: + first = obj.first_name + last = obj.last_name + elif field_obj == DomainInformation.domain.field: + first = obj.name + elif field_obj == DomainInformation.domain_application.field: + first = obj.requested_domain.name + + name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field_obj.name), + ) + + def test_alphabetically_sorted_fk_fields_domain_invitation(self): + with less_console_noise(): + tested_fields = [DomainInvitation.domain.field] + + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("invitation") + + # Create a mock request + request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) + + model_admin = AuditedAdmin(DomainInvitation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + sorted_fields = ["name"] + # We want both of these to be lists, as it is richer test wise. + + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: first = contact.name last = None - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_information(self): - tested_fields = [ - DomainInformation.authorizing_official.field, - DomainInformation.submitter.field, - # DomainInformation.creator.field, - (DomainInformation.domain.field, ["name"]), - (DomainInformation.domain_application.field, ["requested_domain__name"]), - ] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("information") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInformation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - isOtherOrderfield: bool = isinstance(field, tuple) - field_obj = None - if isOtherOrderfield: - sorted_fields = field[1] - field_obj = field[0] - else: - sorted_fields = ["first_name", "last_name"] - field_obj = field - # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper(model_admin, request, field_obj.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field_obj, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for obj in current_sort_order: - last = None - if not isOtherOrderfield: - first = obj.first_name - last = obj.last_name - elif field_obj == DomainInformation.domain.field: - first = obj.name - elif field_obj == DomainInformation.domain_application.field: - first = obj.requested_domain.name - - name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field_obj.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_invitation(self): - tested_fields = [DomainInvitation.domain.field] - - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("invitation") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInvitation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - sorted_fields = ["name"] - # We want both of these to be lists, as it is richer test wise. - - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - first = contact.name - last = None - - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name), + ) def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): """Handles edge cases for test cases""" @@ -1585,93 +1620,98 @@ class DomainSessionVariableTest(TestCase): def test_session_vars_set_correctly(self): """Checks if session variables are being set correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information = generic_domain_object("information", "session") - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual( - request.session["analyst_action_location"], - dummy_domain_information.domain.pk, - ) + dummy_domain_information = generic_domain_object("information", "session") + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + self.populate_session_values(request, dummy_domain_information.domain) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual( + request.session["analyst_action_location"], + dummy_domain_information.domain.pk, + ) def test_session_vars_set_correctly_hardcoded_domain(self): """Checks if session variables are being set correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information: Domain = generic_domain_object("information", "session") - dummy_domain_information.domain.pk = 1 + dummy_domain_information: Domain = generic_domain_object("information", "session") + dummy_domain_information.domain.pk = 1 - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual(request.session["analyst_action_location"], 1) + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + self.populate_session_values(request, dummy_domain_information.domain) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual(request.session["analyst_action_location"], 1) def test_session_variables_reset_correctly(self): """Checks if incorrect session variables get overridden""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information = generic_domain_object("information", "session") - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + dummy_domain_information = generic_domain_object("information", "session") + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain, preload_bad_data=True) + self.populate_session_values(request, dummy_domain_information.domain, preload_bad_data=True) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual( - request.session["analyst_action_location"], - dummy_domain_information.domain.pk, - ) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual( + request.session["analyst_action_location"], + dummy_domain_information.domain.pk, + ) def test_session_variables_retain_information(self): """Checks to see if session variables retain old information""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information_list = multiple_unalphabetical_domain_objects("information") - for item in dummy_domain_information_list: - request = self.get_factory_post_edit_domain(item.domain.pk) - self.populate_session_values(request, item.domain) + dummy_domain_information_list = multiple_unalphabetical_domain_objects("information") + for item in dummy_domain_information_list: + request = self.get_factory_post_edit_domain(item.domain.pk) + self.populate_session_values(request, item.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual(request.session["analyst_action_location"], item.domain.pk) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual(request.session["analyst_action_location"], item.domain.pk) def test_session_variables_concurrent_requests(self): """Simulates two requests at once""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - info_first = generic_domain_object("information", "session") - info_second = generic_domain_object("information", "session2") + info_first = generic_domain_object("information", "session") + info_second = generic_domain_object("information", "session2") - request_first = self.get_factory_post_edit_domain(info_first.domain.pk) - request_second = self.get_factory_post_edit_domain(info_second.domain.pk) + request_first = self.get_factory_post_edit_domain(info_first.domain.pk) + request_second = self.get_factory_post_edit_domain(info_second.domain.pk) - self.populate_session_values(request_first, info_first.domain, True) - self.populate_session_values(request_second, info_second.domain, True) + self.populate_session_values(request_first, info_first.domain, True) + self.populate_session_values(request_second, info_second.domain, True) - # Check if anything got nulled out - self.assertNotEqual(request_first.session["analyst_action"], None) - self.assertNotEqual(request_second.session["analyst_action"], None) - self.assertNotEqual(request_first.session["analyst_action_location"], None) - self.assertNotEqual(request_second.session["analyst_action_location"], None) + # Check if anything got nulled out + self.assertNotEqual(request_first.session["analyst_action"], None) + self.assertNotEqual(request_second.session["analyst_action"], None) + self.assertNotEqual(request_first.session["analyst_action_location"], None) + self.assertNotEqual(request_second.session["analyst_action_location"], None) - # Check if they are both the same action 'type' - self.assertEqual(request_first.session["analyst_action"], "edit") - self.assertEqual(request_second.session["analyst_action"], "edit") + # Check if they are both the same action 'type' + self.assertEqual(request_first.session["analyst_action"], "edit") + self.assertEqual(request_second.session["analyst_action"], "edit") - # Check their locations, and ensure they aren't the same across both - self.assertNotEqual( - request_first.session["analyst_action_location"], - request_second.session["analyst_action_location"], - ) + # Check their locations, and ensure they aren't the same across both + self.assertNotEqual( + request_first.session["analyst_action_location"], + request_second.session["analyst_action_location"], + ) def populate_session_values(self, request, domain_object, preload_bad_data=False): """Boilerplate for creating mock sessions""" @@ -1704,63 +1744,65 @@ class ContactAdminTest(TestCase): self.staffuser = create_user() def test_readonly_when_restricted_staffuser(self): - request = self.factory.get("/") - request.user = self.staffuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.staffuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [ - "user", - ] + expected_fields = [ + "user", + ] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_readonly_when_restricted_superuser(self): - request = self.factory.get("/") - request.user = self.superuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.superuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [] + expected_fields = [] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_change_view_for_joined_contact_five_or_less(self): """Create a contact, join it to 4 domain requests. The 5th join will be a user. Assert that the warning on the contact form lists 5 joins.""" + with less_console_noise(): + self.client.force_login(self.superuser) - self.client.force_login(self.superuser) + # Create an instance of the model + contact, _ = Contact.objects.get_or_create(user=self.staffuser) - # Create an instance of the model - contact, _ = Contact.objects.get_or_create(user=self.staffuser) + # join it to 4 domain requests. The 5th join will be a user. + application1 = completed_application(submitter=contact, name="city1.gov") + application2 = completed_application(submitter=contact, name="city2.gov") + application3 = completed_application(submitter=contact, name="city3.gov") + application4 = completed_application(submitter=contact, name="city4.gov") - # join it to 4 domain requests. The 5th join will be a user. - application1 = completed_application(submitter=contact, name="city1.gov") - application2 = completed_application(submitter=contact, name="city2.gov") - application3 = completed_application(submitter=contact, name="city3.gov") - application4 = completed_application(submitter=contact, name="city4.gov") + with patch("django.contrib.messages.warning") as mock_warning: + # Use the test client to simulate the request + response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) - with patch("django.contrib.messages.warning") as mock_warning: - # Use the test client to simulate the request - response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) - - # Assert that the error message was called with the correct argument - # Note: The 5th join will be a user. - mock_warning.assert_called_once_with( - response.wsgi_request, - "", - ) + # Assert that the error message was called with the correct argument + # Note: The 5th join will be a user. + mock_warning.assert_called_once_with( + response.wsgi_request, + "", + ) def test_change_view_for_joined_contact_five_or_more(self): """Create a contact, join it to 5 domain requests. The 6th join will be a user. @@ -1810,20 +1852,21 @@ class VerifiedByStaffAdminTestCase(TestCase): self.factory = RequestFactory() def test_save_model_sets_user_field(self): - self.client.force_login(self.superuser) + with less_console_noise(): + self.client.force_login(self.superuser) - # Create an instance of the admin class - admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None) + # Create an instance of the admin class + admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None) - # Create a VerifiedByStaff instance - vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") + # Create a VerifiedByStaff instance + vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") - # Create a request object - request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/") - request.user = self.superuser + # Create a request object + request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/") + request.user = self.superuser - # Call the save_model method - admin_instance.save_model(request, vip_instance, None, None) + # Call the save_model method + admin_instance.save_model(request, vip_instance, None, None) - # Check that the user field is set to the request.user - self.assertEqual(vip_instance.requestor, self.superuser) + # Check that the user field is set to the request.user + self.assertEqual(vip_instance.requestor, self.superuser) From 1530d77ad8716fa503c1506ca879713b72a7b932 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 14:37:19 -0500 Subject: [PATCH 05/29] add rejection reason to model --- ...0070_domainapplication_rejection_reason.py | 34 +++++++++++++++++++ src/registrar/models/domain_application.py | 16 +++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/registrar/migrations/0070_domainapplication_rejection_reason.py diff --git a/src/registrar/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py new file mode 100644 index 000000000..c49f4d442 --- /dev/null +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.7 on 2024-02-16 19:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0069_alter_contact_email_alter_contact_first_name_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="domainapplication", + name="rejection_reason", + field=models.TextField( + blank=True, + choices=[ + ("domain_purpose", "Domain purpose requirements not met"), + ("requestor", "Requestor isn't authorized to make the request"), + ( + "second_domain_reasoning", + "Organization already has a domain and does not provide sufficient reasoning for a second domain", + ), + ( + "contacts_or_organization_legitimacy", + "Research could not corroborate legitimacy of contacts or organization", + ), + ("organization_eligibility", "Organization isn't eligible for a .gov"), + ("naming_requirements", "naming requirements not met"), + ], + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 17bc71cbe..563c7699e 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -350,12 +350,27 @@ class DomainApplication(TimeStampedModel): ] AGENCY_CHOICES = [(v, v) for v in AGENCIES] + class RejectionReasons(models.TextChoices): + DOMAIN_PURPOSE = "domain_purpose", "Domain purpose requirements not met" + REQUESTOR = "requestor", "Requestor isn't authorized to make the request" + SECOND_DOMAIN_REASONING = "second_domain_reasoning", "Organization already has a domain and does not provide sufficient reasoning for a second domain" + CONTACTS_OR_ORGANIZATION_LEGITIMACY = "contacts_or_organization_legitimacy", "Research could not corroborate legitimacy of contacts or organization" + ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" + NAMING_REQUIREMENTS = "naming_requirements", "naming requirements not met" + # #### Internal fields about the application ##### status = FSMField( choices=ApplicationStatus.choices, # possible states as an array of constants default=ApplicationStatus.STARTED, # sensible default protected=False, # can change state directly, particularly in Django admin ) + + rejection_reason = models.TextField( + choices=RejectionReasons.choices, + null=True, + blank=True, + ) + # This is the application user who created this application. The contact # information that they gave is in the `submitter` field creator = models.ForeignKey( @@ -363,6 +378,7 @@ class DomainApplication(TimeStampedModel): on_delete=models.PROTECT, related_name="applications_created", ) + investigator = models.ForeignKey( "registrar.User", null=True, From c0fc1bc06f8df562da730f9674c60de420cbecdc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 15:16:27 -0500 Subject: [PATCH 06/29] updated text of reject email --- .../emails/status_change_rejected.txt | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index c5afaf711..5d6f01ee7 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -9,17 +9,61 @@ STATUS: Rejected ---------------------------------------------------------------- -YOU CAN SUBMIT A NEW REQUEST -If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request. +REJECTION REASON{% if application.rejection_reason == 'domain_purpose' %} +Your domain request was rejected because the purpose you provided did not meet our +requirements. You didn’t provide enough information about how you intend to use the +domain. Learn more about: - Eligibility for a .gov domain -- Choosing a .gov domain name +- What you can and can’t do with .gov domains + +If you have questions or comments, reply to this email. +{% elif application.status == 'requestor' %} +Your domain request was rejected because we don’t believe you’re eligible to request a +.gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be +working on behalf of a government organization, to request a .gov domain. -NEED ASSISTANCE? -If you have questions about this domain request or need help choosing a new domain name, reply to this email. +DEMONSTRATE ELIGIBILITY +If you can provide more information that demonstrates your eligibility, or you want to +discuss further, reply to this email. +{% elif application.status == 'second_domain_reasoning' %} +Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our +practice is to approve one domain per online service per government organization. We +evaluate additional requests on a case-by-case basis. You did not provide sufficient +justification for an additional domain. +Read more about our practice of approving one domain per online service +. + +If you have questions or comments, reply to this email. +{% elif application.status == 'contacts_or_organization_legitimacy' %} +Your domain request was rejected because we could not verify the organizational +contacts you provided. If you have questions or comments, reply to this email. +{% elif application.status == 'organization_eligibility' %} +Your domain request was rejected because we determined that {{ application.organization_name }} is not +eligible for a .gov domain. .Gov domains are only available to official U.S.-based +government organizations. + + +DEMONSTRATE ELIGIBILITY +If you can provide documentation that demonstrates your eligibility, reply to this email. +This can include links to (or copies of) your authorizing legislation, your founding +charter or bylaws, or other similar documentation. Without this, we can’t approve a +.gov domain for your organization. Learn more about eligibility for .gov domains +. +{% elif application.status == 'naming_requirements' %} +Your domain request was rejected because it does not meet our naming requirements. +Domains should uniquely identify a government organization and be clear to the +general public. Learn more about naming requirements for your type of organization +. + + +YOU CAN SUBMIT A NEW REQUEST +We encourage you to request a domain that meets our requirements. If you have +questions or want to discuss potential domain names, reply to this email. +{% endif %} THANK YOU .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. From ba53e74e26912c61e619d40cfd22f079b5e28f4b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 15:16:42 -0500 Subject: [PATCH 07/29] Add logic in admin to catch no reason error, add JS to manage UI --- src/registrar/admin.py | 20 +++++++++++++++- src/registrar/assets/js/get-gov-admin.js | 24 +++++++++++++++++++ ...0070_domainapplication_rejection_reason.py | 2 +- src/registrar/models/domain_application.py | 17 ++++++++++++- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c5f5be276..8225246b9 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -793,7 +793,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Detail view form = DomainApplicationAdminForm fieldsets = [ - (None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}), + (None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}), ( "Type of organization", { @@ -901,6 +901,24 @@ class DomainApplicationAdmin(ListHeaderAdmin): "This action is not permitted. The domain is already active.", ) + elif ( + obj + and obj.status == models.DomainApplication.ApplicationStatus.REJECTED + and not obj.rejection_reason + ): + # This condition should never be triggered. + # The opposite of this condition is acceptable (rejected -> other status and rejection_reason) + # because we clean up the rejection reason in the transition in the model. + + # Clear the success message + messages.set_level(request, messages.ERROR) + + messages.error( + request, + "A rejection reason is required.", + ) + + else: if obj.status != original_obj.status: status_method_mapping = { diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 866c7bd7d..b5f5a6aaa 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -312,3 +312,27 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, } })(); + +/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request + * status select amd to show/hide the rejection reason +*/ +(function (){ + + // Get the rejection reason form row + let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') + + if (rejectionReasonFormGroup) { + // Get the status select + let statusSelect = document.getElementById('id_status') + + // If status is rejected, hide the rejection reason on load + if (statusSelect.value != 'rejected') + rejectionReasonFormGroup.style.display = 'none'; + + // Listen to status changes and toggle rejection reason + statusSelect.addEventListener('change', function() { + rejectionReasonFormGroup.style.display = statusSelect.value !== 'rejected' ? 'none' : 'block'; + }); + } + +})(); diff --git a/src/registrar/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py index c49f4d442..defeb4d95 100644 --- a/src/registrar/migrations/0070_domainapplication_rejection_reason.py +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -26,7 +26,7 @@ class Migration(migrations.Migration): "Research could not corroborate legitimacy of contacts or organization", ), ("organization_eligibility", "Organization isn't eligible for a .gov"), - ("naming_requirements", "naming requirements not met"), + ("naming_requirements", "Naming requirements not met"), ], null=True, ), diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 563c7699e..3f6fcc99f 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -356,7 +356,7 @@ class DomainApplication(TimeStampedModel): SECOND_DOMAIN_REASONING = "second_domain_reasoning", "Organization already has a domain and does not provide sufficient reasoning for a second domain" CONTACTS_OR_ORGANIZATION_LEGITIMACY = "contacts_or_organization_legitimacy", "Research could not corroborate legitimacy of contacts or organization" ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" - NAMING_REQUIREMENTS = "naming_requirements", "naming requirements not met" + NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met" # #### Internal fields about the application ##### status = FSMField( @@ -694,12 +694,17 @@ class DomainApplication(TimeStampedModel): This action is logged. + This action cleans up the rejection status if moving away from rejected. + 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") + if self.status == self.ApplicationStatus.REJECTED: + self.rejection_reason = None + 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" @@ -721,12 +726,17 @@ class DomainApplication(TimeStampedModel): This action is logged. + This action cleans up the rejection status if moving away from rejected. + 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") + if self.status == self.ApplicationStatus.REJECTED: + self.rejection_reason = None + 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" @@ -745,6 +755,8 @@ class DomainApplication(TimeStampedModel): def approve(self, send_email=True): """Approve an application that has been submitted. + This action cleans up the rejection status if moving away from rejected. + This has substantial side-effects because it creates another database object for the approved Domain and makes the user who created the application into an admin on that domain. It also triggers an email @@ -767,6 +779,9 @@ class DomainApplication(TimeStampedModel): user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) + if self.status == self.ApplicationStatus.REJECTED: + self.rejection_reason = None + self._send_status_update_email( "application approved", "emails/status_change_approved.txt", From a23c095139db5afa60503a4189038cf01a55ea06 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 16:09:13 -0500 Subject: [PATCH 08/29] updates to election office --- src/registrar/admin.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index bac361c6a..d5a0f6bde 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -767,10 +767,10 @@ class DomainApplicationAdmin(ListHeaderAdmin): "requested_domain", "status", "organization_type", - "federal_agency", "federal_type", + "federal_agency", "organization_name", - "is_election_board", + "custom_election_board", "city", "state_territory", "created_at", @@ -784,6 +784,12 @@ class DomainApplicationAdmin(ListHeaderAdmin): ("investigator", ["first_name", "last_name"]), ] + def custom_election_board(self, obj): + return obj.is_election_board + + custom_election_board.admin_order_field = "is_election_board" # type: ignore + custom_election_board.short_description = "Election office" # type: ignore + # Filters list_filter = ("status", "organization_type", InvestigatorFilter) @@ -1044,8 +1050,8 @@ class DomainAdmin(ListHeaderAdmin): list_display = [ "name", "organization_type", - "federal_agency", "federal_type", + "federal_agency", "organization_name", "is_election_board", "city", @@ -1089,9 +1095,10 @@ class DomainAdmin(ListHeaderAdmin): organization_name.admin_order_field = "domain_info__organization_name" # type: ignore def is_election_board(self, obj): - return obj.domain_info.is_election_board if obj.domain_info else None + return obj.domain_info.is_election_board if obj.domain_info else False is_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore + is_election_board.short_description = "Election office" # type: ignore def city(self, obj): return obj.domain_info.city if obj.domain_info else None From a32b0c03731b6074fd7683d7bc811ed776309711 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 17:14:44 -0500 Subject: [PATCH 09/29] Admin unit tests --- .../emails/status_change_rejected.txt | 10 +- src/registrar/tests/test_admin.py | 154 ++++++++++++++++-- 2 files changed, 144 insertions(+), 20 deletions(-) diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index 5d6f01ee7..4beacf25a 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -19,7 +19,7 @@ Learn more about: - What you can and can’t do with .gov domains If you have questions or comments, reply to this email. -{% elif application.status == 'requestor' %} +{% elif application.rejection_reason == 'requestor' %} Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be working on behalf of a government organization, to request a .gov domain. @@ -28,7 +28,7 @@ working on behalf of a government organization, to request a .gov domain. DEMONSTRATE ELIGIBILITY If you can provide more information that demonstrates your eligibility, or you want to discuss further, reply to this email. -{% elif application.status == 'second_domain_reasoning' %} +{% elif application.rejection_reason == 'second_domain_reasoning' %} Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our practice is to approve one domain per online service per government organization. We evaluate additional requests on a case-by-case basis. You did not provide sufficient @@ -38,10 +38,10 @@ Read more about our practice of approving one domain per online service . If you have questions or comments, reply to this email. -{% elif application.status == 'contacts_or_organization_legitimacy' %} +{% elif application.rejection_reason == 'contacts_or_organization_legitimacy' %} Your domain request was rejected because we could not verify the organizational contacts you provided. If you have questions or comments, reply to this email. -{% elif application.status == 'organization_eligibility' %} +{% elif application.rejection_reason == 'organization_eligibility' %} Your domain request was rejected because we determined that {{ application.organization_name }} is not eligible for a .gov domain. .Gov domains are only available to official U.S.-based government organizations. @@ -53,7 +53,7 @@ This can include links to (or copies of) your authorizing legislation, your foun charter or bylaws, or other similar documentation. Without this, we can’t approve a .gov domain for your organization. Learn more about eligibility for .gov domains . -{% elif application.status == 'naming_requirements' %} +{% elif application.rejection_reason == 'naming_requirements' %} Your domain request was rejected because it does not meet our naming requirements. Domains should uniquely identify a government organization and be clear to the general public. Learn more about naming requirements for your type of organization diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f90b18584..6142c2aee 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -412,7 +412,7 @@ 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") - def transition_state_and_send_email(self, application, status): + def transition_state_and_send_email(self, application, status, rejection_reason=None): """Helper method for the email test cases.""" with boto3_mocking.clients.handler_for("sesv2", self.mock_client): @@ -420,12 +420,15 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a mock request request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Modify the application's property + # Modify the application's properties application.status = status + application.rejection_reason = rejection_reason # Use the model admin's save_model method self.admin.save_model(request, application, form=None, change=True) + logger.info(f'application.rejection_reason {application.rejection_reason}') + 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.""" @@ -512,7 +515,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) # Test Withdrawn Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.DOMAIN_PURPOSE) self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) @@ -520,9 +523,9 @@ class TestDomainApplicationAdmin(MockEppLib): self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - def test_save_model_sends_rejected_email(self): - """When transitioning to rejected on a domain request, - an email is sent out every time.""" + def test_save_model_sends_rejected_email_domain_purpose(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is domain purpose.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -531,19 +534,137 @@ class TestDomainApplicationAdmin(MockEppLib): # 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) + # Reject for reason DOMAIN_PURPOSE and test email + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.DOMAIN_PURPOSE) + self.assert_email_is_accurate("Your domain request was rejected because the purpose you provided did not meet our \nrequirements.", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Test Withdrawn Status + # Approve 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), 3) + def test_save_model_sends_rejected_email_requestor(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is requestor.""" + + # 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) + + # Reject for reason REQUESTOR and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR) + self.assert_email_is_accurate("Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain on behalf of Testorg", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + 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) + + def test_save_model_sends_rejected_email_requestor(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is second domain.""" + + # 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) + + # Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.SECOND_DOMAIN_REASONING) + self.assert_email_is_accurate("Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + 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) + + def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is contacts or org legitimacy.""" + + # 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) + + # Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY) + self.assert_email_is_accurate("Your domain request was rejected because we could not verify the organizational \ncontacts you provided. If you have questions or comments, reply to this email.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + 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) + + def test_save_model_sends_rejected_email_org_eligibility(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is org eligibility.""" + + # 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) + + # Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.ORGANIZATION_ELIGIBILITY) + self.assert_email_is_accurate("Your domain request was rejected because we determined that Testorg is not \neligible for a .gov domain.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + 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) + + def test_save_model_sends_rejected_email_naming(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is naming.""" + + # 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) + + # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.NAMING_REQUIREMENTS) + self.assert_email_is_accurate("Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + 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) + + def test_save_model_clear_rejected_reason(self): + """When transitioning from rejected on a domain request, + the rejected_reason is cleared.""" + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) + application.rejected_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE + application.save() + + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.approve() + + application.refresh_from_db() + self.assertEqual(application.rejected_reason, None) + def test_save_model_sends_withdrawn_email(self): """When transitioning to withdrawn on a domain request, @@ -633,6 +754,7 @@ class TestDomainApplicationAdmin(MockEppLib): "created_at", "updated_at", "status", + "rejection_reason", "creator", "investigator", "organization_type", @@ -744,7 +866,7 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) - def trigger_saving_approved_to_another_state(self, domain_is_active, another_state): + def trigger_saving_approved_to_another_state(self, domain_is_active, another_state, rejection_reason=None): """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. @@ -773,6 +895,8 @@ class TestDomainApplicationAdmin(MockEppLib): stack.enter_context(patch.object(messages, "error")) application.status = another_state + application.rejection_reason = rejection_reason + self.admin.save_model(request, application, None, True) # Assert that the error message was called with the correct argument @@ -814,7 +938,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED) def test_side_effects_when_saving_approved_to_rejected(self): - self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED) + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY) def test_side_effects_when_saving_approved_to_ineligible(self): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE) From f2e7278a86e4aa9e997effeeee83b3a5bd34b959 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 17:42:09 -0500 Subject: [PATCH 10/29] updated tests --- src/registrar/tests/test_admin.py | 17 ---------- src/registrar/tests/test_models.py | 50 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 6142c2aee..5f8ea8fc1 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -649,23 +649,6 @@ class TestDomainApplicationAdmin(MockEppLib): self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - def test_save_model_clear_rejected_reason(self): - """When transitioning from rejected on a domain request, - the rejected_reason is cleared.""" - - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) - application.rejected_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE - application.save() - - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.approve() - - application.refresh_from_db() - self.assertEqual(application.rejected_reason, None) - - def test_save_model_sends_withdrawn_email(self): """When transitioning to withdrawn on a domain request, an email is sent out every time.""" diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index cb7906d7a..f243956ff 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -574,6 +574,56 @@ class TestDomainApplication(TestCase): with self.assertRaises(TransitionNotAllowed): self.approved_application.reject_with_prejudice() + def test_approve_from_rejected_clears_rejection_reason(self): + """When transitioning from rejected to approved on a domain request, + the rejection_reason is cleared.""" + + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) + application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE + + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.approve() + + self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED) + self.assertEqual(application.rejection_reason, None) + + def test_in_review_from_rejected_clears_rejection_reason(self): + """When transitioning from rejected to in_review on a domain request, + the rejection_reason is cleared.""" + + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) + application.domain_is_not_active = True + application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE + + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.in_review() + + self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) + self.assertEqual(application.rejection_reason, None) + + def test_action_needed_from_rejected_clears_rejection_reason(self): + """When transitioning from rejected to action_needed on a domain request, + the rejection_reason is cleared.""" + + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) + application.domain_is_not_active = True + application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE + + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.action_needed() + + self.assertEqual(application.status, DomainApplication.ApplicationStatus.ACTION_NEEDED) + self.assertEqual(application.rejection_reason, None) + def test_has_rationale_returns_true(self): """has_rationale() returns true when an application has no_other_contacts_rationale""" with less_console_noise(): From afd1de0430c24a257d6e3d9f869759af8a1a142e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 17:54:32 -0500 Subject: [PATCH 11/29] test the error alert --- src/registrar/tests/test_admin.py | 54 +++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 6142c2aee..484898be4 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -427,8 +427,6 @@ class TestDomainApplicationAdmin(MockEppLib): # Use the model admin's save_model method self.admin.save_model(request, application, form=None, change=True) - logger.info(f'application.rejection_reason {application.rejection_reason}') - 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.""" @@ -649,6 +647,58 @@ class TestDomainApplicationAdmin(MockEppLib): self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self): + """ + When transitioning to rejected without a rejection reason, admin throws a user friendly message. + + The transition fails. + """ + + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser + + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + application.status = DomainApplication.ApplicationStatus.REJECTED + + self.admin.save_model(request, application, None, True) + + messages.error.assert_called_once_with( + request, + "A rejection reason is required.", + ) + + application.refresh_from_db() + self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED) + + def test_transition_to_rejected_with_rejection_reason_does_not_trigger_error(self): + """ + When transitioning to rejected with a rejection reason, admin does not throw an error alert. + + The transition is successful. + """ + + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser + + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + application.status = DomainApplication.ApplicationStatus.REJECTED + application.rejection_reason = DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY + + self.admin.save_model(request, application, None, True) + + messages.error.assert_not_called() + + application.refresh_from_db() + self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED) + def test_save_model_clear_rejected_reason(self): """When transitioning from rejected on a domain request, the rejected_reason is cleared.""" From d56982ac26736815662d834707e369a9165b9798 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 18:18:03 -0500 Subject: [PATCH 12/29] cleaned up noise in tests --- src/registrar/admin.py | 1 - src/registrar/models/domain_application.py | 10 +- src/registrar/tests/test_admin.py | 2042 ++++++++++---------- 3 files changed, 1073 insertions(+), 980 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8225246b9..c75588a27 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -917,7 +917,6 @@ class DomainApplicationAdmin(ListHeaderAdmin): request, "A rejection reason is required.", ) - else: if obj.status != original_obj.status: diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 3f6fcc99f..ca6a996ce 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -353,8 +353,14 @@ class DomainApplication(TimeStampedModel): class RejectionReasons(models.TextChoices): DOMAIN_PURPOSE = "domain_purpose", "Domain purpose requirements not met" REQUESTOR = "requestor", "Requestor isn't authorized to make the request" - SECOND_DOMAIN_REASONING = "second_domain_reasoning", "Organization already has a domain and does not provide sufficient reasoning for a second domain" - CONTACTS_OR_ORGANIZATION_LEGITIMACY = "contacts_or_organization_legitimacy", "Research could not corroborate legitimacy of contacts or organization" + SECOND_DOMAIN_REASONING = ( + "second_domain_reasoning", + "Organization already has a domain and does not provide sufficient reasoning for a second domain", + ) + CONTACTS_OR_ORGANIZATION_LEGITIMACY = ( + "contacts_or_organization_legitimacy", + "Research could not corroborate legitimacy of contacts or organization", + ) ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met" diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 484898be4..8a7c1c0a4 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -270,40 +270,43 @@ class TestDomainApplicationAdminForm(TestCase): self.application = completed_application() def test_form_choices(self): - # Create a form instance with the test application - form = DomainApplicationAdminForm(instance=self.application) + with less_console_noise(): + # Create a form instance with the test application + form = DomainApplicationAdminForm(instance=self.application) - # Verify that the form choices match the available transitions for started - expected_choices = [("started", "Started"), ("submitted", "Submitted")] - self.assertEqual(form.fields["status"].widget.choices, expected_choices) + # Verify that the form choices match the available transitions for started + expected_choices = [("started", "Started"), ("submitted", "Submitted")] + self.assertEqual(form.fields["status"].widget.choices, expected_choices) def test_form_choices_when_no_instance(self): - # Create a form instance without an instance - form = DomainApplicationAdminForm() + with less_console_noise(): + # Create a form instance without an instance + form = DomainApplicationAdminForm() - # Verify that the form choices show all choices when no instance is provided; - # this is necessary to show all choices when creating a new domain - # application in django admin; - # note that FSM ensures that no domain application exists with invalid status, - # so don't need to test for invalid status - self.assertEqual( - form.fields["status"].widget.choices, - DomainApplication._meta.get_field("status").choices, - ) + # Verify that the form choices show all choices when no instance is provided; + # this is necessary to show all choices when creating a new domain + # application in django admin; + # note that FSM ensures that no domain application exists with invalid status, + # so don't need to test for invalid status + self.assertEqual( + form.fields["status"].widget.choices, + DomainApplication._meta.get_field("status").choices, + ) def test_form_choices_when_ineligible(self): - # Create a form instance with a domain application with ineligible status - ineligible_application = DomainApplication(status="ineligible") + with less_console_noise(): + # Create a form instance with a domain application with ineligible status + ineligible_application = DomainApplication(status="ineligible") - # Attempt to create a form with the ineligible application - # The form should not raise an error, but choices should be the - # full list of possible choices - form = DomainApplicationAdminForm(instance=ineligible_application) + # Attempt to create a form with the ineligible application + # The form should not raise an error, but choices should be the + # full list of possible choices + form = DomainApplicationAdminForm(instance=ineligible_application) - self.assertEqual( - form.fields["status"].widget.choices, - DomainApplication._meta.get_field("status").choices, - ) + self.assertEqual( + form.fields["status"].widget.choices, + DomainApplication._meta.get_field("status").choices, + ) @boto3_mocking.patching @@ -327,90 +330,94 @@ class TestDomainApplicationAdmin(MockEppLib): def test_domain_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") + multiple_unalphabetical_domain_objects("application") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("requested_domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("requested_domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",)) def test_submitter_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") + multiple_unalphabetical_domain_objects("application") - additional_application = generic_domain_object("application", "Xylophone") - new_user = User.objects.filter(username=additional_application.investigator.username).get() - new_user.first_name = "Xylophonic" - new_user.save() + additional_application = generic_domain_object("application", "Xylophone") + new_user = User.objects.filter(username=additional_application.investigator.username).get() + new_user.first_name = "Xylophonic" + new_user.save() - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "5", - ( - "submitter__first_name", - "submitter__last_name", - ), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "5", + ( + "submitter__first_name", + "submitter__last_name", + ), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted( - "-5", - ( - "-submitter__first_name", - "-submitter__last_name", - ), - ) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted( + "-5", + ( + "-submitter__first_name", + "-submitter__last_name", + ), + ) def test_investigator_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") - additional_application = generic_domain_object("application", "Xylophone") - new_user = User.objects.filter(username=additional_application.investigator.username).get() - new_user.first_name = "Xylophonic" - new_user.save() + multiple_unalphabetical_domain_objects("application") + additional_application = generic_domain_object("application", "Xylophone") + new_user = User.objects.filter(username=additional_application.investigator.username).get() + new_user.first_name = "Xylophonic" + new_user.save() - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "6", - ( - "investigator__first_name", - "investigator__last_name", - ), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "6", + ( + "investigator__first_name", + "investigator__last_name", + ), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted( - "-6", - ( - "-investigator__first_name", - "-investigator__last_name", - ), - ) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted( + "-6", + ( + "-investigator__first_name", + "-investigator__last_name", + ), + ) def test_short_org_name_in_applications_list(self): """ Make sure the short name is displaying in admin on the list page """ - self.client.force_login(self.superuser) - completed_application() - response = self.client.get("/admin/registrar/domainapplication/") - # There are 3 template references to Federal (3) plus one reference in the table - # for our actual application - self.assertContains(response, "Federal", count=4) - # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) - # Now let's make sure the long description does not exist - self.assertNotContains(response, "Federal: an agency of the U.S. government") + with less_console_noise(): + self.client.force_login(self.superuser) + completed_application() + response = self.client.get("/admin/registrar/domainapplication/") + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains(response, 'Federal', count=1) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") def transition_state_and_send_email(self, application, status, rejection_reason=None): """Helper method for the email test cases.""" @@ -431,20 +438,21 @@ class TestDomainApplicationAdmin(MockEppLib): """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"] + with less_console_noise(): + # 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"] + # 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) + # 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): """When transitioning to submitted from started or withdrawn on a domain request, @@ -453,468 +461,514 @@ class TestDomainApplicationAdmin(MockEppLib): 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" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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() + # Create a sample application + application = completed_application() - # 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) + # 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) - # 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 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 (from withdrawn) - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Test Submitted Status Again (from withdrawn) + 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 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) + # 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 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) + # 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) + # 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 on a domain request, an email is sent out every time.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # 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) + # 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) - # Test Withdrawn Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.DOMAIN_PURPOSE) - self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Test Withdrawn Status + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.DOMAIN_PURPOSE, + ) + 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), 3) + # 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), 3) def test_save_model_sends_rejected_email_domain_purpose(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is domain purpose.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason DOMAIN_PURPOSE and test email - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.DOMAIN_PURPOSE) - self.assert_email_is_accurate("Your domain request was rejected because the purpose you provided did not meet our \nrequirements.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason DOMAIN_PURPOSE and test email + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.DOMAIN_PURPOSE, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because the purpose you provided did not meet our \nrequirements.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - 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) + # Approve + 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) def test_save_model_sends_rejected_email_requestor(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is requestor.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason REQUESTOR and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR) - self.assert_email_is_accurate("Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain on behalf of Testorg", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason REQUESTOR and test email including dynamic organization name + self.transition_state_and_send_email( + application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain " + "on behalf of Testorg", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - 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) + # Approve + 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) - def test_save_model_sends_rejected_email_requestor(self): + def test_save_model_sends_rejected_email_second_domain_reasoning(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is second domain.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.SECOND_DOMAIN_REASONING) - self.assert_email_is_accurate("Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.SECOND_DOMAIN_REASONING, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - 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) + # Approve + 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) def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is contacts or org legitimacy.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY) - self.assert_email_is_accurate("Your domain request was rejected because we could not verify the organizational \ncontacts you provided. If you have questions or comments, reply to this email.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we could not verify the organizational \n" + "contacts you provided. If you have questions or comments, reply to this email.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + 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) - # Approve - 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) - def test_save_model_sends_rejected_email_org_eligibility(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is org eligibility.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.ORGANIZATION_ELIGIBILITY) - self.assert_email_is_accurate("Your domain request was rejected because we determined that Testorg is not \neligible for a .gov domain.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.ORGANIZATION_ELIGIBILITY, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we determined that Testorg is not \neligible for " + "a .gov domain.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - 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) + # Approve + 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) def test_save_model_sends_rejected_email_naming(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is naming.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.NAMING_REQUIREMENTS) - self.assert_email_is_accurate("Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.NAMING_REQUIREMENTS, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - 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) + # Approve + 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) def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self): """ - When transitioning to rejected without a rejection reason, admin throws a user friendly message. + When transitioning to rejected without a rejection reason, admin throws a user friendly message. - The transition fails. + The transition fails. """ - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + with less_console_noise(): + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - with ExitStack() as stack: - stack.enter_context(patch.object(messages, "error")) - application.status = DomainApplication.ApplicationStatus.REJECTED + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser - self.admin.save_model(request, application, None, True) + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + application.status = DomainApplication.ApplicationStatus.REJECTED - messages.error.assert_called_once_with( - request, - "A rejection reason is required.", - ) + self.admin.save_model(request, application, None, True) - application.refresh_from_db() - self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED) + messages.error.assert_called_once_with( + request, + "A rejection reason is required.", + ) + + application.refresh_from_db() + self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED) def test_transition_to_rejected_with_rejection_reason_does_not_trigger_error(self): """ - When transitioning to rejected with a rejection reason, admin does not throw an error alert. + When transitioning to rejected with a rejection reason, admin does not throw an error alert. - The transition is successful. + The transition is successful. """ - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + with less_console_noise(): + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - with ExitStack() as stack: - stack.enter_context(patch.object(messages, "error")) - application.status = DomainApplication.ApplicationStatus.REJECTED - application.rejection_reason = DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser - self.admin.save_model(request, application, None, True) + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + application.status = DomainApplication.ApplicationStatus.REJECTED + application.rejection_reason = DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY - messages.error.assert_not_called() + self.admin.save_model(request, application, None, True) - application.refresh_from_db() - self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED) - - def test_save_model_clear_rejected_reason(self): - """When transitioning from rejected on a domain request, - the rejected_reason is cleared.""" - - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) - application.rejected_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE - application.save() - - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.approve() - - application.refresh_from_db() - self.assertEqual(application.rejected_reason, None) + messages.error.assert_not_called() + application.refresh_from_db() + self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED) def test_save_model_sends_withdrawn_email(self): """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" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # 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) + # 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 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 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), 3) + # 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), 3) def test_save_model_sets_approved_domain(self): # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # 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)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.APPROVED + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # 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) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) - # Test that approved domain exists and equals requested domain - self.assertEqual(application.requested_domain.name, application.approved_domain.name) + # Test that approved domain exists and equals requested domain + self.assertEqual(application.requested_domain.name, application.approved_domain.name) def test_save_model_sets_restricted_status_on_user(self): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # make sure 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) + # 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)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): # Modify the application's property application.status = DomainApplication.ApplicationStatus.INELIGIBLE # Use the model admin's save_model method self.admin.save_model(request, application, form=None, change=True) - # Test that approved domain exists and equals requested domain - self.assertEqual(application.creator.status, "restricted") + # Test that approved domain exists and equals requested domain + self.assertEqual(application.creator.status, "restricted") def test_readonly_when_restricted_creator(self): - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with less_console_noise(): + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): application.creator.status = User.RESTRICTED application.creator.save() - request = self.factory.get("/") - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request, application) - - expected_fields = [ - "id", - "created_at", - "updated_at", - "status", - "rejection_reason", - "creator", - "investigator", - "organization_type", - "federally_recognized_tribe", - "state_recognized_tribe", - "tribe_name", - "federal_agency", - "federal_type", - "is_election_board", - "organization_name", - "address_line1", - "address_line2", - "city", - "state_territory", - "zipcode", - "urbanization", - "about_your_organization", - "authorizing_official", - "approved_domain", - "requested_domain", - "submitter", - "purpose", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - "submission_date", - "notes", - "current_websites", - "other_contacts", - "alternative_domains", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_analyst(self): - request = self.factory.get("/") # Use the correct method and path - request.user = self.staffuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [ - "creator", - "about_your_organization", - "requested_domain", - "approved_domain", - "alternative_domains", - "purpose", - "submitter", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_superuser(self): - request = self.factory.get("/") # Use the correct method and path - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [] - - self.assertEqual(readonly_fields, expected_fields) - - def test_saving_when_restricted_creator(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.creator.status = User.RESTRICTED - application.creator.save() - - # Create a request object with a superuser - request = self.factory.get("/") - request.user = self.superuser - - with patch("django.contrib.messages.error") as mock_error: - # Simulate saving the model - self.admin.save_model(request, application, None, False) - - # Assert that the error message was called with the correct argument - mock_error.assert_called_once_with( - request, - "This action is not permitted for applications with a restricted creator.", - ) - - # Assert that the status has not changed - self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) - - def test_change_view_with_restricted_creator(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.creator.status = User.RESTRICTED - application.creator.save() - - with patch("django.contrib.messages.warning") as mock_warning: - # Create a request object with a superuser - request = self.factory.get("/admin/your_app/domainapplication/{}/change/".format(application.pk)) + request = self.factory.get("/") request.user = self.superuser - self.admin.display_restricted_warning(request, application) + readonly_fields = self.admin.get_readonly_fields(request, application) - # Assert that the error message was called with the correct argument - mock_warning.assert_called_once_with( - request, - "Cannot edit an application with a restricted creator.", - ) + expected_fields = [ + "id", + "created_at", + "updated_at", + "status", + "rejection_reason", + "creator", + "investigator", + "organization_type", + "federally_recognized_tribe", + "state_recognized_tribe", + "tribe_name", + "federal_agency", + "federal_type", + "is_election_board", + "organization_name", + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + "urbanization", + "about_your_organization", + "authorizing_official", + "approved_domain", + "requested_domain", + "submitter", + "purpose", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + "submission_date", + "notes", + "current_websites", + "other_contacts", + "alternative_domains", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_analyst(self): + with less_console_noise(): + request = self.factory.get("/") # Use the correct method and path + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "creator", + "about_your_organization", + "requested_domain", + "approved_domain", + "alternative_domains", + "purpose", + "submitter", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_superuser(self): + with less_console_noise(): + request = self.factory.get("/") # Use the correct method and path + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [] + + self.assertEqual(readonly_fields, expected_fields) + + def test_saving_when_restricted_creator(self): + with less_console_noise(): + # Create an instance of the model + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.creator.status = User.RESTRICTED + application.creator.save() + + # Create a request object with a superuser + request = self.factory.get("/") + request.user = self.superuser + + with patch("django.contrib.messages.error") as mock_error: + # Simulate saving the model + self.admin.save_model(request, application, None, False) + + # Assert that the error message was called with the correct argument + mock_error.assert_called_once_with( + request, + "This action is not permitted for applications with a restricted creator.", + ) + + # Assert that the status has not changed + self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) + + def test_change_view_with_restricted_creator(self): + with less_console_noise(): + # Create an instance of the model + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.creator.status = User.RESTRICTED + application.creator.save() + + with patch("django.contrib.messages.warning") as mock_warning: + # Create a request object with a superuser + request = self.factory.get("/admin/your_app/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser + + self.admin.display_restricted_warning(request, application) + + # Assert that the error message was called with the correct argument + mock_warning.assert_called_once_with( + request, + "Cannot edit an application with a restricted creator.", + ) def trigger_saving_approved_to_another_state(self, domain_is_active, another_state, rejection_reason=None): """Helper method that triggers domain request state changes from approved to another state, @@ -923,51 +977,52 @@ class TestDomainApplicationAdmin(MockEppLib): Used to test errors when saving a change with an active domain, also used to test side effects when saving a change goes through.""" - # 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() + with less_console_noise(): + # 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 + # 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 domain_is_active # Override to return True + # Define a custom implementation for is_active + def custom_is_active(self): + 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")) + # 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")) - application.status = another_state - application.rejection_reason = rejection_reason + application.status = another_state + application.rejection_reason = rejection_reason - self.admin.save_model(request, application, None, True) + self.admin.save_model(request, application, None, True) - # 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 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() - self.assertEqual(application.approved_domain, None) + self.assertEqual(application.approved_domain, None) - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.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() + # 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) @@ -988,7 +1043,11 @@ class TestDomainApplicationAdmin(MockEppLib): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED) def test_side_effects_when_saving_approved_to_rejected(self): - self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY) + self.trigger_saving_approved_to_another_state( + False, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY, + ) def test_side_effects_when_saving_approved_to_ineligible(self): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE) @@ -1000,14 +1059,15 @@ class TestDomainApplicationAdmin(MockEppLib): It retrieves the current list of filters from DomainApplicationAdmin and checks that it matches the expected list of filters. """ - request = self.factory.get("/") - request.user = self.superuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.superuser - # Grab the current list of table filters - readonly_fields = self.admin.get_list_filter(request) - expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) + # Grab the current list of table filters + readonly_fields = self.admin.get_list_filter(request) + expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_table_sorted_alphabetically(self): """ @@ -1019,23 +1079,24 @@ class TestDomainApplicationAdmin(MockEppLib): that it matches the expected queryset, which is sorted alphabetically by the 'requested_domain__name' field. """ - # Creates a list of DomainApplications in scrambled order - multiple_unalphabetical_domain_objects("application") + with less_console_noise(): + # Creates a list of DomainApplications in scrambled order + multiple_unalphabetical_domain_objects("application") - request = self.factory.get("/") - request.user = self.superuser + request = self.factory.get("/") + request.user = self.superuser - # Get the expected list of alphabetically sorted DomainApplications - expected_order = DomainApplication.objects.order_by("requested_domain__name") + # Get the expected list of alphabetically sorted DomainApplications + expected_order = DomainApplication.objects.order_by("requested_domain__name") - # Get the returned queryset - queryset = self.admin.get_queryset(request) + # Get the returned queryset + queryset = self.admin.get_queryset(request) - # Check the order - self.assertEqual( - list(queryset), - list(expected_order), - ) + # Check the order + self.assertEqual( + list(queryset), + list(expected_order), + ) def test_displays_investigator_filter(self): """ @@ -1051,37 +1112,38 @@ class TestDomainApplicationAdmin(MockEppLib): the filter displays correctly, when the filter isn't filtering correctly. """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainapplication/", - { - "investigator__id__exact": investigator_user.id, - }, - follow=True, - ) - - # Then, test if the filter actually exists - self.assertIn("filters", response.context) - - # Assert the content of filters and search_query - filters = response.context["filters"] - - self.assertEqual( - filters, - [ + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domainapplication/", { - "parameter_name": "investigator", - "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", + "investigator__id__exact": investigator_user.id, }, - ], - ) + follow=True, + ) + + # Then, test if the filter actually exists + self.assertIn("filters", response.context) + + # Assert the content of filters and search_query + filters = response.context["filters"] + + self.assertEqual( + filters, + [ + { + "parameter_name": "investigator", + "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", + }, + ], + ) def test_investigator_dropdown_displays_only_staff(self): """ @@ -1094,68 +1156,71 @@ class TestDomainApplicationAdmin(MockEppLib): It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin and checks that it matches the expected queryset, which only includes staff users. """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() - # Create a mock DomainApplication object, with a user that is not staff - application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy") - investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() - investigator_user_2.is_staff = False - investigator_user_2.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - p = "userpass" - self.client.login(username="staffuser", password=p) + # Create a mock DomainApplication object, with a user that is not staff + application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy") + investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() + investigator_user_2.is_staff = False + investigator_user_2.save() - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + p = "userpass" + self.client.login(username="staffuser", password=p) - # Get the actual field from the model's meta information - investigator_field = DomainApplication._meta.get_field("investigator") + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # We should only be displaying staff users, in alphabetical order - expected_dropdown = list(User.objects.filter(is_staff=True)) - current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset) + # Get the actual field from the model's meta information + investigator_field = DomainApplication._meta.get_field("investigator") - self.assertEqual(expected_dropdown, current_dropdown) + # We should only be displaying staff users, in alphabetical order + expected_dropdown = list(User.objects.filter(is_staff=True)) + current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset) - # Non staff users should not be in the list - self.assertNotIn(application_2, current_dropdown) + self.assertEqual(expected_dropdown, current_dropdown) + + # Non staff users should not be in the list + self.assertNotIn(application_2, current_dropdown) def test_investigator_list_is_alphabetically_sorted(self): """ This test verifies that filter list for the 'investigator' is displayed alphabetically """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - application_2: DomainApplication = generic_domain_object("application", "AGuy") - investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() - investigator_user_2.first_name = "AGuy" - investigator_user_2.is_staff = True - investigator_user_2.save() + application_2: DomainApplication = generic_domain_object("application", "AGuy") + investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() + investigator_user_2.first_name = "AGuy" + investigator_user_2.is_staff = True + investigator_user_2.save() - application_3: DomainApplication = generic_domain_object("application", "FinalGuy") - investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get() - investigator_user_3.first_name = "FinalGuy" - investigator_user_3.is_staff = True - investigator_user_3.save() + application_3: DomainApplication = generic_domain_object("application", "FinalGuy") + investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get() + investigator_user_3.first_name = "FinalGuy" + investigator_user_3.is_staff = True + investigator_user_3.save() - p = "userpass" - self.client.login(username="staffuser", password=p) - request = RequestFactory().get("/") + p = "userpass" + self.client.login(username="staffuser", password=p) + request = RequestFactory().get("/") - expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")) + expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")) - # Get the actual sorted list of investigators from the lookups method - actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] + # Get the actual sorted list of investigators from the lookups method + actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] - self.assertEqual(expected_list, actual_list) + self.assertEqual(expected_list, actual_list) def tearDown(self): super().tearDown() @@ -1184,28 +1249,29 @@ class DomainInvitationAdminTest(TestCase): def test_get_filters(self): """Ensures that our filters are displaying correctly""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domaininvitation/", - {}, - follow=True, - ) + response = self.client.get( + "/admin/registrar/domaininvitation/", + {}, + follow=True, + ) - # Assert that the filters are added - self.assertContains(response, "invited", count=2) - self.assertContains(response, "Invited", count=2) - self.assertContains(response, "retrieved", count=2) - self.assertContains(response, "Retrieved", count=2) + # Assert that the filters are added + self.assertContains(response, "invited", count=2) + self.assertContains(response, "Invited", count=2) + self.assertContains(response, "retrieved", count=2) + self.assertContains(response, "Retrieved", count=2) - # Check for the HTML context specificially - invited_html = 'Invited' - retrieved_html = 'Retrieved' + # Check for the HTML context specificially + invited_html = 'Invited' + retrieved_html = 'Retrieved' - self.assertContains(response, invited_html, count=1) - self.assertContains(response, retrieved_html, count=1) + self.assertContains(response, invited_html, count=1) + self.assertContains(response, retrieved_html, count=1) class TestDomainInformationAdmin(TestCase): @@ -1261,50 +1327,53 @@ class TestDomainInformationAdmin(TestCase): User.objects.all().delete() def test_readonly_fields_for_analyst(self): - """Ensures that analysts have their permissions setup correctly""" - request = self.factory.get("/") - request.user = self.staffuser + with less_console_noise(): + """Ensures that analysts have their permissions setup correctly""" + request = self.factory.get("/") + request.user = self.staffuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [ - "creator", - "type_of_work", - "more_organization_information", - "domain", - "domain_application", - "submitter", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - ] + expected_fields = [ + "creator", + "type_of_work", + "more_organization_information", + "domain", + "domain_application", + "submitter", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + ] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_domain_sortable(self): """Tests if DomainInformation sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-domain__name",)) def test_submitter_sortable(self): """Tests if DomainInformation sorts by submitter correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "4", - ("submitter__first_name", "submitter__last_name"), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "4", + ("submitter__first_name", "submitter__last_name"), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name")) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name")) class UserDomainRoleAdminTest(TestCase): @@ -1331,109 +1400,113 @@ class UserDomainRoleAdminTest(TestCase): def test_domain_sortable(self): """Tests if the UserDomainrole sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" + ) - # Create a list of UserDomainRoles that are in random order - mocks_to_create = ["jkl.gov", "ghi.gov", "abc.gov", "def.gov"] - for name in mocks_to_create: - fake_domain = Domain.objects.create(name=name) - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Create a list of UserDomainRoles that are in random order + mocks_to_create = ["jkl.gov", "ghi.gov", "abc.gov", "def.gov"] + for name in mocks_to_create: + fake_domain = Domain.objects.create(name=name) + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("2", ("domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("2", ("domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-2", ("-domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-2", ("-domain__name",)) def test_user_sortable(self): """Tests if the UserDomainrole sorts by user correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - mock_data_generator = AuditedAdminMockData() + mock_data_generator = AuditedAdminMockData() - fake_domain = Domain.objects.create(name="igorville.gov") - # Create a list of UserDomainRoles that are in random order - mocks_to_create = ["jkl", "ghi", "abc", "def"] - for name in mocks_to_create: - # Creates a fake "User" object - fake_user = mock_data_generator.dummy_user(name, "user") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + fake_domain = Domain.objects.create(name="igorville.gov") + # Create a list of UserDomainRoles that are in random order + mocks_to_create = ["jkl", "ghi", "abc", "def"] + for name in mocks_to_create: + # Creates a fake "User" object + fake_user = mock_data_generator.dummy_user(name, "user") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("user__first_name", "user__last_name")) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("user__first_name", "user__last_name")) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-user__first_name", "-user__last_name")) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-user__first_name", "-user__last_name")) def test_email_not_in_search(self): """Tests the search bar in Django Admin for UserDomainRoleAdmin. Should return no results for an invalid email.""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" - ) - fake_domain = Domain.objects.create(name="test123") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Make the request using the Client class - # which handles CSRF - # Follow=True handles the redirect - response = self.client.get( - "/admin/registrar/userdomainrole/", - { - "q": "testmail@igorville.com", - }, - follow=True, - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" + ) + fake_domain = Domain.objects.create(name="test123") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Make the request using the Client class + # which handles CSRF + # Follow=True handles the redirect + response = self.client.get( + "/admin/registrar/userdomainrole/", + { + "q": "testmail@igorville.com", + }, + follow=True, + ) - # Assert that the query is added to the extra_context - self.assertIn("search_query", response.context) - # Assert the content of filters and search_query - search_query = response.context["search_query"] - self.assertEqual(search_query, "testmail@igorville.com") + # Assert that the query is added to the extra_context + self.assertIn("search_query", response.context) + # Assert the content of filters and search_query + search_query = response.context["search_query"] + self.assertEqual(search_query, "testmail@igorville.com") - # We only need to check for the end of the HTML string - self.assertNotContains(response, "Stewart Jones AntarcticPolarBears@example.com") + # We only need to check for the end of the HTML string + self.assertNotContains(response, "Stewart Jones AntarcticPolarBears@example.com") def test_email_in_search(self): """Tests the search bar in Django Admin for UserDomainRoleAdmin. Should return results for an valid email.""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Joe", last_name="Jones", email="AntarcticPolarBears@example.com" - ) - fake_domain = Domain.objects.create(name="fake") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Make the request using the Client class - # which handles CSRF - # Follow=True handles the redirect - response = self.client.get( - "/admin/registrar/userdomainrole/", - { - "q": "AntarcticPolarBears@example.com", - }, - follow=True, - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Joe", last_name="Jones", email="AntarcticPolarBears@example.com" + ) + fake_domain = Domain.objects.create(name="fake") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Make the request using the Client class + # which handles CSRF + # Follow=True handles the redirect + response = self.client.get( + "/admin/registrar/userdomainrole/", + { + "q": "AntarcticPolarBears@example.com", + }, + follow=True, + ) - # Assert that the query is added to the extra_context - self.assertIn("search_query", response.context) + # Assert that the query is added to the extra_context + self.assertIn("search_query", response.context) - search_query = response.context["search_query"] - self.assertEqual(search_query, "AntarcticPolarBears@example.com") + search_query = response.context["search_query"] + self.assertEqual(search_query, "AntarcticPolarBears@example.com") - # We only need to check for the end of the HTML string - self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com", count=1) + # We only need to check for the end of the HTML string + self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com", count=1) class ListHeaderAdminTest(TestCase): @@ -1515,39 +1588,42 @@ class MyUserAdminTest(TestCase): self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site) def test_list_display_without_username(self): - request = self.client.request().wsgi_request - request.user = create_user() + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_user() - list_display = self.admin.get_list_display(request) - expected_list_display = [ - "email", - "first_name", - "last_name", - "group", - "status", - ] + list_display = self.admin.get_list_display(request) + expected_list_display = [ + "email", + "first_name", + "last_name", + "group", + "status", + ] - self.assertEqual(list_display, expected_list_display) - self.assertNotIn("username", list_display) + self.assertEqual(list_display, expected_list_display) + self.assertNotIn("username", list_display) def test_get_fieldsets_superuser(self): - request = self.client.request().wsgi_request - request.user = create_superuser() - fieldsets = self.admin.get_fieldsets(request) - expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request) - self.assertEqual(fieldsets, expected_fieldsets) + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_superuser() + fieldsets = self.admin.get_fieldsets(request) + expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request) + self.assertEqual(fieldsets, expected_fieldsets) def test_get_fieldsets_cisa_analyst(self): - request = self.client.request().wsgi_request - request.user = create_user() - fieldsets = self.admin.get_fieldsets(request) - expected_fieldsets = ( - (None, {"fields": ("password", "status")}), - ("Personal Info", {"fields": ("first_name", "last_name", "email")}), - ("Permissions", {"fields": ("is_active", "groups")}), - ("Important dates", {"fields": ("last_login", "date_joined")}), - ) - self.assertEqual(fieldsets, expected_fieldsets) + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_user() + fieldsets = self.admin.get_fieldsets(request) + expected_fieldsets = ( + (None, {"fields": ("password", "status")}), + ("Personal Info", {"fields": ("first_name", "last_name", "email")}), + ("Permissions", {"fields": ("is_active", "groups")}), + ("Important dates", {"fields": ("last_login", "date_joined")}), + ) + self.assertEqual(fieldsets, expected_fieldsets) def tearDown(self): User.objects.all().delete() @@ -1571,161 +1647,164 @@ class AuditedAdminTest(TestCase): return ordered_list def test_alphabetically_sorted_fk_fields_domain_application(self): - tested_fields = [ - DomainApplication.authorizing_official.field, - DomainApplication.submitter.field, - DomainApplication.investigator.field, - DomainApplication.creator.field, - DomainApplication.requested_domain.field, - ] + with less_console_noise(): + tested_fields = [ + DomainApplication.authorizing_official.field, + DomainApplication.submitter.field, + DomainApplication.investigator.field, + DomainApplication.creator.field, + DomainApplication.requested_domain.field, + ] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("application") + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("application") - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)) - model_admin = AuditedAdmin(DomainApplication, self.site) + model_admin = AuditedAdmin(DomainApplication, self.site) - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - isNamefield: bool = field == DomainApplication.requested_domain.field - if isNamefield: - sorted_fields = ["name"] - else: - sorted_fields = ["first_name", "last_name"] - # We want both of these to be lists, as it is richer test wise. - - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - if not isNamefield: - first = contact.first_name - last = contact.last_name + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + isNamefield: bool = field == DomainApplication.requested_domain.field + if isNamefield: + sorted_fields = ["name"] else: + sorted_fields = ["first_name", "last_name"] + # We want both of these to be lists, as it is richer test wise. + + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: + if not isNamefield: + first = contact.first_name + last = contact.last_name + else: + first = contact.name + last = None + + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name), + ) + + def test_alphabetically_sorted_fk_fields_domain_information(self): + with less_console_noise(): + tested_fields = [ + DomainInformation.authorizing_official.field, + DomainInformation.submitter.field, + # DomainInformation.creator.field, + (DomainInformation.domain.field, ["name"]), + (DomainInformation.domain_application.field, ["requested_domain__name"]), + ] + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("information") + + # Create a mock request + request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) + + model_admin = AuditedAdmin(DomainInformation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + isOtherOrderfield: bool = isinstance(field, tuple) + field_obj = None + if isOtherOrderfield: + sorted_fields = field[1] + field_obj = field[0] + else: + sorted_fields = ["first_name", "last_name"] + field_obj = field + # We want both of these to be lists, as it is richer test wise. + desired_order = self.order_by_desired_field_helper(model_admin, request, field_obj.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field_obj, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for obj in current_sort_order: + last = None + if not isOtherOrderfield: + first = obj.first_name + last = obj.last_name + elif field_obj == DomainInformation.domain.field: + first = obj.name + elif field_obj == DomainInformation.domain_application.field: + first = obj.requested_domain.name + + name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field_obj.name), + ) + + def test_alphabetically_sorted_fk_fields_domain_invitation(self): + with less_console_noise(): + tested_fields = [DomainInvitation.domain.field] + + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("invitation") + + # Create a mock request + request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) + + model_admin = AuditedAdmin(DomainInvitation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + sorted_fields = ["name"] + # We want both of these to be lists, as it is richer test wise. + + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: first = contact.name last = None - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_information(self): - tested_fields = [ - DomainInformation.authorizing_official.field, - DomainInformation.submitter.field, - # DomainInformation.creator.field, - (DomainInformation.domain.field, ["name"]), - (DomainInformation.domain_application.field, ["requested_domain__name"]), - ] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("information") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInformation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - isOtherOrderfield: bool = isinstance(field, tuple) - field_obj = None - if isOtherOrderfield: - sorted_fields = field[1] - field_obj = field[0] - else: - sorted_fields = ["first_name", "last_name"] - field_obj = field - # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper(model_admin, request, field_obj.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field_obj, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for obj in current_sort_order: - last = None - if not isOtherOrderfield: - first = obj.first_name - last = obj.last_name - elif field_obj == DomainInformation.domain.field: - first = obj.name - elif field_obj == DomainInformation.domain_application.field: - first = obj.requested_domain.name - - name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field_obj.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_invitation(self): - tested_fields = [DomainInvitation.domain.field] - - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("invitation") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInvitation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - sorted_fields = ["name"] - # We want both of these to be lists, as it is richer test wise. - - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - first = contact.name - last = None - - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name), + ) def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): """Handles edge cases for test cases""" @@ -1759,93 +1838,98 @@ class DomainSessionVariableTest(TestCase): def test_session_vars_set_correctly(self): """Checks if session variables are being set correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information = generic_domain_object("information", "session") - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual( - request.session["analyst_action_location"], - dummy_domain_information.domain.pk, - ) + dummy_domain_information = generic_domain_object("information", "session") + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + self.populate_session_values(request, dummy_domain_information.domain) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual( + request.session["analyst_action_location"], + dummy_domain_information.domain.pk, + ) def test_session_vars_set_correctly_hardcoded_domain(self): """Checks if session variables are being set correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information: Domain = generic_domain_object("information", "session") - dummy_domain_information.domain.pk = 1 + dummy_domain_information: Domain = generic_domain_object("information", "session") + dummy_domain_information.domain.pk = 1 - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual(request.session["analyst_action_location"], 1) + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + self.populate_session_values(request, dummy_domain_information.domain) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual(request.session["analyst_action_location"], 1) def test_session_variables_reset_correctly(self): """Checks if incorrect session variables get overridden""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information = generic_domain_object("information", "session") - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + dummy_domain_information = generic_domain_object("information", "session") + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain, preload_bad_data=True) + self.populate_session_values(request, dummy_domain_information.domain, preload_bad_data=True) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual( - request.session["analyst_action_location"], - dummy_domain_information.domain.pk, - ) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual( + request.session["analyst_action_location"], + dummy_domain_information.domain.pk, + ) def test_session_variables_retain_information(self): """Checks to see if session variables retain old information""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information_list = multiple_unalphabetical_domain_objects("information") - for item in dummy_domain_information_list: - request = self.get_factory_post_edit_domain(item.domain.pk) - self.populate_session_values(request, item.domain) + dummy_domain_information_list = multiple_unalphabetical_domain_objects("information") + for item in dummy_domain_information_list: + request = self.get_factory_post_edit_domain(item.domain.pk) + self.populate_session_values(request, item.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual(request.session["analyst_action_location"], item.domain.pk) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual(request.session["analyst_action_location"], item.domain.pk) def test_session_variables_concurrent_requests(self): """Simulates two requests at once""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - info_first = generic_domain_object("information", "session") - info_second = generic_domain_object("information", "session2") + info_first = generic_domain_object("information", "session") + info_second = generic_domain_object("information", "session2") - request_first = self.get_factory_post_edit_domain(info_first.domain.pk) - request_second = self.get_factory_post_edit_domain(info_second.domain.pk) + request_first = self.get_factory_post_edit_domain(info_first.domain.pk) + request_second = self.get_factory_post_edit_domain(info_second.domain.pk) - self.populate_session_values(request_first, info_first.domain, True) - self.populate_session_values(request_second, info_second.domain, True) + self.populate_session_values(request_first, info_first.domain, True) + self.populate_session_values(request_second, info_second.domain, True) - # Check if anything got nulled out - self.assertNotEqual(request_first.session["analyst_action"], None) - self.assertNotEqual(request_second.session["analyst_action"], None) - self.assertNotEqual(request_first.session["analyst_action_location"], None) - self.assertNotEqual(request_second.session["analyst_action_location"], None) + # Check if anything got nulled out + self.assertNotEqual(request_first.session["analyst_action"], None) + self.assertNotEqual(request_second.session["analyst_action"], None) + self.assertNotEqual(request_first.session["analyst_action_location"], None) + self.assertNotEqual(request_second.session["analyst_action_location"], None) - # Check if they are both the same action 'type' - self.assertEqual(request_first.session["analyst_action"], "edit") - self.assertEqual(request_second.session["analyst_action"], "edit") + # Check if they are both the same action 'type' + self.assertEqual(request_first.session["analyst_action"], "edit") + self.assertEqual(request_second.session["analyst_action"], "edit") - # Check their locations, and ensure they aren't the same across both - self.assertNotEqual( - request_first.session["analyst_action_location"], - request_second.session["analyst_action_location"], - ) + # Check their locations, and ensure they aren't the same across both + self.assertNotEqual( + request_first.session["analyst_action_location"], + request_second.session["analyst_action_location"], + ) def populate_session_values(self, request, domain_object, preload_bad_data=False): """Boilerplate for creating mock sessions""" @@ -1878,63 +1962,66 @@ class ContactAdminTest(TestCase): self.staffuser = create_user() def test_readonly_when_restricted_staffuser(self): - request = self.factory.get("/") - request.user = self.staffuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.staffuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [ - "user", - ] + expected_fields = [ + "user", + ] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_readonly_when_restricted_superuser(self): - request = self.factory.get("/") - request.user = self.superuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.superuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [] + expected_fields = [] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_change_view_for_joined_contact_five_or_less(self): """Create a contact, join it to 4 domain requests. The 5th join will be a user. Assert that the warning on the contact form lists 5 joins.""" - self.client.force_login(self.superuser) + with less_console_noise(): + self.client.force_login(self.superuser) - # Create an instance of the model - contact, _ = Contact.objects.get_or_create(user=self.staffuser) + # Create an instance of the model + contact, _ = Contact.objects.get_or_create(user=self.staffuser) - # join it to 4 domain requests. The 5th join will be a user. - application1 = completed_application(submitter=contact, name="city1.gov") - application2 = completed_application(submitter=contact, name="city2.gov") - application3 = completed_application(submitter=contact, name="city3.gov") - application4 = completed_application(submitter=contact, name="city4.gov") + # join it to 4 domain requests. The 5th join will be a user. + application1 = completed_application(submitter=contact, name="city1.gov") + application2 = completed_application(submitter=contact, name="city2.gov") + application3 = completed_application(submitter=contact, name="city3.gov") + application4 = completed_application(submitter=contact, name="city4.gov") - with patch("django.contrib.messages.warning") as mock_warning: - # Use the test client to simulate the request - response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) + with patch("django.contrib.messages.warning") as mock_warning: + # Use the test client to simulate the request + response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) - # Assert that the error message was called with the correct argument - # Note: The 5th join will be a user. - mock_warning.assert_called_once_with( - response.wsgi_request, - "", - ) + # Assert that the error message was called with the correct argument + # Note: The 5th join will be a user. + mock_warning.assert_called_once_with( + response.wsgi_request, + "", + ) def test_change_view_for_joined_contact_five_or_more(self): """Create a contact, join it to 5 domain requests. The 6th join will be a user. @@ -1984,20 +2071,21 @@ class VerifiedByStaffAdminTestCase(TestCase): self.factory = RequestFactory() def test_save_model_sets_user_field(self): - self.client.force_login(self.superuser) + with less_console_noise(): + self.client.force_login(self.superuser) - # Create an instance of the admin class - admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None) + # Create an instance of the admin class + admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None) - # Create a VerifiedByStaff instance - vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") + # Create a VerifiedByStaff instance + vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") - # Create a request object - request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/") - request.user = self.superuser + # Create a request object + request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/") + request.user = self.superuser - # Call the save_model method - admin_instance.save_model(request, vip_instance, None, None) + # Call the save_model method + admin_instance.save_model(request, vip_instance, None, None) - # Check that the user field is set to the request.user - self.assertEqual(vip_instance.requestor, self.superuser) + # Check that the user field is set to the request.user + self.assertEqual(vip_instance.requestor, self.superuser) From 5b2ca61de64589548810ef1bbd045172820a78a6 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 20 Feb 2024 15:30:54 -0500 Subject: [PATCH 13/29] Refactor JS to account for changes to select then back/forward navigation --- src/registrar/assets/js/get-gov-admin.js | 27 ++++++++++++++++++------ src/registrar/tests/test_admin.py | 17 --------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index b5f5a6aaa..803443bc9 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -317,22 +317,37 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, * status select amd to show/hide the rejection reason */ (function (){ - - // Get the rejection reason form row let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') if (rejectionReasonFormGroup) { - // Get the status select let statusSelect = document.getElementById('id_status') - // If status is rejected, hide the rejection reason on load + // Initial handling of rejectionReasonFormGroup display if (statusSelect.value != 'rejected') rejectionReasonFormGroup.style.display = 'none'; - // Listen to status changes and toggle rejection reason + // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage statusSelect.addEventListener('change', function() { - rejectionReasonFormGroup.style.display = statusSelect.value !== 'rejected' ? 'none' : 'block'; + if (statusSelect.value == 'rejected') { + rejectionReasonFormGroup.style.display = 'block'; + sessionStorage.removeItem('hideRejectionReason'); + } else { + rejectionReasonFormGroup.style.display = 'none'; + sessionStorage.setItem('hideRejectionReason', 'true'); + } }); } + // Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage + const observer = new PerformanceObserver((list) => { + list.getEntries().forEach((entry) => { + if (entry.type === "back_forward") { + if (sessionStorage.getItem('hideRejectionReason')) + document.querySelector('.field-rejection_reason').style.display = 'none'; + else + document.querySelector('.field-rejection_reason').style.display = 'block'; + } + }); + }); + observer.observe({ type: "navigation" }); })(); diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 484898be4..779861c67 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -699,23 +699,6 @@ class TestDomainApplicationAdmin(MockEppLib): application.refresh_from_db() self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED) - def test_save_model_clear_rejected_reason(self): - """When transitioning from rejected on a domain request, - the rejected_reason is cleared.""" - - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) - application.rejected_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE - application.save() - - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.approve() - - application.refresh_from_db() - self.assertEqual(application.rejected_reason, None) - - def test_save_model_sends_withdrawn_email(self): """When transitioning to withdrawn on a domain request, an email is sent out every time.""" From 7e9899dd6d0f92868eda3603091874b5c8f017d4 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 20 Feb 2024 15:44:45 -0500 Subject: [PATCH 14/29] lint and cleanup --- src/registrar/tests/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 8a7c1c0a4..893678848 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -582,8 +582,8 @@ class TestDomainApplicationAdmin(MockEppLib): application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR ) self.assert_email_is_accurate( - "Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain " - "on behalf of Testorg", + "Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov " + "domain on behalf of Testorg", 0, EMAIL, ) From ad591086bed23f7550ad2e625ca4e121b9c44a31 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 20 Feb 2024 17:26:30 -0500 Subject: [PATCH 15/29] Add rejection reasons filter --- src/registrar/admin.py | 2 +- src/registrar/tests/test_admin.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c75588a27..1b13d8e74 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -779,7 +779,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): ] # Filters - list_filter = ("status", "organization_type", InvestigatorFilter) + list_filter = ("status", "organization_type", "rejection_reason", InvestigatorFilter) # Search search_fields = [ diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 893678848..69e756026 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1065,7 +1065,12 @@ class TestDomainApplicationAdmin(MockEppLib): # Grab the current list of table filters readonly_fields = self.admin.get_list_filter(request) - expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) + expected_fields = ( + "status", + "organization_type", + "rejection_reason", + DomainApplicationAdmin.InvestigatorFilter, + ) self.assertEqual(readonly_fields, expected_fields) From e27acb2e9303fbd44c246ede37bd25dca6a72b42 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 20 Feb 2024 16:10:54 -0700 Subject: [PATCH 16/29] Made "required" error message match "invalid" error message for zip code on Organization name and mailing address --- src/registrar/forms/application_wizard.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 1ee7e0036..a76626b1e 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -284,6 +284,9 @@ class OrganizationContactForm(RegistrarForm): message="Enter a zip code in the form of 12345 or 12345-6789.", ) ], + error_messages={ + "required": ("Enter a zip code in the form of 12345 or 12345-6789.") + }, ) urbanization = forms.CharField( required=False, From 3da387ab1c17ba52fb7ae7ec124710360a673165 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 20 Feb 2024 16:17:48 -0700 Subject: [PATCH 17/29] linted --- src/registrar/forms/application_wizard.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index a76626b1e..df5b195c6 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -284,9 +284,7 @@ class OrganizationContactForm(RegistrarForm): message="Enter a zip code in the form of 12345 or 12345-6789.", ) ], - error_messages={ - "required": ("Enter a zip code in the form of 12345 or 12345-6789.") - }, + error_messages={"required": ("Enter a zip code in the form of 12345 or 12345-6789.")}, ) urbanization = forms.CharField( required=False, From 8af76b78f0a398940f5ecee38b4c33118713ffa2 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 22 Feb 2024 18:47:10 -0500 Subject: [PATCH 18/29] BCC help on submit --- src/registrar/config/settings.py | 1 + src/registrar/models/domain_application.py | 10 ++- src/registrar/tests/test_admin.py | 79 ++++++++++++++++++- .../test_environment_variables_effects.py | 31 -------- src/registrar/tests/test_views.py | 30 ++++++- src/registrar/utility/email.py | 8 +- src/registrar/views/domain.py | 3 +- 7 files changed, 122 insertions(+), 40 deletions(-) delete mode 100644 src/registrar/tests/test_environment_variables_effects.py diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 009baa1c6..bb8e22ad7 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -286,6 +286,7 @@ AWS_MAX_ATTEMPTS = 3 BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS}) # email address to use for various automated correspondence +# also used as a default to and bcc email DEFAULT_FROM_EMAIL = "help@get.gov " # connect to an (external) SMTP server for sending email diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 17bc71cbe..1a0095ccd 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -4,6 +4,7 @@ from typing import Union import logging from django.apps import apps +from django.conf import settings from django.db import models from django_fsm import FSMField, transition # type: ignore from django.utils import timezone @@ -588,7 +589,7 @@ class DomainApplication(TimeStampedModel): 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): + def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True, bcc_address=''): """Send a status update email to the submitter. The email goes to the email address that the submitter gave as their @@ -613,6 +614,7 @@ class DomainApplication(TimeStampedModel): email_template_subject, self.submitter.email, context={"application": self}, + bcc_address=bcc_address, ) logger.info(f"The {new_status} email sent to: {self.submitter.email}") except EmailSendingError: @@ -654,11 +656,17 @@ class DomainApplication(TimeStampedModel): # Limit email notifications to transitions from Started and Withdrawn limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN] + bcc_address = '' + if settings.IS_PRODUCTION: + bcc_address = settings.DEFAULT_FROM_EMAIL + if self.status in limited_statuses: self._send_status_update_email( "submission confirmation", "emails/submission_confirmation.txt", "emails/submission_confirmation_subject.txt", + True, + bcc_address, ) @transition( diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f90b18584..c44c54ea9 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,4 +1,4 @@ -from django.test import TestCase, RequestFactory, Client +from django.test import TestCase, RequestFactory, Client, override_settings from django.contrib.admin.sites import AdminSite from contextlib import ExitStack from django.contrib import messages @@ -426,7 +426,7 @@ class TestDomainApplicationAdmin(MockEppLib): # 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): + def assert_email_is_accurate(self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address=''): """Helper method for the email test cases. email_index is the index of the email in mock_client.""" @@ -445,12 +445,26 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(to_email, email_address) self.assertIn(expected_string, email_body) + if test_that_no_bcc: + _ = '' + with self.assertRaises(KeyError): + with less_console_noise(): + _ = kwargs["Destination"]["BccAddresses"][0] + self.assertEqual(_, '') + + if bcc_email_address: + bcc_email = kwargs["Destination"]["BccAddresses"][0] + self.assertEqual(bcc_email, bcc_email_address) + def test_save_model_sends_submitted_email(self): """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.""" + no email is sent out. + + Also test that the default email set in settings is NOT BCCd on non-prod whenever + an email does go out.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -461,7 +475,63 @@ class TestDomainApplicationAdmin(MockEppLib): # 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.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # 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, True + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (from withdrawn) + 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) + + # 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) + + @override_settings(IS_PRODUCTION=True) + def test_save_model_sends_submitted_email_with_bcc_on_prod(self): + """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. + + Also test that the default email set in settings IS BCCd on prod whenever + an email does go out.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + BCC_EMAIL = settings.DEFAULT_FROM_EMAIL + + # Create a sample application + application = completed_application() + + # 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, False, BCC_EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) # Test Withdrawn Status @@ -473,6 +543,7 @@ class TestDomainApplicationAdmin(MockEppLib): # Test Submitted Status Again (from withdrawn) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) # Move it to IN_REVIEW diff --git a/src/registrar/tests/test_environment_variables_effects.py b/src/registrar/tests/test_environment_variables_effects.py deleted file mode 100644 index 3a838c2a2..000000000 --- a/src/registrar/tests/test_environment_variables_effects.py +++ /dev/null @@ -1,31 +0,0 @@ -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): - """No banner on prod.""" - home_page = self.client.get("/") - 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 on a test site.") diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 891c254c5..b60c94b76 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,4 +1,4 @@ -from django.test import Client, TestCase +from django.test import Client, TestCase, override_settings from django.contrib.auth import get_user_model from .common import MockEppLib # type: ignore @@ -50,3 +50,31 @@ class TestWithUser(MockEppLib): DomainApplication.objects.all().delete() DomainInformation.objects.all().delete() self.user.delete() + +class TestEnvironmentVariablesEffects(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): + """No banner on prod.""" + home_page = self.client.get("/") + 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 on a test site.") \ No newline at end of file diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 461637f23..d6758969d 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -15,7 +15,7 @@ class EmailSendingError(RuntimeError): pass -def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}): +def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address='', context={}): """Send an email built from a template to one email address. template_name and subject_template_name are relative to the same template @@ -39,11 +39,15 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr ) except Exception as exc: raise EmailSendingError("Could not access the SES client.") from exc + + destination = {"ToAddresses": [to_address]} + if bcc_address: + destination["BccAddresses"] = [bcc_address] try: ses_client.send_email( FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [to_address]}, + Destination=destination, Content={ "Simple": { "Subject": {"Data": subject}, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index f5517da25..72eb65f1e 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -14,6 +14,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.urls import reverse from django.views.generic.edit import FormMixin +from django.conf import settings from registrar.models import ( Domain, @@ -707,7 +708,7 @@ class DomainAddUserView(DomainFormBaseView): adding a success message to the view if the email sending succeeds""" # Set a default email address to send to for staff - requestor_email = "help@get.gov" + requestor_email = settings.DEFAULT_FROM_EMAIL # Check if the email requestor has a valid email address if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "": From d9b56957c5ea956130dee050fbd97004d2891c28 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 22 Feb 2024 19:32:25 -0500 Subject: [PATCH 19/29] add the other rejection reason --- src/registrar/assets/js/get-gov-admin.js | 4 +++ ...0070_domainapplication_rejection_reason.py | 1 + src/registrar/models/domain_application.py | 1 + .../emails/status_change_rejected.txt | 16 +++++++++-- src/registrar/tests/test_admin.py | 28 +++++++++++++++++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 803443bc9..9239a0488 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -339,6 +339,10 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, } // Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage + + // When you navigate using forward/back after changing status but not saving, when you land back on the DA page the + // status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide + // accurately for this edge case, we use cache and test for the back/forward navigation. const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (entry.type === "back_forward") { diff --git a/src/registrar/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py index defeb4d95..60dc8ac73 100644 --- a/src/registrar/migrations/0070_domainapplication_rejection_reason.py +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -27,6 +27,7 @@ class Migration(migrations.Migration): ), ("organization_eligibility", "Organization isn't eligible for a .gov"), ("naming_requirements", "Naming requirements not met"), + ("other", "Other"), ], null=True, ), diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index ca6a996ce..609b9df33 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -363,6 +363,7 @@ class DomainApplication(TimeStampedModel): ) ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met" + OTHER = "other", "Other" # #### Internal fields about the application ##### status = FSMField( diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index 4beacf25a..3dae38c0d 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -8,8 +8,9 @@ REQUEST RECEIVED ON: {{ application.submission_date|date }} STATUS: Rejected ---------------------------------------------------------------- - -REJECTION REASON{% if application.rejection_reason == 'domain_purpose' %} +{% if application.rejection_reason != 'other' %} +REJECTION REASON{% endif %} +{% if application.rejection_reason == 'domain_purpose' %} Your domain request was rejected because the purpose you provided did not meet our requirements. You didn’t provide enough information about how you intend to use the domain. @@ -63,6 +64,17 @@ general public. Learn more about naming requirements for your type of organizati YOU CAN SUBMIT A NEW REQUEST We encourage you to request a domain that meets our requirements. If you have questions or want to discuss potential domain names, reply to this email. +{% elif application.rejection_reason == 'other' %} +YOU CAN SUBMIT A NEW REQUEST +If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request. + +Learn more about: +- Eligibility for a .gov domain +- Choosing a .gov domain name + + +NEED ASSISTANCE? +If you have questions about this domain request or need help choosing a new domain name, reply to this email. {% endif %} THANK YOU diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 69e756026..4e449ca5d 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -712,6 +712,34 @@ class TestDomainApplicationAdmin(MockEppLib): self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_save_model_sends_rejected_email_other(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is other.""" + + with less_console_noise(): + # 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) + + # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.OTHER, + ) + self.assert_email_is_accurate( + "Choosing a .gov domain name", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + 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) + def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self): """ When transitioning to rejected without a rejection reason, admin throws a user friendly message. From 0dc66303bfb0f60aa8fd4f360836be65074c973b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 22 Feb 2024 20:13:55 -0500 Subject: [PATCH 20/29] lint --- src/registrar/tests/test_admin.py | 105 +----------------------------- 1 file changed, 1 insertion(+), 104 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 4225f82c5..341a8fac9 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -905,9 +905,7 @@ class TestDomainApplicationAdmin(MockEppLib): DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.OTHER, ) - self.assert_email_is_accurate( - "Choosing a .gov domain name", 0, EMAIL - ) + self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) # Approve @@ -2065,107 +2063,6 @@ class AuditedAdminTest(TestCase): "{} is not ordered alphabetically".format(field.name), ) - def test_alphabetically_sorted_fk_fields_domain_information(self): - tested_fields = [ - DomainInformation.authorizing_official.field, - DomainInformation.submitter.field, - # DomainInformation.creator.field, - (DomainInformation.domain.field, ["name"]), - (DomainInformation.domain_application.field, ["requested_domain__name"]), - ] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("information") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInformation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - isOtherOrderfield: bool = isinstance(field, tuple) - field_obj = None - if isOtherOrderfield: - sorted_fields = field[1] - field_obj = field[0] - else: - sorted_fields = ["first_name", "last_name"] - field_obj = field - # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper(model_admin, request, field_obj.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field_obj, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for obj in current_sort_order: - last = None - if not isOtherOrderfield: - first = obj.first_name - last = obj.last_name - elif field_obj == DomainInformation.domain.field: - first = obj.name - elif field_obj == DomainInformation.domain_application.field: - first = obj.requested_domain.name - - name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field_obj.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_invitation(self): - tested_fields = [DomainInvitation.domain.field] - - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("invitation") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInvitation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - sorted_fields = ["name"] - # We want both of these to be lists, as it is richer test wise. - - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - first = contact.name - last = None - - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) - def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): """Handles edge cases for test cases""" if first_name is None: From 9b86a154d0ab79766d94669e0269f90a7ec42b5e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 22 Feb 2024 20:46:56 -0500 Subject: [PATCH 21/29] made filters for election office and federal branch, and set the default value for election office to false --- src/registrar/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index d5a0f6bde..4880d174c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -785,13 +785,13 @@ class DomainApplicationAdmin(ListHeaderAdmin): ] def custom_election_board(self, obj): - return obj.is_election_board + return obj.is_election_board if obj.is_election_board else False custom_election_board.admin_order_field = "is_election_board" # type: ignore custom_election_board.short_description = "Election office" # type: ignore # Filters - list_filter = ("status", "organization_type", InvestigatorFilter) + list_filter = ("status", "organization_type", "is_election_board", "federal_type", InvestigatorFilter) # Search search_fields = [ @@ -1111,7 +1111,7 @@ class DomainAdmin(ListHeaderAdmin): state_territory.admin_order_field = "domain_info__state_territory" # type: ignore # Filters - list_filter = ["domain_info__organization_type", "state"] + list_filter = ["domain_info__organization_type", "state", "domain_info__is_election_board", "domain_info__federal_type"] search_fields = ["name"] search_help_text = "Search by domain name." From 262acee1cd39d916e34248954ce4f143dd488b87 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 26 Feb 2024 14:03:06 -0500 Subject: [PATCH 22/29] updated filters and results list header --- src/registrar/admin.py | 55 ++++++++++++++++--- .../templates/admin/change_list.html | 10 +++- src/registrar/tests/test_admin.py | 8 ++- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4880d174c..ddfce2c7a 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1,5 +1,6 @@ import logging from django import forms +from django.db.models import Q from django.db.models.functions import Concat from django.http import HttpResponse from django.shortcuts import redirect @@ -22,6 +23,7 @@ from auditlog.admin import LogEntryAdmin # type: ignore from django_fsm import TransitionNotAllowed # type: ignore from django.utils.safestring import mark_safe from django.utils.html import escape +from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) @@ -762,6 +764,24 @@ class DomainApplicationAdmin(ListHeaderAdmin): else: return queryset.filter(investigator__id__exact=self.value()) + class ElectionOfficeFilter(admin.SimpleListFilter): + """Define a custom filter for is_election_board""" + + title = _("election office") + parameter_name = "is_election_board" + + def lookups(self, request, model_admin): + return ( + ("1", _("Yes")), + ("0", _("No")), + ) + + def queryset(self, request, queryset): + if self.value() == "1": + return queryset.filter(is_election_board=True) + if self.value() == "0": + return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None)) + # Columns list_display = [ "requested_domain", @@ -785,13 +805,13 @@ class DomainApplicationAdmin(ListHeaderAdmin): ] def custom_election_board(self, obj): - return obj.is_election_board if obj.is_election_board else False + return "Yes" if obj.is_election_board else "No" custom_election_board.admin_order_field = "is_election_board" # type: ignore custom_election_board.short_description = "Election office" # type: ignore # Filters - list_filter = ("status", "organization_type", "is_election_board", "federal_type", InvestigatorFilter) + list_filter = ("status", "organization_type", "federal_type", ElectionOfficeFilter, InvestigatorFilter) # Search search_fields = [ @@ -1044,6 +1064,25 @@ class DomainInformationInline(admin.StackedInline): class DomainAdmin(ListHeaderAdmin): """Custom domain admin class to add extra buttons.""" + class ElectionOfficeFilter(admin.SimpleListFilter): + """Define a custom filter for is_election_board""" + + title = _("election office") + parameter_name = "is_election_board" + + def lookups(self, request, model_admin): + return ( + ("1", _("Yes")), + ("0", _("No")), + ) + + def queryset(self, request, queryset): + logger.debug(self.value()) + if self.value() == "1": + return queryset.filter(domain_info__is_election_board=True) + if self.value() == "0": + return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None)) + inlines = [DomainInformationInline] # Columns @@ -1053,7 +1092,7 @@ class DomainAdmin(ListHeaderAdmin): "federal_type", "federal_agency", "organization_name", - "is_election_board", + "custom_election_board", "city", "state_territory", "state", @@ -1094,11 +1133,11 @@ class DomainAdmin(ListHeaderAdmin): organization_name.admin_order_field = "domain_info__organization_name" # type: ignore - def is_election_board(self, obj): - return obj.domain_info.is_election_board if obj.domain_info else False + def custom_election_board(self, obj): + return "Yes" if obj.domain_info.is_election_board else "No" - is_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore - is_election_board.short_description = "Election office" # type: ignore + custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore + custom_election_board.short_description = "Election office" # type: ignore def city(self, obj): return obj.domain_info.city if obj.domain_info else None @@ -1111,7 +1150,7 @@ class DomainAdmin(ListHeaderAdmin): state_territory.admin_order_field = "domain_info__state_territory" # type: ignore # Filters - list_filter = ["domain_info__organization_type", "state", "domain_info__is_election_board", "domain_info__federal_type"] + list_filter = ["domain_info__organization_type", "state", "domain_info__federal_type", ElectionOfficeFilter] search_fields = ["name"] search_help_text = "Search by domain name." diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html index 4a58a4b7e..479b7b1ff 100644 --- a/src/registrar/templates/admin/change_list.html +++ b/src/registrar/templates/admin/change_list.html @@ -15,7 +15,15 @@ {% if filters %} filtered by {% for filter_param in filters %} - {{ filter_param.parameter_name }} = {{ filter_param.parameter_value }} + {% if filter_param.parameter_name == 'is_election_board' %} + {%if filter_param.parameter_value == '0' %} + election office = No + {% else %} + election office = Yes + {% endif %} + {% else %} + {{ filter_param.parameter_name }} = {{ filter_param.parameter_value }} + {% endif %} {% if not forloop.last %}, {% endif %} {% endfor %} {% endif %} diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 1b133a052..9a46c58aa 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -847,7 +847,13 @@ class TestDomainApplicationAdmin(MockEppLib): # Grab the current list of table filters readonly_fields = self.admin.get_list_filter(request) - expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) + expected_fields = ( + "status", + "organization_type", + "federal_type", + DomainApplicationAdmin.ElectionOfficeFilter, + DomainApplicationAdmin.InvestigatorFilter, + ) self.assertEqual(readonly_fields, expected_fields) From 9f7db1630c7d981da65adc5bc5cbe552afdc53b9 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 26 Feb 2024 14:38:29 -0500 Subject: [PATCH 23/29] fixed case where domain_info is None --- src/registrar/admin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ddfce2c7a..1e6ff0b21 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1134,7 +1134,10 @@ class DomainAdmin(ListHeaderAdmin): organization_name.admin_order_field = "domain_info__organization_name" # type: ignore def custom_election_board(self, obj): - return "Yes" if obj.domain_info.is_election_board else "No" + domain_info = getattr(obj, 'domain_info', None) + if domain_info: + return "Yes" if domain_info.is_election_board else "No" + return "No" custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore custom_election_board.short_description = "Election office" # type: ignore From 45b8ca358acb6d675bc15cdbb95a6079361d54a8 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 26 Feb 2024 15:14:20 -0500 Subject: [PATCH 24/29] updated merge --- src/registrar/admin.py | 1 + src/registrar/tests/test_admin.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 151305580..808433100 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -24,6 +24,7 @@ from auditlog.admin import LogEntryAdmin # type: ignore from django_fsm import TransitionNotAllowed # type: ignore from django.utils.safestring import mark_safe from django.utils.html import escape +from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index c1f357554..17e8c171f 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1186,13 +1186,13 @@ class TestDomainApplicationAdmin(MockEppLib): self.client.login(username="staffuser", password=p) request = RequestFactory().get("/") - # These names have metadata embedded in them. :investigator implicitly tests if - # these are actually from the attribute "investigator". - expected_list = [ - "AGuy AGuy last_name:investigator", - "FinalGuy FinalGuy last_name:investigator", - "SomeGuy first_name:investigator SomeGuy last_name:investigator", - ] + # These names have metadata embedded in them. :investigator implicitly tests if + # these are actually from the attribute "investigator". + expected_list = [ + "AGuy AGuy last_name:investigator", + "FinalGuy FinalGuy last_name:investigator", + "SomeGuy first_name:investigator SomeGuy last_name:investigator", + ] # Get the actual sorted list of investigators from the lookups method actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] From ce7304d6ac81bf3c34b7d2ea6e375f25600a972c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 26 Feb 2024 15:26:26 -0500 Subject: [PATCH 25/29] fixed linting --- src/registrar/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 808433100..ec07c5f36 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1225,9 +1225,9 @@ class DomainAdmin(ListHeaderAdmin): organization_name.admin_order_field = "domain_info__organization_name" # type: ignore def custom_election_board(self, obj): - domain_info = getattr(obj, 'domain_info', None) + domain_info = getattr(obj, "domain_info", None) if domain_info: - return "Yes" if domain_info.is_election_board else "No" + return "Yes" if domain_info.is_election_board else "No" return "No" custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore From d860bbdb8b5714296305d0091c16bdeeac901213 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 26 Feb 2024 17:28:29 -0500 Subject: [PATCH 26/29] Tweak email templates, model copy --- ...0070_domainapplication_rejection_reason.py | 22 +++++++------------ src/registrar/models/domain_application.py | 18 +++++++-------- .../emails/status_change_rejected.txt | 12 +++++----- src/registrar/tests/test_admin.py | 4 ++-- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/registrar/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py index 60dc8ac73..d559973e2 100644 --- a/src/registrar/migrations/0070_domainapplication_rejection_reason.py +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-02-16 19:36 +# Generated by Django 4.2.7 on 2024-02-26 22:12 from django.db import migrations, models @@ -15,19 +15,13 @@ class Migration(migrations.Migration): field=models.TextField( blank=True, choices=[ - ("domain_purpose", "Domain purpose requirements not met"), - ("requestor", "Requestor isn't authorized to make the request"), - ( - "second_domain_reasoning", - "Organization already has a domain and does not provide sufficient reasoning for a second domain", - ), - ( - "contacts_or_organization_legitimacy", - "Research could not corroborate legitimacy of contacts or organization", - ), - ("organization_eligibility", "Organization isn't eligible for a .gov"), - ("naming_requirements", "Naming requirements not met"), - ("other", "Other"), + ("purpose_not_met", "Purpose requirements not met"), + ("requestor_not_eligible", "Requestor not eligible to make request"), + ("org_has_domain", "Org already has a .gov domain"), + ("contacts_not_verified", "Org contacts couldn't be verified"), + ("org_not_eligible", "Org not eligible for a .gov domain"), + ("naming_not_met", "Naming requirements not met"), + ("other", "Other/Unspecified"), ], null=True, ), diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 11b456337..1d0a9b332 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -352,19 +352,19 @@ class DomainApplication(TimeStampedModel): AGENCY_CHOICES = [(v, v) for v in AGENCIES] class RejectionReasons(models.TextChoices): - DOMAIN_PURPOSE = "domain_purpose", "Domain purpose requirements not met" - REQUESTOR = "requestor", "Requestor isn't authorized to make the request" + DOMAIN_PURPOSE = "purpose_not_met", "Purpose requirements not met" + REQUESTOR = "requestor_not_eligible", "Requestor not eligible to make request" SECOND_DOMAIN_REASONING = ( - "second_domain_reasoning", - "Organization already has a domain and does not provide sufficient reasoning for a second domain", + "org_has_domain", + "Org already has a .gov domain", ) CONTACTS_OR_ORGANIZATION_LEGITIMACY = ( - "contacts_or_organization_legitimacy", - "Research could not corroborate legitimacy of contacts or organization", + "contacts_not_verified", + "Org contacts couldn't be verified", ) - ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" - NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met" - OTHER = "other", "Other" + ORGANIZATION_ELIGIBILITY = "org_not_eligible", "Org not eligible for a .gov domain" + NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met" + OTHER = "other", "Other/Unspecified" # #### Internal fields about the application ##### status = FSMField( diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index b9c89be07..0ece4b8ed 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -9,7 +9,7 @@ STATUS: Rejected ---------------------------------------------------------------- {% if application.rejection_reason != 'other' %} -REJECTION REASON{% endif %}{% if application.rejection_reason == 'domain_purpose' %} +REJECTION REASON{% endif %}{% if application.rejection_reason == 'purpose_not_met' %} Your domain request was rejected because the purpose you provided did not meet our requirements. You didn’t provide enough information about how you intend to use the domain. @@ -18,7 +18,7 @@ Learn more about: - Eligibility for a .gov domain - What you can and can’t do with .gov domains -If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'requestor' %} +If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'requestor_not_eligible' %} Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be working on behalf of a government organization, to request a .gov domain. @@ -26,7 +26,7 @@ working on behalf of a government organization, to request a .gov domain. DEMONSTRATE ELIGIBILITY If you can provide more information that demonstrates your eligibility, or you want to -discuss further, reply to this email.{% elif application.rejection_reason == 'second_domain_reasoning' %} +discuss further, reply to this email.{% elif application.rejection_reason == 'org_has_domain' %} Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our practice is to approve one domain per online service per government organization. We evaluate additional requests on a case-by-case basis. You did not provide sufficient @@ -35,9 +35,9 @@ justification for an additional domain. Read more about our practice of approving one domain per online service . -If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'contacts_or_organization_legitimacy' %} +If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'contacts_not_verified' %} Your domain request was rejected because we could not verify the organizational -contacts you provided. If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'organization_eligibility' %} +contacts you provided. If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'org_not_eligible' %} Your domain request was rejected because we determined that {{ application.organization_name }} is not eligible for a .gov domain. .Gov domains are only available to official U.S.-based government organizations. @@ -48,7 +48,7 @@ If you can provide documentation that demonstrates your eligibility, reply to th This can include links to (or copies of) your authorizing legislation, your founding charter or bylaws, or other similar documentation. Without this, we can’t approve a .gov domain for your organization. Learn more about eligibility for .gov domains -.{% elif application.rejection_reason == 'naming_requirements' %} +.{% elif application.rejection_reason == 'naming_not_met' %} Your domain request was rejected because it does not meet our naming requirements. Domains should uniquely identify a government organization and be clear to the general public. Learn more about naming requirements for your type of organization diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f88721649..7bef239fc 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -784,7 +784,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - def test_save_model_sends_rejected_email_domain_purpose(self): + def test_save_model_sends_rejected_email_purpose_not_met(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is domain purpose.""" @@ -843,7 +843,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - def test_save_model_sends_rejected_email_second_domain_reasoning(self): + def test_save_model_sends_rejected_email_org_has_domain(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is second domain.""" From 3e193b05b039985c3fdbecc43cb8dfce3a5f71c8 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 26 Feb 2024 18:31:28 -0500 Subject: [PATCH 27/29] moved state to the last filter on domains list in admin --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ec07c5f36..a6ea1f09f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1244,7 +1244,7 @@ class DomainAdmin(ListHeaderAdmin): state_territory.admin_order_field = "domain_info__state_territory" # type: ignore # Filters - list_filter = ["domain_info__organization_type", "state", "domain_info__federal_type", ElectionOfficeFilter] + list_filter = ["domain_info__organization_type", "domain_info__federal_type", ElectionOfficeFilter, "state"] search_fields = ["name"] search_help_text = "Search by domain name." From 06c30c2b73642e659d4cf18389f69cc40686e833 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 26 Feb 2024 18:52:34 -0500 Subject: [PATCH 28/29] removed logging noise --- src/registrar/tests/test_models_domain.py | 164 +++++++++++----------- 1 file changed, 84 insertions(+), 80 deletions(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 647d0ff47..3caa1f632 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -321,21 +321,21 @@ class TestDomainCreation(MockEppLib): Then a Domain exists in the database with the same `name` But a domain object does not exist in the registry """ - draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") - user, _ = User.objects.get_or_create() - application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain) + with less_console_noise(): + draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") + user, _ = User.objects.get_or_create() + application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain) - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): # skip using the submit method application.status = DomainApplication.ApplicationStatus.SUBMITTED # transition to approve state application.approve() - # should have information present for this domain - domain = Domain.objects.get(name="igorville.gov") - self.assertTrue(domain) - self.mockedSendFunction.assert_not_called() + # should have information present for this domain + domain = Domain.objects.get(name="igorville.gov") + self.assertTrue(domain) + self.mockedSendFunction.assert_not_called() def test_accessing_domain_properties_creates_domain_in_registry(self): """ @@ -346,33 +346,34 @@ class TestDomainCreation(MockEppLib): And `domain.state` is set to `UNKNOWN` And `domain.is_active()` returns False """ - domain = Domain.objects.create(name="beef-tongue.gov") - # trigger getter - _ = domain.statuses + with less_console_noise(): + domain = Domain.objects.create(name="beef-tongue.gov") + # trigger getter + _ = domain.statuses - # contacts = PublicContact.objects.filter(domain=domain, - # type=PublicContact.ContactTypeChoices.REGISTRANT).get() + # contacts = PublicContact.objects.filter(domain=domain, + # type=PublicContact.ContactTypeChoices.REGISTRANT).get() - # Called in _fetch_cache - self.mockedSendFunction.assert_has_calls( - [ - # TODO: due to complexity of the test, will return to it in - # a future ticket - # call( - # commands.CreateDomain(name="beef-tongue.gov", - # id=contact.registry_id, auth_info=None), - # cleaned=True, - # ), - call( - commands.InfoDomain(name="beef-tongue.gov", auth_info=None), - cleaned=True, - ), - ], - any_order=False, # Ensure calls are in the specified order - ) + # Called in _fetch_cache + self.mockedSendFunction.assert_has_calls( + [ + # TODO: due to complexity of the test, will return to it in + # a future ticket + # call( + # commands.CreateDomain(name="beef-tongue.gov", + # id=contact.registry_id, auth_info=None), + # cleaned=True, + # ), + call( + commands.InfoDomain(name="beef-tongue.gov", auth_info=None), + cleaned=True, + ), + ], + any_order=False, # Ensure calls are in the specified order + ) - self.assertEqual(domain.state, Domain.State.UNKNOWN) - self.assertEqual(domain.is_active(), False) + self.assertEqual(domain.state, Domain.State.UNKNOWN) + self.assertEqual(domain.is_active(), False) @skip("assertion broken with mock addition") def test_empty_domain_creation(self): @@ -382,7 +383,8 @@ class TestDomainCreation(MockEppLib): def test_minimal_creation(self): """Can create with just a name.""" - Domain.objects.create(name="igorville.gov") + with less_console_noise(): + Domain.objects.create(name="igorville.gov") @skip("assertion broken with mock addition") def test_duplicate_creation(self): @@ -503,24 +505,24 @@ class TestDomainAvailable(MockEppLib): return MagicMock( res_data=[responses.check.CheckDomainResultData(name="available.gov", avail=True, reason=None)], ) + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - available = Domain.available("available.gov") - mocked_send.assert_has_calls( - [ - call( - commands.CheckDomain( - ["available.gov"], - ), - cleaned=True, - ) - ] - ) - self.assertTrue(available) - patcher.stop() + available = Domain.available("available.gov") + mocked_send.assert_has_calls( + [ + call( + commands.CheckDomain( + ["available.gov"], + ), + cleaned=True, + ) + ] + ) + self.assertTrue(available) + patcher.stop() def test_domain_unavailable(self): """ @@ -536,24 +538,24 @@ class TestDomainAvailable(MockEppLib): return MagicMock( res_data=[responses.check.CheckDomainResultData(name="unavailable.gov", avail=False, reason="In Use")], ) + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - available = Domain.available("unavailable.gov") - mocked_send.assert_has_calls( - [ - call( - commands.CheckDomain( - ["unavailable.gov"], - ), - cleaned=True, - ) - ] - ) - self.assertFalse(available) - patcher.stop() + available = Domain.available("unavailable.gov") + mocked_send.assert_has_calls( + [ + call( + commands.CheckDomain( + ["unavailable.gov"], + ), + cleaned=True, + ) + ] + ) + self.assertFalse(available) + patcher.stop() def test_domain_available_with_invalid_error(self): """ @@ -562,8 +564,9 @@ class TestDomainAvailable(MockEppLib): Validate InvalidDomainError is raised """ - with self.assertRaises(errors.InvalidDomainError): - Domain.available("invalid-string") + with less_console_noise(): + with self.assertRaises(errors.InvalidDomainError): + Domain.available("invalid-string") def test_domain_available_with_empty_string(self): """ @@ -572,8 +575,9 @@ class TestDomainAvailable(MockEppLib): Validate InvalidDomainError is raised """ - with self.assertRaises(errors.InvalidDomainError): - Domain.available("") + with less_console_noise(): + with self.assertRaises(errors.InvalidDomainError): + Domain.available("") def test_domain_available_unsuccessful(self): """ @@ -584,14 +588,14 @@ class TestDomainAvailable(MockEppLib): def side_effect(_request, cleaned): raise RegistryError(code=ErrorCode.COMMAND_SYNTAX_ERROR) + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - with self.assertRaises(RegistryError): - Domain.available("raises-error.gov") - patcher.stop() + with self.assertRaises(RegistryError): + Domain.available("raises-error.gov") + patcher.stop() class TestRegistrantContacts(MockEppLib): From bd71317da32852d12072aefd005fd6a358f1da7e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 26 Feb 2024 19:32:35 -0500 Subject: [PATCH 29/29] lint and fixing merge problems --- src/registrar/admin.py | 9 ++- src/registrar/tests/test_admin.py | 81 ----------------------- src/registrar/tests/test_models_domain.py | 3 + 3 files changed, 11 insertions(+), 82 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ed0a671c7..86234431d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -902,7 +902,14 @@ class DomainApplicationAdmin(ListHeaderAdmin): custom_election_board.short_description = "Election office" # type: ignore # Filters - list_filter = ("status", "organization_type", "federal_type", ElectionOfficeFilter, "rejection_reason", InvestigatorFilter) + list_filter = ( + "status", + "organization_type", + "federal_type", + ElectionOfficeFilter, + "rejection_reason", + InvestigatorFilter, + ) # Search search_fields = [ diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index d97631ab8..d76f12f35 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1223,87 +1223,6 @@ class TestDomainApplicationAdmin(MockEppLib): # Assert that the status has not changed self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) - def test_change_view_with_restricted_creator(self): - with less_console_noise(): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.creator.status = User.RESTRICTED - application.creator.save() - - with patch("django.contrib.messages.warning") as mock_warning: - # Create a request object with a superuser - request = self.factory.get("/admin/your_app/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser - - self.admin.display_restricted_warning(request, application) - - # Assert that the error message was called with the correct argument - mock_warning.assert_called_once_with( - request, - "Cannot edit an application with a restricted creator.", - ) - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_analyst(self): - with less_console_noise(): - request = self.factory.get("/") # Use the correct method and path - request.user = self.staffuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [ - "creator", - "about_your_organization", - "requested_domain", - "approved_domain", - "alternative_domains", - "purpose", - "submitter", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_superuser(self): - with less_console_noise(): - request = self.factory.get("/") # Use the correct method and path - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [] - - self.assertEqual(readonly_fields, expected_fields) - - def test_saving_when_restricted_creator(self): - with less_console_noise(): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.creator.status = User.RESTRICTED - application.creator.save() - - # Create a request object with a superuser - request = self.factory.get("/") - request.user = self.superuser - - with patch("django.contrib.messages.error") as mock_error: - # Simulate saving the model - self.admin.save_model(request, application, None, False) - - # Assert that the error message was called with the correct argument - mock_error.assert_called_once_with( - request, - "This action is not permitted for applications with a restricted creator.", - ) - - # Assert that the status has not changed - self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) - def test_change_view_with_restricted_creator(self): with less_console_noise(): # Create an instance of the model diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 3caa1f632..d99eaa25c 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -505,6 +505,7 @@ class TestDomainAvailable(MockEppLib): return MagicMock( res_data=[responses.check.CheckDomainResultData(name="available.gov", avail=True, reason=None)], ) + with less_console_noise(): patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() @@ -538,6 +539,7 @@ class TestDomainAvailable(MockEppLib): return MagicMock( res_data=[responses.check.CheckDomainResultData(name="unavailable.gov", avail=False, reason="In Use")], ) + with less_console_noise(): patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() @@ -588,6 +590,7 @@ class TestDomainAvailable(MockEppLib): def side_effect(_request, cleaned): raise RegistryError(code=ErrorCode.COMMAND_SYNTAX_ERROR) + with less_console_noise(): patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start()