diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8734efe9f..7003affe4 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -872,7 +872,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): ] # Filters - list_filter = ("status", "organization_type", InvestigatorFilter) + list_filter = ("status", "organization_type", "rejection_reason", InvestigatorFilter) # Search search_fields = [ @@ -886,7 +886,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", { @@ -987,6 +987,23 @@ 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 29aa9ce03..fc6af408a 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -339,3 +339,46 @@ 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 (){ + let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') + + if (rejectionReasonFormGroup) { + let statusSelect = document.getElementById('id_status') + + // Initial handling of rejectionReasonFormGroup display + if (statusSelect.value != 'rejected') + rejectionReasonFormGroup.style.display = 'none'; + + // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage + statusSelect.addEventListener('change', function() { + 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 + + // 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") { + 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/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py new file mode 100644 index 000000000..d559973e2 --- /dev/null +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.7 on 2024-02-26 22:12 + +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=[ + ("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 293e1a39d..1d0a9b332 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -351,12 +351,34 @@ class DomainApplication(TimeStampedModel): ] AGENCY_CHOICES = [(v, v) for v in AGENCIES] + class RejectionReasons(models.TextChoices): + DOMAIN_PURPOSE = "purpose_not_met", "Purpose requirements not met" + REQUESTOR = "requestor_not_eligible", "Requestor not eligible to make request" + SECOND_DOMAIN_REASONING = ( + "org_has_domain", + "Org already has a .gov domain", + ) + CONTACTS_OR_ORGANIZATION_LEGITIMACY = ( + "contacts_not_verified", + "Org contacts couldn't be verified", + ) + 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( 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( @@ -364,6 +386,7 @@ class DomainApplication(TimeStampedModel): on_delete=models.PROTECT, related_name="applications_created", ) + investigator = models.ForeignKey( "registrar.User", null=True, @@ -688,12 +711,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" @@ -715,12 +743,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" @@ -739,6 +772,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 @@ -761,6 +796,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", diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index c5afaf711..0ece4b8ed 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -8,9 +8,58 @@ REQUEST RECEIVED ON: {{ application.submission_date|date }} STATUS: Rejected ---------------------------------------------------------------- +{% if application.rejection_reason != 'other' %} +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. + +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_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. + + +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 == '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 +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_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 == '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. + + +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.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 +. + 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. +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 @@ -19,7 +68,7 @@ Learn more about: 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 .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 5ba730b80..7bef239fc 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -445,40 +445,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 @@ -502,92 +505,96 @@ 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): + 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): @@ -595,8 +602,9 @@ 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) @@ -607,20 +615,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) if test_that_no_bcc: _ = "" @@ -643,48 +652,49 @@ class TestDomainApplicationAdmin(MockEppLib): 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" - 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, True) - 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, 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 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) + # 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) @override_settings(IS_PRODUCTION=True) def test_save_model_sends_submitted_email_with_bcc_on_prod(self): @@ -697,351 +707,597 @@ class TestDomainApplicationAdmin(MockEppLib): 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() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - BCC_EMAIL = settings.DEFAULT_FROM_EMAIL + BCC_EMAIL = settings.DEFAULT_FROM_EMAIL - # 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, False, BCC_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, False, BCC_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.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL) - 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.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 - 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, + 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(self): - """When transitioning to rejected on a domain request, - an email is sent out every time.""" + 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.""" - # 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) + # 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 - 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) - # 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.""" + + 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 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 \n.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_org_has_domain(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is second domain.""" + + 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 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.""" + + 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 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) + + 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.""" + + 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 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.""" + + 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.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_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. + + The transition fails. + """ + + with less_console_noise(): + 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. + """ + + with less_console_noise(): + 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_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", - "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", + ] - def trigger_saving_approved_to_another_state(self, domain_is_active, another_state): + 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, with an associated domain that can be either active (READY) or not. 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 + application.rejection_reason = rejection_reason - # 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.admin.save_model(request, application, None, True) - self.assertEqual(application.approved_domain, None) + # Assert that the error message was called with the correct argument + if domain_is_active: + messages.error.assert_called_once_with( + request, + "This action is not permitted. The domain " + "is already active.", + ) + else: + # Assert that the error message was never called + messages.error.assert_not_called() - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() + self.assertEqual(application.approved_domain, None) - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() + # Assert that Domain got Deleted + with self.assertRaises(Domain.DoesNotExist): + domain.refresh_from_db() + + # Assert that DomainInformation got Deleted + with self.assertRaises(DomainInformation.DoesNotExist): + domain_information.refresh_from_db() def test_error_when_saving_approved_to_in_review_and_domain_is_active(self): self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW) @@ -1062,7 +1318,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) + 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) @@ -1074,14 +1334,20 @@ 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", + "rejection_reason", + DomainApplicationAdmin.InvestigatorFilter, + ) - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_table_sorted_alphabetically(self): """ @@ -1093,23 +1359,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): """ @@ -1125,37 +1392,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): """ @@ -1168,83 +1436,86 @@ 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 - sorted_fields = ["first_name", "last_name", "email"] - expected_dropdown = list(User.objects.filter(is_staff=True).order_by(*sorted_fields)) + # Get the actual field from the model's meta information + investigator_field = DomainApplication._meta.get_field("investigator") - # Grab the current dropdown. We do an API call to autocomplete to get this info. - application_queryset = self.admin.formfield_for_foreignkey(investigator_field, request).queryset - user_request = self.factory.post( - "/admin/autocomplete/?app_label=registrar&model_name=domainapplication&field_name=investigator" - ) - user_admin = MyUserAdmin(User, self.site) - user_queryset = user_admin.get_search_results(user_request, application_queryset, None)[0] - current_dropdown = list(user_queryset) + # We should only be displaying staff users, in alphabetical order + sorted_fields = ["first_name", "last_name", "email"] + expected_dropdown = list(User.objects.filter(is_staff=True).order_by(*sorted_fields)) - self.assertEqual(expected_dropdown, current_dropdown) + # Grab the current dropdown. We do an API call to autocomplete to get this info. + application_queryset = self.admin.formfield_for_foreignkey(investigator_field, request).queryset + user_request = self.factory.post( + "/admin/autocomplete/?app_label=registrar&model_name=domainapplication&field_name=investigator" + ) + user_admin = MyUserAdmin(User, self.site) + user_queryset = user_admin.get_search_results(user_request, application_queryset, None)[0] + current_dropdown = list(user_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("/") - # 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)] + # 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() @@ -1273,28 +1544,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): @@ -1350,50 +1622,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): @@ -1420,109 +1695,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): @@ -1604,39 +1883,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() @@ -1695,34 +1977,35 @@ class AuditedAdminTest(TestCase): # This test case should be refactored in general, as it is too overly specific and engineered 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: - with self.subTest(field=field): - 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. + 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: + with self.subTest(field=field): + 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) @@ -1752,105 +2035,107 @@ class AuditedAdminTest(TestCase): ) 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") + 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)) + # Create a mock request + request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) - model_admin = AuditedAdmin(DomainInformation, self.site) + 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) + 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 = [] + # 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 + # 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) + 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), - ) + 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] + with less_console_noise(): + tested_fields = [DomainInvitation.domain.field] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("invitation") + # 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)) + # Create a mock request + request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) - model_admin = AuditedAdmin(DomainInvitation, self.site) + 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. + 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) + 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 = [] + # 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 + # 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), - ) + 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""" @@ -1884,93 +2169,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""" @@ -2003,63 +2293,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. @@ -2109,20 +2402,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) 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():