From 40cc30a98849863e5e981b86ec84facd996d121f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 11:17:51 -0500 Subject: [PATCH 01/51] 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/51] 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/51] 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 60834894882dd47271a6f947d2de79cc66e9bd69 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:27:54 -0700 Subject: [PATCH 04/51] Add autocompelte fields --- src/registrar/admin.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index f1680c76a..67f93a376 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -758,6 +758,14 @@ class DomainInformationAdmin(ListHeaderAdmin): # to activate the edit/delete/view buttons filter_horizontal = ("other_contacts",) + autocomplete_fields = [ + "creator", + "domain_application", + "authorizing_official", + "domain", + "submitter", + ] + # Table ordering ordering = ["domain__name"] @@ -1129,6 +1137,13 @@ class DomainAdmin(ListHeaderAdmin): ), ) + autocomplete_fields = [ + "creator", + "domain_application", + "authorizing_official", + "submitter", + ] + # this ordering effects the ordering of results # in autocomplete_fields for domain ordering = ["name"] From 4a3a72cfdd38945acf5c899be6384fff2240c40c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 12:41:15 -0500 Subject: [PATCH 05/51] 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 d72a3fc7d035915a617c247106e73566c578ce23 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:36:05 -0700 Subject: [PATCH 06/51] Update admin.py --- 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 67f93a376..abb9832bc 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1138,10 +1138,10 @@ class DomainAdmin(ListHeaderAdmin): ) autocomplete_fields = [ - "creator", + "domain_information__creator", "domain_application", "authorizing_official", - "submitter", + "domain_information__submitter", ] # this ordering effects the ordering of results From 7b227a26bb8dc0a4297cd63c1bb7c19ea34e569b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:48:40 -0700 Subject: [PATCH 07/51] Update admin.py --- src/registrar/admin.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index abb9832bc..9da8fc855 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1137,13 +1137,6 @@ class DomainAdmin(ListHeaderAdmin): ), ) - autocomplete_fields = [ - "domain_information__creator", - "domain_application", - "authorizing_official", - "domain_information__submitter", - ] - # this ordering effects the ordering of results # in autocomplete_fields for domain ordering = ["name"] From 2aeb34c9eb4f8dae0f4c6831e2572e04f10e7749 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:58:22 -0700 Subject: [PATCH 08/51] Fix bug with autocomplete on domains --- src/registrar/admin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 9da8fc855..36d6b4fd9 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1088,6 +1088,14 @@ class DomainInformationInline(admin.StackedInline): # to activate the edit/delete/view buttons filter_horizontal = ("other_contacts",) + autocomplete_fields = [ + "creator", + "domain_application", + "authorizing_official", + "domain", + "submitter", + ] + def formfield_for_manytomany(self, db_field, request, **kwargs): """customize the behavior of formfields with manytomany relationships. the customized behavior includes sorting of objects in lists as well as customizing helper text""" From 1530d77ad8716fa503c1506ca879713b72a7b932 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 14:37:19 -0500 Subject: [PATCH 09/51] 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 448fc6ed16ad7d4f45557db0a78afebeb4cfff5c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 16 Feb 2024 12:01:44 -0800 Subject: [PATCH 10/51] Update logic for making sure we clear out ip address field --- src/registrar/models/domain.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 7ebe3dc34..dca0693c2 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -427,7 +427,6 @@ class Domain(TimeStampedModel, DomainHelper): raise NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=nameserver) elif cls.isSubdomain(name, nameserver) and (ip is None or ip == []): raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) - elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != []): raise NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip) elif ip is not None and ip != []: @@ -1778,6 +1777,10 @@ class Domain(TimeStampedModel, DomainHelper): for cleaned_host in cleaned_hosts: # Check if the cleaned_host already exists host_in_db, host_created = Host.objects.get_or_create(domain=self, name=cleaned_host["name"]) + # Check if the nameserver is a subdomain of the current domain + # If it is NOT a subdomain, we remove the IP address + if not Domain.isSubdomain(self.name, cleaned_host["name"]): + cleaned_host["addrs"] = [] # or None # Get cleaned list of ips for update cleaned_ips = cleaned_host["addrs"] if not host_created: From c0fc1bc06f8df562da730f9674c60de420cbecdc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 15:16:27 -0500 Subject: [PATCH 11/51] 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 12/51] 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 13/51] 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 14/51] 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 15/51] 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 16/51] 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 17/51] 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 7e1c1f419ed01eee827944035cd1eaa04883ecc5 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 16 Feb 2024 16:18:17 -0800 Subject: [PATCH 18/51] Fix tests --- src/registrar/tests/common.py | 14 ++++++ src/registrar/tests/test_models_domain.py | 57 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 2865bf5c5..336740c02 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -691,6 +691,19 @@ class MockEppLib(TestCase): ], ex_date=datetime.date(2023, 5, 25), ) + + mockDataInfoDomainSubdomain = fakedEppObject( + "fakePw", + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), + contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], + hosts=["fake.meoward.gov"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ex_date=datetime.date(2023, 5, 25), + ) + mockDataExtensionDomain = fakedEppObject( "fakePw", cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), @@ -1083,6 +1096,7 @@ class MockEppLib(TestCase): "adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None), "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None), "justnameserver.com": (self.justNameserver, None), + "meoward.gov": (self.mockDataInfoDomainSubdomain, None), } # Retrieve the corresponding values from the dictionary diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 1c4d2521e..f4dea616b 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -96,7 +96,7 @@ class TestDomainCache(MockEppLib): self.mockedSendFunction.assert_has_calls(expectedCalls) - def test_cache_nested_elements(self): + def test_cache_nested_elements_not_subdomain(self): """Cache works correctly with the nested objects cache and hosts""" with less_console_noise(): domain, _ = Domain.objects.get_or_create(name="igorville.gov") @@ -113,7 +113,7 @@ class TestDomainCache(MockEppLib): } expectedHostsDict = { "name": self.mockDataInfoDomain.hosts[0], - "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], + "addrs": [], # hould return empty bc fake.host.com is not a subdomain of igorville.gov "cr_date": self.mockDataInfoHosts.cr_date, } @@ -138,6 +138,59 @@ class TestDomainCache(MockEppLib): # invalidate cache domain._cache = {} + # get host + domain._get_property("hosts") + # Should return empty bc fake.host.com is not a subdomain of igorville.gov + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + + # get contacts + domain._get_property("contacts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) + + def test_cache_nested_elements_is_subdomain(self): + """Cache works correctly with the nested objects cache and hosts""" + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="meoward.gov") + + # The contact list will initially contain objects of type 'DomainContact' + # this is then transformed into PublicContact, and cache should NOT + # hold onto the DomainContact object + expectedUnfurledContactsList = [ + common.DomainContact(contact="123", type="security"), + ] + expectedContactsDict = { + PublicContact.ContactTypeChoices.ADMINISTRATIVE: None, + PublicContact.ContactTypeChoices.SECURITY: "123", + PublicContact.ContactTypeChoices.TECHNICAL: None, + } + expectedHostsDict = { + "name": self.mockDataInfoDomainSubdomain.hosts[0], + "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], + "cr_date": self.mockDataInfoHosts.cr_date, + } + + # this can be changed when the getter for contacts is implemented + domain._get_property("contacts") + + # check domain info is still correct and not overridden + self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomainSubdomain.auth_info) + self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomainSubdomain.cr_date) + + # check contacts + self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomainSubdomain.contacts) + # The contact list should not contain what is sent by the registry by default, + # as _fetch_cache will transform the type to PublicContact + self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) + + # get and check hosts is set correctly + domain._get_property("hosts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) + # invalidate cache + domain._cache = {} + # get host domain._get_property("hosts") self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) From 5b2ca61de64589548810ef1bbd045172820a78a6 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 20 Feb 2024 15:30:54 -0500 Subject: [PATCH 19/51] 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 20/51] 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 21/51] 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 22/51] 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 23/51] 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 a14a32159c79ce9bb72a121e6646f7d5c633c32e Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 20 Feb 2024 18:04:13 -0800 Subject: [PATCH 24/51] Add more tests in for caching, still WIP --- src/registrar/models/domain.py | 2 + src/registrar/tests/common.py | 79 +++++++++++++++++ src/registrar/tests/test_models_domain.py | 100 ++++++++++++++++++++-- 3 files changed, 173 insertions(+), 8 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index dca0693c2..f86990a5e 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1679,6 +1679,8 @@ class Domain(TimeStampedModel, DomainHelper): try: data_response = self._get_or_create_domain() cache = self._extract_data_from_response(data_response) + # print("!!! cache is") + # print(cache) cleaned = self._clean_cache(cache, data_response) self._update_hosts_and_contacts(cleaned, fetch_hosts, fetch_contacts) if fetch_hosts: diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 336740c02..2419c9687 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -704,6 +704,43 @@ class MockEppLib(TestCase): ex_date=datetime.date(2023, 5, 25), ) + mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject( + "fakePw", + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), + contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], + hosts=["fake.meow.gov"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ex_date=datetime.date(2023, 5, 25), + addrs=[common.Ip(addr="2.0.0.8")], + ) + + mockDataInfoDomainNotSubdomainNoIP = fakedEppObject( + "fakePw", + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), + contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], + hosts=["fake.meow.com"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ex_date=datetime.date(2023, 5, 25), + ) + + mockDataInfoDomainSubdomainNoIP = fakedEppObject( + "fakePw", + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), + contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], + hosts=["fake.subdomainwoip.gov"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ex_date=datetime.date(2023, 5, 25), + ) + mockDataExtensionDomain = fakedEppObject( "fakePw", cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), @@ -841,6 +878,24 @@ class MockEppLib(TestCase): addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")], ) + mockDataInfoHosts1IP = fakedEppObject( + "lastPw", + cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)), + addrs=[common.Ip(addr="2.0.0.8")], + ) + + mockDataInfoHostsNotSubdomainNoIP = fakedEppObject( + "lastPw", + cr_date=make_aware(datetime.datetime(2023, 8, 26, 19, 45, 35)), + addrs=[], + ) + + mockDataInfoHostsSubdomainNoIP = fakedEppObject( + "lastPw", + cr_date=make_aware(datetime.datetime(2023, 8, 27, 19, 45, 35)), + addrs=[], + ) + mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35))) addDsData1 = { "keyTag": 1234, @@ -1002,6 +1057,8 @@ class MockEppLib(TestCase): return self.mockDeleteDomainCommands(_request, cleaned) case commands.RenewDomain: return self.mockRenewDomainCommand(_request, cleaned) + case commands.InfoHost: + return self.mockInfoHostCommmands(_request, cleaned) case _: return MagicMock(res_data=[self.mockDataInfoHosts]) @@ -1016,6 +1073,25 @@ class MockEppLib(TestCase): code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) + # default - mock send + def mockInfoHostCommmands(self, _request, cleaned): + request_name = getattr(_request, "name", None) + + # Define a dictionary to map request names to data and extension values + request_mappings = { + "fake.meow.gov": (self.mockDataInfoHosts1IP, None), # is subdomain and has ip + "fake.meow.com": (self.mockDataInfoHostsNotSubdomainNoIP, None), # not subdomain w no ip + "fake.subdomainwoip.gov": (self.mockDataInfoHostsSubdomainNoIP, None), # subdomain w no ip + } + + # Retrieve the corresponding values from the dictionary + res_data, extensions = request_mappings.get(request_name, (self.mockDataInfoHosts, None)) # default + + return MagicMock( + res_data=[res_data], + extensions=[extensions] if extensions is not None else [], + ) + def mockUpdateHostCommands(self, _request, cleaned): test_ws_ip = common.Ip(addr="1.1. 1.1") addrs_submitted = getattr(_request, "addrs", []) @@ -1097,6 +1173,9 @@ class MockEppLib(TestCase): "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None), "justnameserver.com": (self.justNameserver, None), "meoward.gov": (self.mockDataInfoDomainSubdomain, None), + "meow.gov": (self.mockDataInfoDomainSubdomainAndIPAddress, None), + "fakemeow.gov": (self.mockDataInfoDomainNotSubdomainNoIP, None), + "subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None), } # Retrieve the corresponding values from the dictionary diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index f4dea616b..adde4b410 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1625,7 +1625,65 @@ class TestRegistrantNameservers(MockEppLib): self.assertEqual(nameservers[0][1], ["1.1.1.1"]) patcher.stop() - def test_nameservers_stored_on_fetch_cache(self): + # 1 - is it a subdomain and it has an ip address -- COVERED? + # 2 - is it a subdomain and it doesn't have an ip address + # 3 - no subdomain, it has an ip address -- COVERED + # 4 - no subomdina, doens't have ip address + + def test_nameservers_stored_on_fetch_cache_a_subdomain_with_ip(self): + """ + #1: It is a subdomain, and has an IP address -- referenced by mockDataInfoDomainSubdomainAndIPAddress + fake.meow.com is not a subdomain of fake.gov + """ + with less_console_noise(): + # make the domain + domain, _ = Domain.objects.get_or_create(name="meow.gov", state=Domain.State.READY) + + # mock the get_or_create methods for Host and HostIP + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( + HostIP.objects, "get_or_create" + ) as mock_host_ip_get_or_create: + mock_host_get_or_create.return_value = (Host(domain=domain), True) + mock_host_ip_get_or_create.return_value = (HostIP(), True) + + # force fetch_cache to be called, which will return above documented mocked hosts + domain.nameservers + + # This is never called? + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.gov") + # Retrieve the mocked_host from the return value of the mock + actual_mocked_host, _ = mock_host_get_or_create.return_value + mock_host_ip_get_or_create.assert_called_with(address="2.0.0.8", host=actual_mocked_host) + self.assertEqual(mock_host_ip_get_or_create.call_count, 1) + + def test_nameservers_stored_on_fetch_cache_a_subdomain_without_ip(self): + """ + #2: It is a subdomain, but doesn't has an IP address + """ + with less_console_noise(): + # make the domain + domain, _ = Domain.objects.get_or_create(name="subdomainwoip.gov", state=Domain.State.READY) + + # mock the get_or_create methods for Host and HostIP + # below should do it for mock_host_get_or_create and mock_host_ip_get_or_create right? + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( + HostIP.objects, "get_or_create" + ) as mock_host_ip_get_or_create: + mock_host_get_or_create.return_value = (Host(domain=domain), True) + mock_host_ip_get_or_create.return_value = (HostIP(), True) + + # force fetch_cache to be called, which will return above documented mocked hosts + domain.nameservers + + # This is never called? + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.subdomainwoip.gov") + # Retrieve the mocked_host from the return value of the mock + actual_mocked_host, _ = mock_host_get_or_create.return_value + mock_host_ip_get_or_create.assert_called_with(address="", host=actual_mocked_host) + + self.assertEqual(mock_host_ip_get_or_create.call_count, 1) + + def test_nameservers_stored_on_fetch_cache_not_subdomain_with_ip(self): """ Scenario: Nameservers are stored in db when they are retrieved from fetch_cache. Verify the success of this by asserting get_or_create calls to db. @@ -1633,23 +1691,49 @@ class TestRegistrantNameservers(MockEppLib): of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5 from InfoHost """ + + """ + #3: Not a subdomain, but it has an IP address returned due to how we return our defaults + fake.host.com is not a subdomain of fake.gov + """ with less_console_noise(): domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - # mock the get_or_create methods for Host and HostIP + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( HostIP.objects, "get_or_create" ) as mock_host_ip_get_or_create: - # Set the return value for the mocks - mock_host_get_or_create.return_value = (Host(), True) + mock_host_get_or_create.return_value = (Host(domain=domain), True) mock_host_ip_get_or_create.return_value = (HostIP(), True) + # force fetch_cache to be called, which will return above documented mocked hosts domain.nameservers - # assert that the mocks are called + # # assert that the mocks are called + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com") # Retrieve the mocked_host from the return value of the mock - actual_mocked_host, _ = mock_host_get_or_create.return_value - mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host) - self.assertEqual(mock_host_ip_get_or_create.call_count, 2) + mock_host_ip_get_or_create.assert_not_called() + self.assertEqual(mock_host_ip_get_or_create.call_count, 0) + + def test_nameservers_stored_on_fetch_cache_not_subdomain_without_ip(self): + """ + #4: Not a subdomain and doesn't has an IP address (not pointing to default) + referenced by self.mockDataInfoDomainNotSubdomainNoIP + """ + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="fakemeow.gov", state=Domain.State.READY) + + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( + HostIP.objects, "get_or_create" + ) as mock_host_ip_get_or_create: + mock_host_get_or_create.return_value = (Host(domain=domain), True) + mock_host_ip_get_or_create.return_value = (HostIP(), True) + + # force fetch_cache to be called, which will return above documented mocked hosts + domain.nameservers + # # assert that the mocks are called + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.com") + mock_host_ip_get_or_create.assert_not_called() + self.assertEqual(mock_host_ip_get_or_create.call_count, 0) @skip("not implemented yet") def test_update_is_unsuccessful(self): From 51411f4e3ee2ad34c7bdb29b949729438bc88d7c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 21 Feb 2024 08:57:26 -0800 Subject: [PATCH 25/51] Fix tests for diff scenarios --- src/registrar/models/domain.py | 2 -- src/registrar/tests/test_models_domain.py | 33 +++++++---------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index f86990a5e..dca0693c2 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1679,8 +1679,6 @@ class Domain(TimeStampedModel, DomainHelper): try: data_response = self._get_or_create_domain() cache = self._extract_data_from_response(data_response) - # print("!!! cache is") - # print(cache) cleaned = self._clean_cache(cache, data_response) self._update_hosts_and_contacts(cleaned, fetch_hosts, fetch_contacts) if fetch_hosts: diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index adde4b410..f4047556e 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1625,15 +1625,10 @@ class TestRegistrantNameservers(MockEppLib): self.assertEqual(nameservers[0][1], ["1.1.1.1"]) patcher.stop() - # 1 - is it a subdomain and it has an ip address -- COVERED? - # 2 - is it a subdomain and it doesn't have an ip address - # 3 - no subdomain, it has an ip address -- COVERED - # 4 - no subomdina, doens't have ip address - def test_nameservers_stored_on_fetch_cache_a_subdomain_with_ip(self): """ - #1: It is a subdomain, and has an IP address -- referenced by mockDataInfoDomainSubdomainAndIPAddress - fake.meow.com is not a subdomain of fake.gov + #1: Nameserver is a subdomain, and has an IP address + referenced by mockDataInfoDomainSubdomainAndIPAddress """ with less_console_noise(): # make the domain @@ -1649,7 +1644,6 @@ class TestRegistrantNameservers(MockEppLib): # force fetch_cache to be called, which will return above documented mocked hosts domain.nameservers - # This is never called? mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.gov") # Retrieve the mocked_host from the return value of the mock actual_mocked_host, _ = mock_host_get_or_create.return_value @@ -1658,14 +1652,14 @@ class TestRegistrantNameservers(MockEppLib): def test_nameservers_stored_on_fetch_cache_a_subdomain_without_ip(self): """ - #2: It is a subdomain, but doesn't has an IP address + #2: Nameserver is a subdomain, but doesn't have an IP address associated + referenced by mockDataInfoDomainSubdomainNoIP """ with less_console_noise(): # make the domain domain, _ = Domain.objects.get_or_create(name="subdomainwoip.gov", state=Domain.State.READY) # mock the get_or_create methods for Host and HostIP - # below should do it for mock_host_get_or_create and mock_host_ip_get_or_create right? with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( HostIP.objects, "get_or_create" ) as mock_host_ip_get_or_create: @@ -1675,13 +1669,9 @@ class TestRegistrantNameservers(MockEppLib): # force fetch_cache to be called, which will return above documented mocked hosts domain.nameservers - # This is never called? mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.subdomainwoip.gov") - # Retrieve the mocked_host from the return value of the mock - actual_mocked_host, _ = mock_host_get_or_create.return_value - mock_host_ip_get_or_create.assert_called_with(address="", host=actual_mocked_host) - - self.assertEqual(mock_host_ip_get_or_create.call_count, 1) + mock_host_ip_get_or_create.assert_not_called() + self.assertEqual(mock_host_ip_get_or_create.call_count, 0) def test_nameservers_stored_on_fetch_cache_not_subdomain_with_ip(self): """ @@ -1690,11 +1680,9 @@ class TestRegistrantNameservers(MockEppLib): The mocked data for the EPP calls returns a host name of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5 from InfoHost - """ - """ - #3: Not a subdomain, but it has an IP address returned due to how we return our defaults - fake.host.com is not a subdomain of fake.gov + #3: Nameserver is not a subdomain, but it does have an IP address returned + due to how we set up our defaults """ with less_console_noise(): domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) @@ -1707,16 +1695,14 @@ class TestRegistrantNameservers(MockEppLib): # force fetch_cache to be called, which will return above documented mocked hosts domain.nameservers - # # assert that the mocks are called mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com") - # Retrieve the mocked_host from the return value of the mock mock_host_ip_get_or_create.assert_not_called() self.assertEqual(mock_host_ip_get_or_create.call_count, 0) def test_nameservers_stored_on_fetch_cache_not_subdomain_without_ip(self): """ - #4: Not a subdomain and doesn't has an IP address (not pointing to default) + #4: Nameserver is not a subdomain and doesn't have an associated IP address referenced by self.mockDataInfoDomainNotSubdomainNoIP """ with less_console_noise(): @@ -1730,7 +1716,6 @@ class TestRegistrantNameservers(MockEppLib): # force fetch_cache to be called, which will return above documented mocked hosts domain.nameservers - # # assert that the mocks are called mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.com") mock_host_ip_get_or_create.assert_not_called() self.assertEqual(mock_host_ip_get_or_create.call_count, 0) From 56c253f4788352ba1aaed0ae63d4cb603fa5370b Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 21 Feb 2024 09:56:13 -0800 Subject: [PATCH 26/51] Update test for domain views --- src/registrar/tests/test_views_domain.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 2c8e796ac..bd0fe11a9 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -821,6 +821,7 @@ class TestDomainNameservers(TestDomainOverview): nameserver1 = "ns1.igorville.gov" nameserver2 = "ns2.igorville.gov" valid_ip = "1.1. 1.1" + valid_ip_2 = "2.2. 2.2" # initial nameservers page has one server with two ips # have to throw an error in order to test that the whitespace has been stripped from ip nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) @@ -828,7 +829,8 @@ class TestDomainNameservers(TestDomainOverview): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # attempt to submit the form without one host and an ip with whitespace nameservers_page.form["form-0-server"] = nameserver1 - nameservers_page.form["form-1-ip"] = valid_ip + nameservers_page.form["form-0-ip"] = valid_ip + nameservers_page.form["form-1-ip"] = valid_ip_2 nameservers_page.form["form-1-server"] = nameserver2 with less_console_noise(): # swallow log warning message result = nameservers_page.form.submit() @@ -937,15 +939,15 @@ class TestDomainNameservers(TestDomainOverview): nameserver1 = "ns1.igorville.gov" nameserver2 = "ns2.igorville.gov" valid_ip = "127.0.0.1" + valid_ip_2 = "128.0.0.2" # initial nameservers page has one server with two ips nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-0-ip"] = valid_ip nameservers_page.form["form-1-server"] = nameserver2 - nameservers_page.form["form-1-ip"] = valid_ip + nameservers_page.form["form-1-ip"] = valid_ip_2 with less_console_noise(): # swallow log warning message result = nameservers_page.form.submit() # form submission was a successful post, response should be a 302 From c914c36096ca7558e0b756e6a7d825749d4eb791 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 21 Feb 2024 13:01:36 -0800 Subject: [PATCH 27/51] Testing if sandbox is updating --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index a79065f50..918a0defd 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -13,7 +13,7 @@ {% block messages %} {% include "includes/form_messages.html" %} {% endblock %} -

Manage your domains

+

Manage your domains -- TEST TEST 123

Date: Wed, 21 Feb 2024 13:55:52 -0800 Subject: [PATCH 28/51] Remove extraneous code for deployment testing --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 918a0defd..a79065f50 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -13,7 +13,7 @@ {% block messages %} {% include "includes/form_messages.html" %} {% endblock %} -

Manage your domains -- TEST TEST 123

+

Manage your domains

Date: Wed, 21 Feb 2024 14:45:26 -0800 Subject: [PATCH 29/51] Adjust comments --- src/registrar/tests/test_views_domain.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index bd0fe11a9..59b5faaa9 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -822,7 +822,6 @@ class TestDomainNameservers(TestDomainOverview): nameserver2 = "ns2.igorville.gov" valid_ip = "1.1. 1.1" valid_ip_2 = "2.2. 2.2" - # initial nameservers page has one server with two ips # have to throw an error in order to test that the whitespace has been stripped from ip nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] @@ -940,7 +939,6 @@ class TestDomainNameservers(TestDomainOverview): nameserver2 = "ns2.igorville.gov" valid_ip = "127.0.0.1" valid_ip_2 = "128.0.0.2" - # initial nameservers page has one server with two ips nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) From 1447156c0c03619c6b814d0c737e138fb947df98 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 21 Feb 2024 15:19:11 -0800 Subject: [PATCH 30/51] Just going to use this as a correct deployment holder --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index a79065f50..945f939a4 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -13,7 +13,7 @@ {% block messages %} {% include "includes/form_messages.html" %} {% endblock %} -

Manage your domains

+

Manage your domains - TESTING TESTING 123

Date: Wed, 21 Feb 2024 15:50:55 -0800 Subject: [PATCH 31/51] Remove extra comment --- src/registrar/tests/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 2419c9687..9ce65626a 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1073,7 +1073,6 @@ class MockEppLib(TestCase): code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) - # default - mock send def mockInfoHostCommmands(self, _request, cleaned): request_name = getattr(_request, "name", None) From 65813f448e5eeb923cb9c7ae6b06532b6759bd9d Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Wed, 21 Feb 2024 16:51:57 -0800 Subject: [PATCH 32/51] handle 500 error with empty string --- src/registrar/models/domain.py | 13 ++++++++++-- src/registrar/models/utility/domain_helper.py | 11 +++++----- src/registrar/tests/test_models_domain.py | 20 ++++++++++++++----- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 7ebe3dc34..d7e4a9471 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -13,6 +13,7 @@ from typing import Any from registrar.models.host import Host from registrar.models.host_ip import HostIP from registrar.utility.enums import DefaultEmail +from registrar.utility import errors from registrar.utility.errors import ( ActionNotAllowed, @@ -192,9 +193,17 @@ class Domain(TimeStampedModel, DomainHelper): @classmethod def available(cls, domain: str) -> bool: - """Check if a domain is available.""" + """Check if a domain is available. + This is called by the availablility api and + is called in the validate function on the request/domain page + + throws- RegistryError or InvalidDomainError""" if not cls.string_could_be_domain(domain): - raise ValueError("Not a valid domain: %s" % str(domain)) + logger.warning("Not a valid domain: %s" % str(domain)) + # throw invalid domain error so that it can be caught in + # validate_and_handle_errors in domain_helper + raise errors.InvalidDomainError() + domain_name = domain.lower() req = commands.CheckDomain([domain_name]) return registry.send(req, cleaned=True).res_data[0].avail diff --git a/src/registrar/models/utility/domain_helper.py b/src/registrar/models/utility/domain_helper.py index 9e3559676..8b9391add 100644 --- a/src/registrar/models/utility/domain_helper.py +++ b/src/registrar/models/utility/domain_helper.py @@ -33,11 +33,12 @@ class DomainHelper: # Split into pieces for the linter domain = cls._validate_domain_string(domain, blank_ok) - try: - if not check_domain_available(domain): - raise errors.DomainUnavailableError() - except RegistryError as err: - raise errors.RegistrySystemError() from err + if domain != "": + try: + if not check_domain_available(domain): + raise errors.DomainUnavailableError() + except RegistryError as err: + raise errors.RegistrySystemError() from err return domain @staticmethod diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 1c4d2521e..fbf47a745 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -20,7 +20,7 @@ from registrar.models.user import User from registrar.utility.errors import ActionNotAllowed, NameserverError from registrar.models.utility.contact_error import ContactError, ContactErrorCodes - +from registrar.utility import errors from django_fsm import TransitionNotAllowed # type: ignore from epplibwrapper import ( @@ -502,16 +502,26 @@ class TestDomainAvailable(MockEppLib): self.assertFalse(available) patcher.stop() - def test_domain_available_with_value_error(self): + def test_domain_available_with_invalid_error(self): """ Scenario: Testing whether an invalid domain is available - Should throw ValueError + Should throw InvalidDomainError - Validate ValueError is raised + Validate InvalidDomainError is raised """ - with self.assertRaises(ValueError): + with self.assertRaises(errors.InvalidDomainError()): Domain.available("invalid-string") + def test_domain_available_with_empty_string(self): + """ + Scenario: Testing whether an empty string domain name is available + Should throw InvalidDomainError + + Validate InvalidDomainError is raised + """ + with self.assertRaises(errors.InvalidDomainError()): + Domain.available("") + def test_domain_available_unsuccessful(self): """ Scenario: Testing behavior when registry raises a RegistryError From 68d321143f71d3ff78061a28b210bc85fb340f20 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 21 Feb 2024 18:04:57 -0800 Subject: [PATCH 33/51] Testing redeployment --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 945f939a4..1f4cfc13b 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -13,7 +13,7 @@ {% block messages %} {% include "includes/form_messages.html" %} {% endblock %} -

Manage your domains - TESTING TESTING 123

+

Manage your domains - TESTING TESTING 1234

Date: Wed, 21 Feb 2024 19:03:59 -0800 Subject: [PATCH 34/51] removed () on Exception type --- src/registrar/tests/test_models_domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index fbf47a745..2bd581734 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -509,7 +509,7 @@ class TestDomainAvailable(MockEppLib): Validate InvalidDomainError is raised """ - with self.assertRaises(errors.InvalidDomainError()): + with self.assertRaises(errors.InvalidDomainError): Domain.available("invalid-string") def test_domain_available_with_empty_string(self): @@ -519,7 +519,7 @@ class TestDomainAvailable(MockEppLib): Validate InvalidDomainError is raised """ - with self.assertRaises(errors.InvalidDomainError()): + with self.assertRaises(errors.InvalidDomainError): Domain.available("") def test_domain_available_unsuccessful(self): From 0f12a535fb5e8148d5efbc4901470347012b748f Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 22 Feb 2024 11:44:30 -0800 Subject: [PATCH 35/51] Address feedback and remove head for deployment check --- src/registrar/models/domain.py | 4 ++-- src/registrar/templates/home.html | 2 +- src/registrar/tests/common.py | 3 ++- src/registrar/tests/test_models_domain.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index dca0693c2..500bf63be 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1779,8 +1779,8 @@ class Domain(TimeStampedModel, DomainHelper): host_in_db, host_created = Host.objects.get_or_create(domain=self, name=cleaned_host["name"]) # Check if the nameserver is a subdomain of the current domain # If it is NOT a subdomain, we remove the IP address - if not Domain.isSubdomain(self.name, cleaned_host["name"]): - cleaned_host["addrs"] = [] # or None + if not Domain.isSubdomain(self.name, host_in_db.name): + cleaned_host["addrs"] = [] # Get cleaned list of ips for update cleaned_ips = cleaned_host["addrs"] if not host_created: diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 1f4cfc13b..a79065f50 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -13,7 +13,7 @@ {% block messages %} {% include "includes/form_messages.html" %} {% endblock %} -

Manage your domains - TESTING TESTING 1234

+

Manage your domains

Date: Thu, 22 Feb 2024 11:53:56 -0800 Subject: [PATCH 36/51] Point back to cleaned host --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 500bf63be..7f684e327 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1779,7 +1779,7 @@ class Domain(TimeStampedModel, DomainHelper): host_in_db, host_created = Host.objects.get_or_create(domain=self, name=cleaned_host["name"]) # Check if the nameserver is a subdomain of the current domain # If it is NOT a subdomain, we remove the IP address - if not Domain.isSubdomain(self.name, host_in_db.name): + if not Domain.isSubdomain(self.name, cleaned_host["name"]): cleaned_host["addrs"] = [] # Get cleaned list of ips for update cleaned_ips = cleaned_host["addrs"] From 8af76b78f0a398940f5ecee38b4c33118713ffa2 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 22 Feb 2024 18:47:10 -0500 Subject: [PATCH 37/51] 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 38/51] 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 39/51] 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 40/51] 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 ec44f8fe58e32390d1f580723abd2dc1a1cb083b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 23 Feb 2024 10:45:54 -0700 Subject: [PATCH 41/51] Update admin.py --- src/registrar/admin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index cdd8611f0..0ab6c3022 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1159,7 +1159,14 @@ class DomainAdmin(ListHeaderAdmin): if object_id is not None: domain = Domain.objects.get(pk=object_id) years_to_extend_by = self._get_calculated_years_for_exp_date(domain) - curr_exp_date = domain.registry_expiration_date + + try: + curr_exp_date = domain.registry_expiration_date + except KeyError: + # No expiration date was found. Return none. + extra_context["extended_expiration_date"] = None + return super().changeform_view(request, object_id, form_url, extra_context) + if curr_exp_date < date.today(): extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by) else: From 262acee1cd39d916e34248954ce4f143dd488b87 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 26 Feb 2024 14:03:06 -0500 Subject: [PATCH 42/51] 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 43/51] 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 44/51] 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 45/51] 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 46/51] 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 47/51] 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 48/51] 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 49/51] 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() From 788561f7445c56f611b0cb15bcc394cfb59983a5 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 27 Feb 2024 07:16:58 -0500 Subject: [PATCH 50/51] investigators list in filter shows only distinct values --- 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 86234431d..12c9d67cc 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -844,7 +844,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): "investigator__email", output_field=CharField(), ) - ).values_list("investigator__id", "full_name") + ).values_list("investigator__id", "full_name").distinct() return privileged_users_annotated From 52824c75c6f5ef5196d325027c6c174c6c94bea1 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 27 Feb 2024 07:27:53 -0500 Subject: [PATCH 51/51] formatting for linter --- src/registrar/admin.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 12c9d67cc..0b7553c30 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -838,13 +838,19 @@ class DomainApplicationAdmin(ListHeaderAdmin): ) # Annotate the full name and return a values list that lookups can use - privileged_users_annotated = privileged_users.annotate( - full_name=Coalesce( - Concat("investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()), - "investigator__email", - output_field=CharField(), + privileged_users_annotated = ( + privileged_users.annotate( + full_name=Coalesce( + Concat( + "investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField() + ), + "investigator__email", + output_field=CharField(), + ) ) - ).values_list("investigator__id", "full_name").distinct() + .values_list("investigator__id", "full_name") + .distinct() + ) return privileged_users_annotated