From 1530d77ad8716fa503c1506ca879713b72a7b932 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 14:37:19 -0500 Subject: [PATCH 01/13] add rejection reason to model --- ...0070_domainapplication_rejection_reason.py | 34 +++++++++++++++++++ src/registrar/models/domain_application.py | 16 +++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/registrar/migrations/0070_domainapplication_rejection_reason.py diff --git a/src/registrar/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py new file mode 100644 index 000000000..c49f4d442 --- /dev/null +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.7 on 2024-02-16 19:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0069_alter_contact_email_alter_contact_first_name_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="domainapplication", + name="rejection_reason", + field=models.TextField( + blank=True, + choices=[ + ("domain_purpose", "Domain purpose requirements not met"), + ("requestor", "Requestor isn't authorized to make the request"), + ( + "second_domain_reasoning", + "Organization already has a domain and does not provide sufficient reasoning for a second domain", + ), + ( + "contacts_or_organization_legitimacy", + "Research could not corroborate legitimacy of contacts or organization", + ), + ("organization_eligibility", "Organization isn't eligible for a .gov"), + ("naming_requirements", "naming requirements not met"), + ], + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 17bc71cbe..563c7699e 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -350,12 +350,27 @@ class DomainApplication(TimeStampedModel): ] AGENCY_CHOICES = [(v, v) for v in AGENCIES] + class RejectionReasons(models.TextChoices): + DOMAIN_PURPOSE = "domain_purpose", "Domain purpose requirements not met" + REQUESTOR = "requestor", "Requestor isn't authorized to make the request" + SECOND_DOMAIN_REASONING = "second_domain_reasoning", "Organization already has a domain and does not provide sufficient reasoning for a second domain" + CONTACTS_OR_ORGANIZATION_LEGITIMACY = "contacts_or_organization_legitimacy", "Research could not corroborate legitimacy of contacts or organization" + ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" + NAMING_REQUIREMENTS = "naming_requirements", "naming requirements not met" + # #### Internal fields about the application ##### status = FSMField( choices=ApplicationStatus.choices, # possible states as an array of constants default=ApplicationStatus.STARTED, # sensible default protected=False, # can change state directly, particularly in Django admin ) + + rejection_reason = models.TextField( + choices=RejectionReasons.choices, + null=True, + blank=True, + ) + # This is the application user who created this application. The contact # information that they gave is in the `submitter` field creator = models.ForeignKey( @@ -363,6 +378,7 @@ class DomainApplication(TimeStampedModel): on_delete=models.PROTECT, related_name="applications_created", ) + investigator = models.ForeignKey( "registrar.User", null=True, From c0fc1bc06f8df562da730f9674c60de420cbecdc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 15:16:27 -0500 Subject: [PATCH 02/13] updated text of reject email --- .../emails/status_change_rejected.txt | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index c5afaf711..5d6f01ee7 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -9,17 +9,61 @@ STATUS: Rejected ---------------------------------------------------------------- -YOU CAN SUBMIT A NEW REQUEST -If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request. +REJECTION REASON{% if application.rejection_reason == 'domain_purpose' %} +Your domain request was rejected because the purpose you provided did not meet our +requirements. You didn’t provide enough information about how you intend to use the +domain. Learn more about: - Eligibility for a .gov domain -- Choosing a .gov domain name +- What you can and can’t do with .gov domains + +If you have questions or comments, reply to this email. +{% elif application.status == 'requestor' %} +Your domain request was rejected because we don’t believe you’re eligible to request a +.gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be +working on behalf of a government organization, to request a .gov domain. -NEED ASSISTANCE? -If you have questions about this domain request or need help choosing a new domain name, reply to this email. +DEMONSTRATE ELIGIBILITY +If you can provide more information that demonstrates your eligibility, or you want to +discuss further, reply to this email. +{% elif application.status == 'second_domain_reasoning' %} +Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our +practice is to approve one domain per online service per government organization. We +evaluate additional requests on a case-by-case basis. You did not provide sufficient +justification for an additional domain. +Read more about our practice of approving one domain per online service +. + +If you have questions or comments, reply to this email. +{% elif application.status == 'contacts_or_organization_legitimacy' %} +Your domain request was rejected because we could not verify the organizational +contacts you provided. If you have questions or comments, reply to this email. +{% elif application.status == 'organization_eligibility' %} +Your domain request was rejected because we determined that {{ application.organization_name }} is not +eligible for a .gov domain. .Gov domains are only available to official U.S.-based +government organizations. + + +DEMONSTRATE ELIGIBILITY +If you can provide documentation that demonstrates your eligibility, reply to this email. +This can include links to (or copies of) your authorizing legislation, your founding +charter or bylaws, or other similar documentation. Without this, we can’t approve a +.gov domain for your organization. Learn more about eligibility for .gov domains +. +{% elif application.status == 'naming_requirements' %} +Your domain request was rejected because it does not meet our naming requirements. +Domains should uniquely identify a government organization and be clear to the +general public. Learn more about naming requirements for your type of organization +. + + +YOU CAN SUBMIT A NEW REQUEST +We encourage you to request a domain that meets our requirements. If you have +questions or want to discuss potential domain names, reply to this email. +{% endif %} THANK YOU .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. From ba53e74e26912c61e619d40cfd22f079b5e28f4b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 15:16:42 -0500 Subject: [PATCH 03/13] Add logic in admin to catch no reason error, add JS to manage UI --- src/registrar/admin.py | 20 +++++++++++++++- src/registrar/assets/js/get-gov-admin.js | 24 +++++++++++++++++++ ...0070_domainapplication_rejection_reason.py | 2 +- src/registrar/models/domain_application.py | 17 ++++++++++++- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c5f5be276..8225246b9 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -793,7 +793,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Detail view form = DomainApplicationAdminForm fieldsets = [ - (None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}), + (None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}), ( "Type of organization", { @@ -901,6 +901,24 @@ class DomainApplicationAdmin(ListHeaderAdmin): "This action is not permitted. The domain is already active.", ) + elif ( + obj + and obj.status == models.DomainApplication.ApplicationStatus.REJECTED + and not obj.rejection_reason + ): + # This condition should never be triggered. + # The opposite of this condition is acceptable (rejected -> other status and rejection_reason) + # because we clean up the rejection reason in the transition in the model. + + # Clear the success message + messages.set_level(request, messages.ERROR) + + messages.error( + request, + "A rejection reason is required.", + ) + + else: if obj.status != original_obj.status: status_method_mapping = { diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 866c7bd7d..b5f5a6aaa 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -312,3 +312,27 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, } })(); + +/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request + * status select amd to show/hide the rejection reason +*/ +(function (){ + + // Get the rejection reason form row + let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') + + if (rejectionReasonFormGroup) { + // Get the status select + let statusSelect = document.getElementById('id_status') + + // If status is rejected, hide the rejection reason on load + if (statusSelect.value != 'rejected') + rejectionReasonFormGroup.style.display = 'none'; + + // Listen to status changes and toggle rejection reason + statusSelect.addEventListener('change', function() { + rejectionReasonFormGroup.style.display = statusSelect.value !== 'rejected' ? 'none' : 'block'; + }); + } + +})(); diff --git a/src/registrar/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py index c49f4d442..defeb4d95 100644 --- a/src/registrar/migrations/0070_domainapplication_rejection_reason.py +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -26,7 +26,7 @@ class Migration(migrations.Migration): "Research could not corroborate legitimacy of contacts or organization", ), ("organization_eligibility", "Organization isn't eligible for a .gov"), - ("naming_requirements", "naming requirements not met"), + ("naming_requirements", "Naming requirements not met"), ], null=True, ), diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 563c7699e..3f6fcc99f 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -356,7 +356,7 @@ class DomainApplication(TimeStampedModel): SECOND_DOMAIN_REASONING = "second_domain_reasoning", "Organization already has a domain and does not provide sufficient reasoning for a second domain" CONTACTS_OR_ORGANIZATION_LEGITIMACY = "contacts_or_organization_legitimacy", "Research could not corroborate legitimacy of contacts or organization" ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" - NAMING_REQUIREMENTS = "naming_requirements", "naming requirements not met" + NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met" # #### Internal fields about the application ##### status = FSMField( @@ -694,12 +694,17 @@ class DomainApplication(TimeStampedModel): This action is logged. + This action cleans up the rejection status if moving away from rejected. + As side effects this will delete the domain and domain_information (will cascade) when they exist.""" if self.status == self.ApplicationStatus.APPROVED: self.delete_and_clean_up_domain("in_review") + if self.status == self.ApplicationStatus.REJECTED: + self.rejection_reason = None + literal = DomainApplication.ApplicationStatus.IN_REVIEW # Check if the tuple exists, then grab its value in_review = literal if literal is not None else "In Review" @@ -721,12 +726,17 @@ class DomainApplication(TimeStampedModel): This action is logged. + This action cleans up the rejection status if moving away from rejected. + As side effects this will delete the domain and domain_information (will cascade) when they exist.""" if self.status == self.ApplicationStatus.APPROVED: self.delete_and_clean_up_domain("reject_with_prejudice") + if self.status == self.ApplicationStatus.REJECTED: + self.rejection_reason = None + literal = DomainApplication.ApplicationStatus.ACTION_NEEDED # Check if the tuple is setup correctly, then grab its value action_needed = literal if literal is not None else "Action Needed" @@ -745,6 +755,8 @@ class DomainApplication(TimeStampedModel): def approve(self, send_email=True): """Approve an application that has been submitted. + This action cleans up the rejection status if moving away from rejected. + This has substantial side-effects because it creates another database object for the approved Domain and makes the user who created the application into an admin on that domain. It also triggers an email @@ -767,6 +779,9 @@ class DomainApplication(TimeStampedModel): user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) + if self.status == self.ApplicationStatus.REJECTED: + self.rejection_reason = None + self._send_status_update_email( "application approved", "emails/status_change_approved.txt", From a32b0c03731b6074fd7683d7bc811ed776309711 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 17:14:44 -0500 Subject: [PATCH 04/13] Admin unit tests --- .../emails/status_change_rejected.txt | 10 +- src/registrar/tests/test_admin.py | 154 ++++++++++++++++-- 2 files changed, 144 insertions(+), 20 deletions(-) diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index 5d6f01ee7..4beacf25a 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -19,7 +19,7 @@ Learn more about: - What you can and can’t do with .gov domains If you have questions or comments, reply to this email. -{% elif application.status == 'requestor' %} +{% elif application.rejection_reason == 'requestor' %} Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be working on behalf of a government organization, to request a .gov domain. @@ -28,7 +28,7 @@ working on behalf of a government organization, to request a .gov domain. DEMONSTRATE ELIGIBILITY If you can provide more information that demonstrates your eligibility, or you want to discuss further, reply to this email. -{% elif application.status == 'second_domain_reasoning' %} +{% elif application.rejection_reason == 'second_domain_reasoning' %} Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our practice is to approve one domain per online service per government organization. We evaluate additional requests on a case-by-case basis. You did not provide sufficient @@ -38,10 +38,10 @@ Read more about our practice of approving one domain per online service . If you have questions or comments, reply to this email. -{% elif application.status == 'contacts_or_organization_legitimacy' %} +{% elif application.rejection_reason == 'contacts_or_organization_legitimacy' %} Your domain request was rejected because we could not verify the organizational contacts you provided. If you have questions or comments, reply to this email. -{% elif application.status == 'organization_eligibility' %} +{% elif application.rejection_reason == 'organization_eligibility' %} Your domain request was rejected because we determined that {{ application.organization_name }} is not eligible for a .gov domain. .Gov domains are only available to official U.S.-based government organizations. @@ -53,7 +53,7 @@ This can include links to (or copies of) your authorizing legislation, your foun charter or bylaws, or other similar documentation. Without this, we can’t approve a .gov domain for your organization. Learn more about eligibility for .gov domains . -{% elif application.status == 'naming_requirements' %} +{% elif application.rejection_reason == 'naming_requirements' %} Your domain request was rejected because it does not meet our naming requirements. Domains should uniquely identify a government organization and be clear to the general public. Learn more about naming requirements for your type of organization diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f90b18584..6142c2aee 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -412,7 +412,7 @@ class TestDomainApplicationAdmin(MockEppLib): # Now let's make sure the long description does not exist self.assertNotContains(response, "Federal: an agency of the U.S. government") - def transition_state_and_send_email(self, application, status): + def transition_state_and_send_email(self, application, status, rejection_reason=None): """Helper method for the email test cases.""" with boto3_mocking.clients.handler_for("sesv2", self.mock_client): @@ -420,12 +420,15 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a mock request request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Modify the application's property + # Modify the application's properties application.status = status + application.rejection_reason = rejection_reason # Use the model admin's save_model method self.admin.save_model(request, application, form=None, change=True) + logger.info(f'application.rejection_reason {application.rejection_reason}') + def assert_email_is_accurate(self, expected_string, email_index, email_address): """Helper method for the email test cases. email_index is the index of the email in mock_client.""" @@ -512,7 +515,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) # Test Withdrawn Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.DOMAIN_PURPOSE) self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) @@ -520,9 +523,9 @@ class TestDomainApplicationAdmin(MockEppLib): self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - def test_save_model_sends_rejected_email(self): - """When transitioning to rejected on a domain request, - an email is sent out every time.""" + def test_save_model_sends_rejected_email_domain_purpose(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is domain purpose.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -531,19 +534,137 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a sample application application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Test Submitted Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) - self.assert_email_is_accurate("Your .gov domain request has been rejected.", 0, EMAIL) + # Reject for reason DOMAIN_PURPOSE and test email + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.DOMAIN_PURPOSE) + self.assert_email_is_accurate("Your domain request was rejected because the purpose you provided did not meet our \nrequirements.", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Test Withdrawn Status + # Approve self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - # Test Submitted Status Again (No new email should be sent) - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + def test_save_model_sends_rejected_email_requestor(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is requestor.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Reject for reason REQUESTOR and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR) + self.assert_email_is_accurate("Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain on behalf of Testorg", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_sends_rejected_email_requestor(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is second domain.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.SECOND_DOMAIN_REASONING) + self.assert_email_is_accurate("Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is contacts or org legitimacy.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY) + self.assert_email_is_accurate("Your domain request was rejected because we could not verify the organizational \ncontacts you provided. If you have questions or comments, reply to this email.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_sends_rejected_email_org_eligibility(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is org eligibility.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.ORGANIZATION_ELIGIBILITY) + self.assert_email_is_accurate("Your domain request was rejected because we determined that Testorg is not \neligible for a .gov domain.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_sends_rejected_email_naming(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is naming.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.NAMING_REQUIREMENTS) + self.assert_email_is_accurate("Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_clear_rejected_reason(self): + """When transitioning from rejected on a domain request, + the rejected_reason is cleared.""" + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) + application.rejected_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE + application.save() + + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.approve() + + application.refresh_from_db() + self.assertEqual(application.rejected_reason, None) + def test_save_model_sends_withdrawn_email(self): """When transitioning to withdrawn on a domain request, @@ -633,6 +754,7 @@ class TestDomainApplicationAdmin(MockEppLib): "created_at", "updated_at", "status", + "rejection_reason", "creator", "investigator", "organization_type", @@ -744,7 +866,7 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) - def trigger_saving_approved_to_another_state(self, domain_is_active, another_state): + def trigger_saving_approved_to_another_state(self, domain_is_active, another_state, rejection_reason=None): """Helper method that triggers domain request state changes from approved to another state, with an associated domain that can be either active (READY) or not. @@ -773,6 +895,8 @@ class TestDomainApplicationAdmin(MockEppLib): stack.enter_context(patch.object(messages, "error")) application.status = another_state + application.rejection_reason = rejection_reason + self.admin.save_model(request, application, None, True) # Assert that the error message was called with the correct argument @@ -814,7 +938,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED) def test_side_effects_when_saving_approved_to_rejected(self): - self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED) + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY) def test_side_effects_when_saving_approved_to_ineligible(self): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE) From f2e7278a86e4aa9e997effeeee83b3a5bd34b959 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 17:42:09 -0500 Subject: [PATCH 05/13] updated tests --- src/registrar/tests/test_admin.py | 17 ---------- src/registrar/tests/test_models.py | 50 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 6142c2aee..5f8ea8fc1 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -649,23 +649,6 @@ class TestDomainApplicationAdmin(MockEppLib): self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - def test_save_model_clear_rejected_reason(self): - """When transitioning from rejected on a domain request, - the rejected_reason is cleared.""" - - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) - application.rejected_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE - application.save() - - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.approve() - - application.refresh_from_db() - self.assertEqual(application.rejected_reason, None) - - def test_save_model_sends_withdrawn_email(self): """When transitioning to withdrawn on a domain request, an email is sent out every time.""" diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index cb7906d7a..f243956ff 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -574,6 +574,56 @@ class TestDomainApplication(TestCase): with self.assertRaises(TransitionNotAllowed): self.approved_application.reject_with_prejudice() + def test_approve_from_rejected_clears_rejection_reason(self): + """When transitioning from rejected to approved on a domain request, + the rejection_reason is cleared.""" + + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) + application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE + + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.approve() + + self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED) + self.assertEqual(application.rejection_reason, None) + + def test_in_review_from_rejected_clears_rejection_reason(self): + """When transitioning from rejected to in_review on a domain request, + the rejection_reason is cleared.""" + + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) + application.domain_is_not_active = True + application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE + + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.in_review() + + self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) + self.assertEqual(application.rejection_reason, None) + + def test_action_needed_from_rejected_clears_rejection_reason(self): + """When transitioning from rejected to action_needed on a domain request, + the rejection_reason is cleared.""" + + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) + application.domain_is_not_active = True + application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE + + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.action_needed() + + self.assertEqual(application.status, DomainApplication.ApplicationStatus.ACTION_NEEDED) + self.assertEqual(application.rejection_reason, None) + def test_has_rationale_returns_true(self): """has_rationale() returns true when an application has no_other_contacts_rationale""" with less_console_noise(): From afd1de0430c24a257d6e3d9f869759af8a1a142e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 16 Feb 2024 17:54:32 -0500 Subject: [PATCH 06/13] test the error alert --- src/registrar/tests/test_admin.py | 54 +++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 6142c2aee..484898be4 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -427,8 +427,6 @@ class TestDomainApplicationAdmin(MockEppLib): # Use the model admin's save_model method self.admin.save_model(request, application, form=None, change=True) - logger.info(f'application.rejection_reason {application.rejection_reason}') - def assert_email_is_accurate(self, expected_string, email_index, email_address): """Helper method for the email test cases. email_index is the index of the email in mock_client.""" @@ -649,6 +647,58 @@ class TestDomainApplicationAdmin(MockEppLib): self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self): + """ + When transitioning to rejected without a rejection reason, admin throws a user friendly message. + + The transition fails. + """ + + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser + + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + application.status = DomainApplication.ApplicationStatus.REJECTED + + self.admin.save_model(request, application, None, True) + + messages.error.assert_called_once_with( + request, + "A rejection reason is required.", + ) + + application.refresh_from_db() + self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED) + + def test_transition_to_rejected_with_rejection_reason_does_not_trigger_error(self): + """ + When transitioning to rejected with a rejection reason, admin does not throw an error alert. + + The transition is successful. + """ + + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser + + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + application.status = DomainApplication.ApplicationStatus.REJECTED + application.rejection_reason = DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY + + self.admin.save_model(request, application, None, True) + + messages.error.assert_not_called() + + application.refresh_from_db() + self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED) + def test_save_model_clear_rejected_reason(self): """When transitioning from rejected on a domain request, the rejected_reason is cleared.""" From d56982ac26736815662d834707e369a9165b9798 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 16 Feb 2024 18:18:03 -0500 Subject: [PATCH 07/13] cleaned up noise in tests --- src/registrar/admin.py | 1 - src/registrar/models/domain_application.py | 10 +- src/registrar/tests/test_admin.py | 2042 ++++++++++---------- 3 files changed, 1073 insertions(+), 980 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8225246b9..c75588a27 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -917,7 +917,6 @@ class DomainApplicationAdmin(ListHeaderAdmin): request, "A rejection reason is required.", ) - else: if obj.status != original_obj.status: diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 3f6fcc99f..ca6a996ce 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -353,8 +353,14 @@ class DomainApplication(TimeStampedModel): class RejectionReasons(models.TextChoices): DOMAIN_PURPOSE = "domain_purpose", "Domain purpose requirements not met" REQUESTOR = "requestor", "Requestor isn't authorized to make the request" - SECOND_DOMAIN_REASONING = "second_domain_reasoning", "Organization already has a domain and does not provide sufficient reasoning for a second domain" - CONTACTS_OR_ORGANIZATION_LEGITIMACY = "contacts_or_organization_legitimacy", "Research could not corroborate legitimacy of contacts or organization" + SECOND_DOMAIN_REASONING = ( + "second_domain_reasoning", + "Organization already has a domain and does not provide sufficient reasoning for a second domain", + ) + CONTACTS_OR_ORGANIZATION_LEGITIMACY = ( + "contacts_or_organization_legitimacy", + "Research could not corroborate legitimacy of contacts or organization", + ) ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met" diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 484898be4..8a7c1c0a4 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -270,40 +270,43 @@ class TestDomainApplicationAdminForm(TestCase): self.application = completed_application() def test_form_choices(self): - # Create a form instance with the test application - form = DomainApplicationAdminForm(instance=self.application) + with less_console_noise(): + # Create a form instance with the test application + form = DomainApplicationAdminForm(instance=self.application) - # Verify that the form choices match the available transitions for started - expected_choices = [("started", "Started"), ("submitted", "Submitted")] - self.assertEqual(form.fields["status"].widget.choices, expected_choices) + # Verify that the form choices match the available transitions for started + expected_choices = [("started", "Started"), ("submitted", "Submitted")] + self.assertEqual(form.fields["status"].widget.choices, expected_choices) def test_form_choices_when_no_instance(self): - # Create a form instance without an instance - form = DomainApplicationAdminForm() + with less_console_noise(): + # Create a form instance without an instance + form = DomainApplicationAdminForm() - # Verify that the form choices show all choices when no instance is provided; - # this is necessary to show all choices when creating a new domain - # application in django admin; - # note that FSM ensures that no domain application exists with invalid status, - # so don't need to test for invalid status - self.assertEqual( - form.fields["status"].widget.choices, - DomainApplication._meta.get_field("status").choices, - ) + # Verify that the form choices show all choices when no instance is provided; + # this is necessary to show all choices when creating a new domain + # application in django admin; + # note that FSM ensures that no domain application exists with invalid status, + # so don't need to test for invalid status + self.assertEqual( + form.fields["status"].widget.choices, + DomainApplication._meta.get_field("status").choices, + ) def test_form_choices_when_ineligible(self): - # Create a form instance with a domain application with ineligible status - ineligible_application = DomainApplication(status="ineligible") + with less_console_noise(): + # Create a form instance with a domain application with ineligible status + ineligible_application = DomainApplication(status="ineligible") - # Attempt to create a form with the ineligible application - # The form should not raise an error, but choices should be the - # full list of possible choices - form = DomainApplicationAdminForm(instance=ineligible_application) + # Attempt to create a form with the ineligible application + # The form should not raise an error, but choices should be the + # full list of possible choices + form = DomainApplicationAdminForm(instance=ineligible_application) - self.assertEqual( - form.fields["status"].widget.choices, - DomainApplication._meta.get_field("status").choices, - ) + self.assertEqual( + form.fields["status"].widget.choices, + DomainApplication._meta.get_field("status").choices, + ) @boto3_mocking.patching @@ -327,90 +330,94 @@ class TestDomainApplicationAdmin(MockEppLib): def test_domain_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") + multiple_unalphabetical_domain_objects("application") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("requested_domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("requested_domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",)) def test_submitter_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") + multiple_unalphabetical_domain_objects("application") - additional_application = generic_domain_object("application", "Xylophone") - new_user = User.objects.filter(username=additional_application.investigator.username).get() - new_user.first_name = "Xylophonic" - new_user.save() + additional_application = generic_domain_object("application", "Xylophone") + new_user = User.objects.filter(username=additional_application.investigator.username).get() + new_user.first_name = "Xylophonic" + new_user.save() - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "5", - ( - "submitter__first_name", - "submitter__last_name", - ), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "5", + ( + "submitter__first_name", + "submitter__last_name", + ), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted( - "-5", - ( - "-submitter__first_name", - "-submitter__last_name", - ), - ) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted( + "-5", + ( + "-submitter__first_name", + "-submitter__last_name", + ), + ) def test_investigator_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - multiple_unalphabetical_domain_objects("application") - additional_application = generic_domain_object("application", "Xylophone") - new_user = User.objects.filter(username=additional_application.investigator.username).get() - new_user.first_name = "Xylophonic" - new_user.save() + multiple_unalphabetical_domain_objects("application") + additional_application = generic_domain_object("application", "Xylophone") + new_user = User.objects.filter(username=additional_application.investigator.username).get() + new_user.first_name = "Xylophonic" + new_user.save() - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "6", - ( - "investigator__first_name", - "investigator__last_name", - ), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "6", + ( + "investigator__first_name", + "investigator__last_name", + ), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted( - "-6", - ( - "-investigator__first_name", - "-investigator__last_name", - ), - ) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted( + "-6", + ( + "-investigator__first_name", + "-investigator__last_name", + ), + ) def test_short_org_name_in_applications_list(self): """ Make sure the short name is displaying in admin on the list page """ - self.client.force_login(self.superuser) - completed_application() - response = self.client.get("/admin/registrar/domainapplication/") - # There are 3 template references to Federal (3) plus one reference in the table - # for our actual application - self.assertContains(response, "Federal", count=4) - # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) - # Now let's make sure the long description does not exist - self.assertNotContains(response, "Federal: an agency of the U.S. government") + with less_console_noise(): + self.client.force_login(self.superuser) + completed_application() + response = self.client.get("/admin/registrar/domainapplication/") + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains(response, 'Federal', count=1) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") def transition_state_and_send_email(self, application, status, rejection_reason=None): """Helper method for the email test cases.""" @@ -431,20 +438,21 @@ class TestDomainApplicationAdmin(MockEppLib): """Helper method for the email test cases. email_index is the index of the email in mock_client.""" - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[email_index]["kwargs"] + with less_console_noise(): + # Access the arguments passed to send_email + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[email_index]["kwargs"] - # Retrieve the email details from the arguments - from_email = kwargs.get("FromEmailAddress") - to_email = kwargs["Destination"]["ToAddresses"][0] - email_content = kwargs["Content"] - email_body = email_content["Simple"]["Body"]["Text"]["Data"] + # Retrieve the email details from the arguments + from_email = kwargs.get("FromEmailAddress") + to_email = kwargs["Destination"]["ToAddresses"][0] + email_content = kwargs["Content"] + email_body = email_content["Simple"]["Body"]["Text"]["Data"] - # Assert or perform other checks on the email details - self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(to_email, email_address) - self.assertIn(expected_string, email_body) + # Assert or perform other checks on the email details + self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) + self.assertEqual(to_email, email_address) + self.assertIn(expected_string, email_body) def test_save_model_sends_submitted_email(self): """When transitioning to submitted from started or withdrawn on a domain request, @@ -453,468 +461,514 @@ class TestDomainApplicationAdmin(MockEppLib): When transitioning to submitted from dns needed or in review on a domain request, no email is sent out.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application() + # Create a sample application + application = completed_application() - # Test Submitted Status from started - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Test Submitted Status from started + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Test Withdrawn Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) - self.assert_email_is_accurate( - "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - # Test Submitted Status Again (from withdrawn) - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Test Submitted Status Again (from withdrawn) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - # Move it to IN_REVIEW - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Move it to IN_REVIEW + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - # Test Submitted Status Again from in IN_REVIEW, no new email should be sent - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Test Submitted Status Again from in IN_REVIEW, no new email should be sent + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - # Move it to IN_REVIEW - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Move it to IN_REVIEW + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - # Move it to ACTION_NEEDED - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Move it to ACTION_NEEDED + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sends_approved_email(self): """When transitioning to approved on a domain request, an email is sent out every time.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Test Submitted Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Test Withdrawn Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.DOMAIN_PURPOSE) - self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Test Withdrawn Status + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.DOMAIN_PURPOSE, + ) + self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - # Test Submitted Status Again (No new email should be sent) - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sends_rejected_email_domain_purpose(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is domain purpose.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason DOMAIN_PURPOSE and test email - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.DOMAIN_PURPOSE) - self.assert_email_is_accurate("Your domain request was rejected because the purpose you provided did not meet our \nrequirements.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason DOMAIN_PURPOSE and test email + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.DOMAIN_PURPOSE, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because the purpose you provided did not meet our \nrequirements.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) def test_save_model_sends_rejected_email_requestor(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is requestor.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason REQUESTOR and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR) - self.assert_email_is_accurate("Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain on behalf of Testorg", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason REQUESTOR and test email including dynamic organization name + self.transition_state_and_send_email( + application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain " + "on behalf of Testorg", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - def test_save_model_sends_rejected_email_requestor(self): + def test_save_model_sends_rejected_email_second_domain_reasoning(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is second domain.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.SECOND_DOMAIN_REASONING) - self.assert_email_is_accurate("Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.SECOND_DOMAIN_REASONING, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is contacts or org legitimacy.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY) - self.assert_email_is_accurate("Your domain request was rejected because we could not verify the organizational \ncontacts you provided. If you have questions or comments, reply to this email.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we could not verify the organizational \n" + "contacts you provided. If you have questions or comments, reply to this email.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - # Approve - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - def test_save_model_sends_rejected_email_org_eligibility(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is org eligibility.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.ORGANIZATION_ELIGIBILITY) - self.assert_email_is_accurate("Your domain request was rejected because we determined that Testorg is not \neligible for a .gov domain.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.ORGANIZATION_ELIGIBILITY, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we determined that Testorg is not \neligible for " + "a .gov domain.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) def test_save_model_sends_rejected_email_naming(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is naming.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.NAMING_REQUIREMENTS) - self.assert_email_is_accurate("Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.NAMING_REQUIREMENTS, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Approve - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self): """ - When transitioning to rejected without a rejection reason, admin throws a user friendly message. + When transitioning to rejected without a rejection reason, admin throws a user friendly message. - The transition fails. + The transition fails. """ - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + with less_console_noise(): + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - with ExitStack() as stack: - stack.enter_context(patch.object(messages, "error")) - application.status = DomainApplication.ApplicationStatus.REJECTED + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser - self.admin.save_model(request, application, None, True) + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + application.status = DomainApplication.ApplicationStatus.REJECTED - messages.error.assert_called_once_with( - request, - "A rejection reason is required.", - ) + self.admin.save_model(request, application, None, True) - application.refresh_from_db() - self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED) + messages.error.assert_called_once_with( + request, + "A rejection reason is required.", + ) + + application.refresh_from_db() + self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED) def test_transition_to_rejected_with_rejection_reason_does_not_trigger_error(self): """ - When transitioning to rejected with a rejection reason, admin does not throw an error alert. + When transitioning to rejected with a rejection reason, admin does not throw an error alert. - The transition is successful. + The transition is successful. """ - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + with less_console_noise(): + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - with ExitStack() as stack: - stack.enter_context(patch.object(messages, "error")) - application.status = DomainApplication.ApplicationStatus.REJECTED - application.rejection_reason = DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser - self.admin.save_model(request, application, None, True) + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + application.status = DomainApplication.ApplicationStatus.REJECTED + application.rejection_reason = DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY - messages.error.assert_not_called() + self.admin.save_model(request, application, None, True) - application.refresh_from_db() - self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED) - - def test_save_model_clear_rejected_reason(self): - """When transitioning from rejected on a domain request, - the rejected_reason is cleared.""" - - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) - application.rejected_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE - application.save() - - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.approve() - - application.refresh_from_db() - self.assertEqual(application.rejected_reason, None) + messages.error.assert_not_called() + application.refresh_from_db() + self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED) def test_save_model_sends_withdrawn_email(self): """When transitioning to withdrawn on a domain request, an email is sent out every time.""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Test Submitted Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) - self.assert_email_is_accurate( - "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - # Test Withdrawn Status - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - # Test Submitted Status Again (No new email should be sent) - self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sets_approved_domain(self): # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.APPROVED + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.APPROVED - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) - # Test that approved domain exists and equals requested domain - self.assertEqual(application.requested_domain.name, application.approved_domain.name) + # Test that approved domain exists and equals requested domain + self.assertEqual(application.requested_domain.name, application.approved_domain.name) def test_save_model_sets_restricted_status_on_user(self): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() + with less_console_noise(): + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): # Modify the application's property application.status = DomainApplication.ApplicationStatus.INELIGIBLE # Use the model admin's save_model method self.admin.save_model(request, application, form=None, change=True) - # Test that approved domain exists and equals requested domain - self.assertEqual(application.creator.status, "restricted") + # Test that approved domain exists and equals requested domain + self.assertEqual(application.creator.status, "restricted") def test_readonly_when_restricted_creator(self): - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with less_console_noise(): + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): application.creator.status = User.RESTRICTED application.creator.save() - request = self.factory.get("/") - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request, application) - - expected_fields = [ - "id", - "created_at", - "updated_at", - "status", - "rejection_reason", - "creator", - "investigator", - "organization_type", - "federally_recognized_tribe", - "state_recognized_tribe", - "tribe_name", - "federal_agency", - "federal_type", - "is_election_board", - "organization_name", - "address_line1", - "address_line2", - "city", - "state_territory", - "zipcode", - "urbanization", - "about_your_organization", - "authorizing_official", - "approved_domain", - "requested_domain", - "submitter", - "purpose", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - "submission_date", - "notes", - "current_websites", - "other_contacts", - "alternative_domains", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_analyst(self): - request = self.factory.get("/") # Use the correct method and path - request.user = self.staffuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [ - "creator", - "about_your_organization", - "requested_domain", - "approved_domain", - "alternative_domains", - "purpose", - "submitter", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_superuser(self): - request = self.factory.get("/") # Use the correct method and path - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [] - - self.assertEqual(readonly_fields, expected_fields) - - def test_saving_when_restricted_creator(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.creator.status = User.RESTRICTED - application.creator.save() - - # Create a request object with a superuser - request = self.factory.get("/") - request.user = self.superuser - - with patch("django.contrib.messages.error") as mock_error: - # Simulate saving the model - self.admin.save_model(request, application, None, False) - - # Assert that the error message was called with the correct argument - mock_error.assert_called_once_with( - request, - "This action is not permitted for applications with a restricted creator.", - ) - - # Assert that the status has not changed - self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) - - def test_change_view_with_restricted_creator(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.creator.status = User.RESTRICTED - application.creator.save() - - with patch("django.contrib.messages.warning") as mock_warning: - # Create a request object with a superuser - request = self.factory.get("/admin/your_app/domainapplication/{}/change/".format(application.pk)) + request = self.factory.get("/") request.user = self.superuser - self.admin.display_restricted_warning(request, application) + readonly_fields = self.admin.get_readonly_fields(request, application) - # Assert that the error message was called with the correct argument - mock_warning.assert_called_once_with( - request, - "Cannot edit an application with a restricted creator.", - ) + expected_fields = [ + "id", + "created_at", + "updated_at", + "status", + "rejection_reason", + "creator", + "investigator", + "organization_type", + "federally_recognized_tribe", + "state_recognized_tribe", + "tribe_name", + "federal_agency", + "federal_type", + "is_election_board", + "organization_name", + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + "urbanization", + "about_your_organization", + "authorizing_official", + "approved_domain", + "requested_domain", + "submitter", + "purpose", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + "submission_date", + "notes", + "current_websites", + "other_contacts", + "alternative_domains", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_analyst(self): + with less_console_noise(): + request = self.factory.get("/") # Use the correct method and path + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "creator", + "about_your_organization", + "requested_domain", + "approved_domain", + "alternative_domains", + "purpose", + "submitter", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_superuser(self): + with less_console_noise(): + request = self.factory.get("/") # Use the correct method and path + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [] + + self.assertEqual(readonly_fields, expected_fields) + + def test_saving_when_restricted_creator(self): + with less_console_noise(): + # Create an instance of the model + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.creator.status = User.RESTRICTED + application.creator.save() + + # Create a request object with a superuser + request = self.factory.get("/") + request.user = self.superuser + + with patch("django.contrib.messages.error") as mock_error: + # Simulate saving the model + self.admin.save_model(request, application, None, False) + + # Assert that the error message was called with the correct argument + mock_error.assert_called_once_with( + request, + "This action is not permitted for applications with a restricted creator.", + ) + + # Assert that the status has not changed + self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW) + + def test_change_view_with_restricted_creator(self): + with less_console_noise(): + # Create an instance of the model + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.creator.status = User.RESTRICTED + application.creator.save() + + with patch("django.contrib.messages.warning") as mock_warning: + # Create a request object with a superuser + request = self.factory.get("/admin/your_app/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser + + self.admin.display_restricted_warning(request, application) + + # Assert that the error message was called with the correct argument + mock_warning.assert_called_once_with( + request, + "Cannot edit an application with a restricted creator.", + ) def trigger_saving_approved_to_another_state(self, domain_is_active, another_state, rejection_reason=None): """Helper method that triggers domain request state changes from approved to another state, @@ -923,51 +977,52 @@ class TestDomainApplicationAdmin(MockEppLib): Used to test errors when saving a change with an active domain, also used to test side effects when saving a change goes through.""" - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain) - application.approved_domain = domain - application.save() + with less_console_noise(): + # Create an instance of the model + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + domain = Domain.objects.create(name=application.requested_domain.name) + domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain) + application.approved_domain = domain + application.save() - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request.user = self.superuser - # Define a custom implementation for is_active - def custom_is_active(self): - return domain_is_active # Override to return True + # Define a custom implementation for is_active + def custom_is_active(self): + return domain_is_active # Override to return True - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) + # Use ExitStack to combine patch contexts + with ExitStack() as stack: + # Patch Domain.is_active and django.contrib.messages.error simultaneously + stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) + stack.enter_context(patch.object(messages, "error")) - application.status = another_state - application.rejection_reason = rejection_reason + application.status = another_state + application.rejection_reason = rejection_reason - self.admin.save_model(request, application, None, True) + self.admin.save_model(request, application, None, True) - # Assert that the error message was called with the correct argument - if domain_is_active: - messages.error.assert_called_once_with( - request, - "This action is not permitted. The domain " + "is already active.", - ) - else: - # Assert that the error message was never called - messages.error.assert_not_called() + # Assert that the error message was called with the correct argument + if domain_is_active: + messages.error.assert_called_once_with( + request, + "This action is not permitted. The domain " + "is already active.", + ) + else: + # Assert that the error message was never called + messages.error.assert_not_called() - self.assertEqual(application.approved_domain, None) + self.assertEqual(application.approved_domain, None) - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() + # Assert that Domain got Deleted + with self.assertRaises(Domain.DoesNotExist): + domain.refresh_from_db() - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() + # Assert that DomainInformation got Deleted + with self.assertRaises(DomainInformation.DoesNotExist): + domain_information.refresh_from_db() def test_error_when_saving_approved_to_in_review_and_domain_is_active(self): self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW) @@ -988,7 +1043,11 @@ class TestDomainApplicationAdmin(MockEppLib): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED) def test_side_effects_when_saving_approved_to_rejected(self): - self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY) + self.trigger_saving_approved_to_another_state( + False, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY, + ) def test_side_effects_when_saving_approved_to_ineligible(self): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE) @@ -1000,14 +1059,15 @@ class TestDomainApplicationAdmin(MockEppLib): It retrieves the current list of filters from DomainApplicationAdmin and checks that it matches the expected list of filters. """ - request = self.factory.get("/") - request.user = self.superuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.superuser - # Grab the current list of table filters - readonly_fields = self.admin.get_list_filter(request) - expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) + # Grab the current list of table filters + readonly_fields = self.admin.get_list_filter(request) + expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_table_sorted_alphabetically(self): """ @@ -1019,23 +1079,24 @@ class TestDomainApplicationAdmin(MockEppLib): that it matches the expected queryset, which is sorted alphabetically by the 'requested_domain__name' field. """ - # Creates a list of DomainApplications in scrambled order - multiple_unalphabetical_domain_objects("application") + with less_console_noise(): + # Creates a list of DomainApplications in scrambled order + multiple_unalphabetical_domain_objects("application") - request = self.factory.get("/") - request.user = self.superuser + request = self.factory.get("/") + request.user = self.superuser - # Get the expected list of alphabetically sorted DomainApplications - expected_order = DomainApplication.objects.order_by("requested_domain__name") + # Get the expected list of alphabetically sorted DomainApplications + expected_order = DomainApplication.objects.order_by("requested_domain__name") - # Get the returned queryset - queryset = self.admin.get_queryset(request) + # Get the returned queryset + queryset = self.admin.get_queryset(request) - # Check the order - self.assertEqual( - list(queryset), - list(expected_order), - ) + # Check the order + self.assertEqual( + list(queryset), + list(expected_order), + ) def test_displays_investigator_filter(self): """ @@ -1051,37 +1112,38 @@ class TestDomainApplicationAdmin(MockEppLib): the filter displays correctly, when the filter isn't filtering correctly. """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainapplication/", - { - "investigator__id__exact": investigator_user.id, - }, - follow=True, - ) - - # Then, test if the filter actually exists - self.assertIn("filters", response.context) - - # Assert the content of filters and search_query - filters = response.context["filters"] - - self.assertEqual( - filters, - [ + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domainapplication/", { - "parameter_name": "investigator", - "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", + "investigator__id__exact": investigator_user.id, }, - ], - ) + follow=True, + ) + + # Then, test if the filter actually exists + self.assertIn("filters", response.context) + + # Assert the content of filters and search_query + filters = response.context["filters"] + + self.assertEqual( + filters, + [ + { + "parameter_name": "investigator", + "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", + }, + ], + ) def test_investigator_dropdown_displays_only_staff(self): """ @@ -1094,68 +1156,71 @@ class TestDomainApplicationAdmin(MockEppLib): It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin and checks that it matches the expected queryset, which only includes staff users. """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() - # Create a mock DomainApplication object, with a user that is not staff - application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy") - investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() - investigator_user_2.is_staff = False - investigator_user_2.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - p = "userpass" - self.client.login(username="staffuser", password=p) + # Create a mock DomainApplication object, with a user that is not staff + application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy") + investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() + investigator_user_2.is_staff = False + investigator_user_2.save() - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + p = "userpass" + self.client.login(username="staffuser", password=p) - # Get the actual field from the model's meta information - investigator_field = DomainApplication._meta.get_field("investigator") + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # We should only be displaying staff users, in alphabetical order - expected_dropdown = list(User.objects.filter(is_staff=True)) - current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset) + # Get the actual field from the model's meta information + investigator_field = DomainApplication._meta.get_field("investigator") - self.assertEqual(expected_dropdown, current_dropdown) + # We should only be displaying staff users, in alphabetical order + expected_dropdown = list(User.objects.filter(is_staff=True)) + current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset) - # Non staff users should not be in the list - self.assertNotIn(application_2, current_dropdown) + self.assertEqual(expected_dropdown, current_dropdown) + + # Non staff users should not be in the list + self.assertNotIn(application_2, current_dropdown) def test_investigator_list_is_alphabetically_sorted(self): """ This test verifies that filter list for the 'investigator' is displayed alphabetically """ - # Create a mock DomainApplication object, with a fake investigator - application: DomainApplication = generic_domain_object("application", "SomeGuy") - investigator_user = User.objects.filter(username=application.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() + with less_console_noise(): + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() - application_2: DomainApplication = generic_domain_object("application", "AGuy") - investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() - investigator_user_2.first_name = "AGuy" - investigator_user_2.is_staff = True - investigator_user_2.save() + application_2: DomainApplication = generic_domain_object("application", "AGuy") + investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() + investigator_user_2.first_name = "AGuy" + investigator_user_2.is_staff = True + investigator_user_2.save() - application_3: DomainApplication = generic_domain_object("application", "FinalGuy") - investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get() - investigator_user_3.first_name = "FinalGuy" - investigator_user_3.is_staff = True - investigator_user_3.save() + application_3: DomainApplication = generic_domain_object("application", "FinalGuy") + investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get() + investigator_user_3.first_name = "FinalGuy" + investigator_user_3.is_staff = True + investigator_user_3.save() - p = "userpass" - self.client.login(username="staffuser", password=p) - request = RequestFactory().get("/") + p = "userpass" + self.client.login(username="staffuser", password=p) + request = RequestFactory().get("/") - expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")) + expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")) - # Get the actual sorted list of investigators from the lookups method - actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] + # Get the actual sorted list of investigators from the lookups method + actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] - self.assertEqual(expected_list, actual_list) + self.assertEqual(expected_list, actual_list) def tearDown(self): super().tearDown() @@ -1184,28 +1249,29 @@ class DomainInvitationAdminTest(TestCase): def test_get_filters(self): """Ensures that our filters are displaying correctly""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domaininvitation/", - {}, - follow=True, - ) + response = self.client.get( + "/admin/registrar/domaininvitation/", + {}, + follow=True, + ) - # Assert that the filters are added - self.assertContains(response, "invited", count=2) - self.assertContains(response, "Invited", count=2) - self.assertContains(response, "retrieved", count=2) - self.assertContains(response, "Retrieved", count=2) + # Assert that the filters are added + self.assertContains(response, "invited", count=2) + self.assertContains(response, "Invited", count=2) + self.assertContains(response, "retrieved", count=2) + self.assertContains(response, "Retrieved", count=2) - # Check for the HTML context specificially - invited_html = 'Invited' - retrieved_html = 'Retrieved' + # Check for the HTML context specificially + invited_html = 'Invited' + retrieved_html = 'Retrieved' - self.assertContains(response, invited_html, count=1) - self.assertContains(response, retrieved_html, count=1) + self.assertContains(response, invited_html, count=1) + self.assertContains(response, retrieved_html, count=1) class TestDomainInformationAdmin(TestCase): @@ -1261,50 +1327,53 @@ class TestDomainInformationAdmin(TestCase): User.objects.all().delete() def test_readonly_fields_for_analyst(self): - """Ensures that analysts have their permissions setup correctly""" - request = self.factory.get("/") - request.user = self.staffuser + with less_console_noise(): + """Ensures that analysts have their permissions setup correctly""" + request = self.factory.get("/") + request.user = self.staffuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [ - "creator", - "type_of_work", - "more_organization_information", - "domain", - "domain_application", - "submitter", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - ] + expected_fields = [ + "creator", + "type_of_work", + "more_organization_information", + "domain", + "domain_application", + "submitter", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + ] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_domain_sortable(self): """Tests if DomainInformation sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-domain__name",)) def test_submitter_sortable(self): """Tests if DomainInformation sorts by submitter correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "4", - ("submitter__first_name", "submitter__last_name"), - ) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "4", + ("submitter__first_name", "submitter__last_name"), + ) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name")) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name")) class UserDomainRoleAdminTest(TestCase): @@ -1331,109 +1400,113 @@ class UserDomainRoleAdminTest(TestCase): def test_domain_sortable(self): """Tests if the UserDomainrole sorts by domain correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" + ) - # Create a list of UserDomainRoles that are in random order - mocks_to_create = ["jkl.gov", "ghi.gov", "abc.gov", "def.gov"] - for name in mocks_to_create: - fake_domain = Domain.objects.create(name=name) - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Create a list of UserDomainRoles that are in random order + mocks_to_create = ["jkl.gov", "ghi.gov", "abc.gov", "def.gov"] + for name in mocks_to_create: + fake_domain = Domain.objects.create(name=name) + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("2", ("domain__name",)) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("2", ("domain__name",)) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-2", ("-domain__name",)) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-2", ("-domain__name",)) def test_user_sortable(self): """Tests if the UserDomainrole sorts by user correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - mock_data_generator = AuditedAdminMockData() + mock_data_generator = AuditedAdminMockData() - fake_domain = Domain.objects.create(name="igorville.gov") - # Create a list of UserDomainRoles that are in random order - mocks_to_create = ["jkl", "ghi", "abc", "def"] - for name in mocks_to_create: - # Creates a fake "User" object - fake_user = mock_data_generator.dummy_user(name, "user") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + fake_domain = Domain.objects.create(name="igorville.gov") + # Create a list of UserDomainRoles that are in random order + mocks_to_create = ["jkl", "ghi", "abc", "def"] + for name in mocks_to_create: + # Creates a fake "User" object + fake_user = mock_data_generator.dummy_user(name, "user") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("user__first_name", "user__last_name")) + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("user__first_name", "user__last_name")) - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-user__first_name", "-user__last_name")) + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-user__first_name", "-user__last_name")) def test_email_not_in_search(self): """Tests the search bar in Django Admin for UserDomainRoleAdmin. Should return no results for an invalid email.""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" - ) - fake_domain = Domain.objects.create(name="test123") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Make the request using the Client class - # which handles CSRF - # Follow=True handles the redirect - response = self.client.get( - "/admin/registrar/userdomainrole/", - { - "q": "testmail@igorville.com", - }, - follow=True, - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" + ) + fake_domain = Domain.objects.create(name="test123") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Make the request using the Client class + # which handles CSRF + # Follow=True handles the redirect + response = self.client.get( + "/admin/registrar/userdomainrole/", + { + "q": "testmail@igorville.com", + }, + follow=True, + ) - # Assert that the query is added to the extra_context - self.assertIn("search_query", response.context) - # Assert the content of filters and search_query - search_query = response.context["search_query"] - self.assertEqual(search_query, "testmail@igorville.com") + # Assert that the query is added to the extra_context + self.assertIn("search_query", response.context) + # Assert the content of filters and search_query + search_query = response.context["search_query"] + self.assertEqual(search_query, "testmail@igorville.com") - # We only need to check for the end of the HTML string - self.assertNotContains(response, "Stewart Jones AntarcticPolarBears@example.com") + # We only need to check for the end of the HTML string + self.assertNotContains(response, "Stewart Jones AntarcticPolarBears@example.com") def test_email_in_search(self): """Tests the search bar in Django Admin for UserDomainRoleAdmin. Should return results for an valid email.""" - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - fake_user = User.objects.create( - username="dummyuser", first_name="Joe", last_name="Jones", email="AntarcticPolarBears@example.com" - ) - fake_domain = Domain.objects.create(name="fake") - UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") - # Make the request using the Client class - # which handles CSRF - # Follow=True handles the redirect - response = self.client.get( - "/admin/registrar/userdomainrole/", - { - "q": "AntarcticPolarBears@example.com", - }, - follow=True, - ) + fake_user = User.objects.create( + username="dummyuser", first_name="Joe", last_name="Jones", email="AntarcticPolarBears@example.com" + ) + fake_domain = Domain.objects.create(name="fake") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + # Make the request using the Client class + # which handles CSRF + # Follow=True handles the redirect + response = self.client.get( + "/admin/registrar/userdomainrole/", + { + "q": "AntarcticPolarBears@example.com", + }, + follow=True, + ) - # Assert that the query is added to the extra_context - self.assertIn("search_query", response.context) + # Assert that the query is added to the extra_context + self.assertIn("search_query", response.context) - search_query = response.context["search_query"] - self.assertEqual(search_query, "AntarcticPolarBears@example.com") + search_query = response.context["search_query"] + self.assertEqual(search_query, "AntarcticPolarBears@example.com") - # We only need to check for the end of the HTML string - self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com", count=1) + # We only need to check for the end of the HTML string + self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com", count=1) class ListHeaderAdminTest(TestCase): @@ -1515,39 +1588,42 @@ class MyUserAdminTest(TestCase): self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site) def test_list_display_without_username(self): - request = self.client.request().wsgi_request - request.user = create_user() + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_user() - list_display = self.admin.get_list_display(request) - expected_list_display = [ - "email", - "first_name", - "last_name", - "group", - "status", - ] + list_display = self.admin.get_list_display(request) + expected_list_display = [ + "email", + "first_name", + "last_name", + "group", + "status", + ] - self.assertEqual(list_display, expected_list_display) - self.assertNotIn("username", list_display) + self.assertEqual(list_display, expected_list_display) + self.assertNotIn("username", list_display) def test_get_fieldsets_superuser(self): - request = self.client.request().wsgi_request - request.user = create_superuser() - fieldsets = self.admin.get_fieldsets(request) - expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request) - self.assertEqual(fieldsets, expected_fieldsets) + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_superuser() + fieldsets = self.admin.get_fieldsets(request) + expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request) + self.assertEqual(fieldsets, expected_fieldsets) def test_get_fieldsets_cisa_analyst(self): - request = self.client.request().wsgi_request - request.user = create_user() - fieldsets = self.admin.get_fieldsets(request) - expected_fieldsets = ( - (None, {"fields": ("password", "status")}), - ("Personal Info", {"fields": ("first_name", "last_name", "email")}), - ("Permissions", {"fields": ("is_active", "groups")}), - ("Important dates", {"fields": ("last_login", "date_joined")}), - ) - self.assertEqual(fieldsets, expected_fieldsets) + with less_console_noise(): + request = self.client.request().wsgi_request + request.user = create_user() + fieldsets = self.admin.get_fieldsets(request) + expected_fieldsets = ( + (None, {"fields": ("password", "status")}), + ("Personal Info", {"fields": ("first_name", "last_name", "email")}), + ("Permissions", {"fields": ("is_active", "groups")}), + ("Important dates", {"fields": ("last_login", "date_joined")}), + ) + self.assertEqual(fieldsets, expected_fieldsets) def tearDown(self): User.objects.all().delete() @@ -1571,161 +1647,164 @@ class AuditedAdminTest(TestCase): return ordered_list def test_alphabetically_sorted_fk_fields_domain_application(self): - tested_fields = [ - DomainApplication.authorizing_official.field, - DomainApplication.submitter.field, - DomainApplication.investigator.field, - DomainApplication.creator.field, - DomainApplication.requested_domain.field, - ] + with less_console_noise(): + tested_fields = [ + DomainApplication.authorizing_official.field, + DomainApplication.submitter.field, + DomainApplication.investigator.field, + DomainApplication.creator.field, + DomainApplication.requested_domain.field, + ] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("application") + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("application") - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)) - model_admin = AuditedAdmin(DomainApplication, self.site) + model_admin = AuditedAdmin(DomainApplication, self.site) - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - isNamefield: bool = field == DomainApplication.requested_domain.field - if isNamefield: - sorted_fields = ["name"] - else: - sorted_fields = ["first_name", "last_name"] - # We want both of these to be lists, as it is richer test wise. - - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - if not isNamefield: - first = contact.first_name - last = contact.last_name + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + isNamefield: bool = field == DomainApplication.requested_domain.field + if isNamefield: + sorted_fields = ["name"] else: + sorted_fields = ["first_name", "last_name"] + # We want both of these to be lists, as it is richer test wise. + + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: + if not isNamefield: + first = contact.first_name + last = contact.last_name + else: + first = contact.name + last = None + + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name), + ) + + def test_alphabetically_sorted_fk_fields_domain_information(self): + with less_console_noise(): + tested_fields = [ + DomainInformation.authorizing_official.field, + DomainInformation.submitter.field, + # DomainInformation.creator.field, + (DomainInformation.domain.field, ["name"]), + (DomainInformation.domain_application.field, ["requested_domain__name"]), + ] + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("information") + + # Create a mock request + request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) + + model_admin = AuditedAdmin(DomainInformation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + isOtherOrderfield: bool = isinstance(field, tuple) + field_obj = None + if isOtherOrderfield: + sorted_fields = field[1] + field_obj = field[0] + else: + sorted_fields = ["first_name", "last_name"] + field_obj = field + # We want both of these to be lists, as it is richer test wise. + desired_order = self.order_by_desired_field_helper(model_admin, request, field_obj.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field_obj, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for obj in current_sort_order: + last = None + if not isOtherOrderfield: + first = obj.first_name + last = obj.last_name + elif field_obj == DomainInformation.domain.field: + first = obj.name + elif field_obj == DomainInformation.domain_application.field: + first = obj.requested_domain.name + + name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field_obj.name), + ) + + def test_alphabetically_sorted_fk_fields_domain_invitation(self): + with less_console_noise(): + tested_fields = [DomainInvitation.domain.field] + + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("invitation") + + # Create a mock request + request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) + + model_admin = AuditedAdmin(DomainInvitation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + sorted_fields = ["name"] + # We want both of these to be lists, as it is richer test wise. + + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: first = contact.name last = None - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") + if name_tuple is not None: + current_sort_order_coerced_type.append(name_tuple) - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_information(self): - tested_fields = [ - DomainInformation.authorizing_official.field, - DomainInformation.submitter.field, - # DomainInformation.creator.field, - (DomainInformation.domain.field, ["name"]), - (DomainInformation.domain_application.field, ["requested_domain__name"]), - ] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("information") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInformation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - isOtherOrderfield: bool = isinstance(field, tuple) - field_obj = None - if isOtherOrderfield: - sorted_fields = field[1] - field_obj = field[0] - else: - sorted_fields = ["first_name", "last_name"] - field_obj = field - # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper(model_admin, request, field_obj.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field_obj, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for obj in current_sort_order: - last = None - if not isOtherOrderfield: - first = obj.first_name - last = obj.last_name - elif field_obj == DomainInformation.domain.field: - first = obj.name - elif field_obj == DomainInformation.domain_application.field: - first = obj.requested_domain.name - - name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field_obj.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_invitation(self): - tested_fields = [DomainInvitation.domain.field] - - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("invitation") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInvitation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - sorted_fields = ["name"] - # We want both of these to be lists, as it is richer test wise. - - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - first = contact.name - last = None - - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name), + ) def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): """Handles edge cases for test cases""" @@ -1759,93 +1838,98 @@ class DomainSessionVariableTest(TestCase): def test_session_vars_set_correctly(self): """Checks if session variables are being set correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information = generic_domain_object("information", "session") - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual( - request.session["analyst_action_location"], - dummy_domain_information.domain.pk, - ) + dummy_domain_information = generic_domain_object("information", "session") + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + self.populate_session_values(request, dummy_domain_information.domain) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual( + request.session["analyst_action_location"], + dummy_domain_information.domain.pk, + ) def test_session_vars_set_correctly_hardcoded_domain(self): """Checks if session variables are being set correctly""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information: Domain = generic_domain_object("information", "session") - dummy_domain_information.domain.pk = 1 + dummy_domain_information: Domain = generic_domain_object("information", "session") + dummy_domain_information.domain.pk = 1 - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual(request.session["analyst_action_location"], 1) + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + self.populate_session_values(request, dummy_domain_information.domain) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual(request.session["analyst_action_location"], 1) def test_session_variables_reset_correctly(self): """Checks if incorrect session variables get overridden""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information = generic_domain_object("information", "session") - request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + dummy_domain_information = generic_domain_object("information", "session") + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) - self.populate_session_values(request, dummy_domain_information.domain, preload_bad_data=True) + self.populate_session_values(request, dummy_domain_information.domain, preload_bad_data=True) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual( - request.session["analyst_action_location"], - dummy_domain_information.domain.pk, - ) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual( + request.session["analyst_action_location"], + dummy_domain_information.domain.pk, + ) def test_session_variables_retain_information(self): """Checks to see if session variables retain old information""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - dummy_domain_information_list = multiple_unalphabetical_domain_objects("information") - for item in dummy_domain_information_list: - request = self.get_factory_post_edit_domain(item.domain.pk) - self.populate_session_values(request, item.domain) + dummy_domain_information_list = multiple_unalphabetical_domain_objects("information") + for item in dummy_domain_information_list: + request = self.get_factory_post_edit_domain(item.domain.pk) + self.populate_session_values(request, item.domain) - self.assertEqual(request.session["analyst_action"], "edit") - self.assertEqual(request.session["analyst_action_location"], item.domain.pk) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual(request.session["analyst_action_location"], item.domain.pk) def test_session_variables_concurrent_requests(self): """Simulates two requests at once""" - p = "adminpass" - self.client.login(username="superuser", password=p) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) - info_first = generic_domain_object("information", "session") - info_second = generic_domain_object("information", "session2") + info_first = generic_domain_object("information", "session") + info_second = generic_domain_object("information", "session2") - request_first = self.get_factory_post_edit_domain(info_first.domain.pk) - request_second = self.get_factory_post_edit_domain(info_second.domain.pk) + request_first = self.get_factory_post_edit_domain(info_first.domain.pk) + request_second = self.get_factory_post_edit_domain(info_second.domain.pk) - self.populate_session_values(request_first, info_first.domain, True) - self.populate_session_values(request_second, info_second.domain, True) + self.populate_session_values(request_first, info_first.domain, True) + self.populate_session_values(request_second, info_second.domain, True) - # Check if anything got nulled out - self.assertNotEqual(request_first.session["analyst_action"], None) - self.assertNotEqual(request_second.session["analyst_action"], None) - self.assertNotEqual(request_first.session["analyst_action_location"], None) - self.assertNotEqual(request_second.session["analyst_action_location"], None) + # Check if anything got nulled out + self.assertNotEqual(request_first.session["analyst_action"], None) + self.assertNotEqual(request_second.session["analyst_action"], None) + self.assertNotEqual(request_first.session["analyst_action_location"], None) + self.assertNotEqual(request_second.session["analyst_action_location"], None) - # Check if they are both the same action 'type' - self.assertEqual(request_first.session["analyst_action"], "edit") - self.assertEqual(request_second.session["analyst_action"], "edit") + # Check if they are both the same action 'type' + self.assertEqual(request_first.session["analyst_action"], "edit") + self.assertEqual(request_second.session["analyst_action"], "edit") - # Check their locations, and ensure they aren't the same across both - self.assertNotEqual( - request_first.session["analyst_action_location"], - request_second.session["analyst_action_location"], - ) + # Check their locations, and ensure they aren't the same across both + self.assertNotEqual( + request_first.session["analyst_action_location"], + request_second.session["analyst_action_location"], + ) def populate_session_values(self, request, domain_object, preload_bad_data=False): """Boilerplate for creating mock sessions""" @@ -1878,63 +1962,66 @@ class ContactAdminTest(TestCase): self.staffuser = create_user() def test_readonly_when_restricted_staffuser(self): - request = self.factory.get("/") - request.user = self.staffuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.staffuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [ - "user", - ] + expected_fields = [ + "user", + ] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_readonly_when_restricted_superuser(self): - request = self.factory.get("/") - request.user = self.superuser + with less_console_noise(): + request = self.factory.get("/") + request.user = self.superuser - readonly_fields = self.admin.get_readonly_fields(request) + readonly_fields = self.admin.get_readonly_fields(request) - expected_fields = [] + expected_fields = [] - self.assertEqual(readonly_fields, expected_fields) + self.assertEqual(readonly_fields, expected_fields) def test_change_view_for_joined_contact_five_or_less(self): """Create a contact, join it to 4 domain requests. The 5th join will be a user. Assert that the warning on the contact form lists 5 joins.""" - self.client.force_login(self.superuser) + with less_console_noise(): + self.client.force_login(self.superuser) - # Create an instance of the model - contact, _ = Contact.objects.get_or_create(user=self.staffuser) + # Create an instance of the model + contact, _ = Contact.objects.get_or_create(user=self.staffuser) - # join it to 4 domain requests. The 5th join will be a user. - application1 = completed_application(submitter=contact, name="city1.gov") - application2 = completed_application(submitter=contact, name="city2.gov") - application3 = completed_application(submitter=contact, name="city3.gov") - application4 = completed_application(submitter=contact, name="city4.gov") + # join it to 4 domain requests. The 5th join will be a user. + application1 = completed_application(submitter=contact, name="city1.gov") + application2 = completed_application(submitter=contact, name="city2.gov") + application3 = completed_application(submitter=contact, name="city3.gov") + application4 = completed_application(submitter=contact, name="city4.gov") - with patch("django.contrib.messages.warning") as mock_warning: - # Use the test client to simulate the request - response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) + with patch("django.contrib.messages.warning") as mock_warning: + # Use the test client to simulate the request + response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) - # Assert that the error message was called with the correct argument - # Note: The 5th join will be a user. - mock_warning.assert_called_once_with( - response.wsgi_request, - "", - ) + # Assert that the error message was called with the correct argument + # Note: The 5th join will be a user. + mock_warning.assert_called_once_with( + response.wsgi_request, + "", + ) def test_change_view_for_joined_contact_five_or_more(self): """Create a contact, join it to 5 domain requests. The 6th join will be a user. @@ -1984,20 +2071,21 @@ class VerifiedByStaffAdminTestCase(TestCase): self.factory = RequestFactory() def test_save_model_sets_user_field(self): - self.client.force_login(self.superuser) + with less_console_noise(): + self.client.force_login(self.superuser) - # Create an instance of the admin class - admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None) + # Create an instance of the admin class + admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None) - # Create a VerifiedByStaff instance - vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") + # Create a VerifiedByStaff instance + vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") - # Create a request object - request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/") - request.user = self.superuser + # Create a request object + request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/") + request.user = self.superuser - # Call the save_model method - admin_instance.save_model(request, vip_instance, None, None) + # Call the save_model method + admin_instance.save_model(request, vip_instance, None, None) - # Check that the user field is set to the request.user - self.assertEqual(vip_instance.requestor, self.superuser) + # Check that the user field is set to the request.user + self.assertEqual(vip_instance.requestor, self.superuser) From 5b2ca61de64589548810ef1bbd045172820a78a6 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 20 Feb 2024 15:30:54 -0500 Subject: [PATCH 08/13] Refactor JS to account for changes to select then back/forward navigation --- src/registrar/assets/js/get-gov-admin.js | 27 ++++++++++++++++++------ src/registrar/tests/test_admin.py | 17 --------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index b5f5a6aaa..803443bc9 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -317,22 +317,37 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, * status select amd to show/hide the rejection reason */ (function (){ - - // Get the rejection reason form row let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') if (rejectionReasonFormGroup) { - // Get the status select let statusSelect = document.getElementById('id_status') - // If status is rejected, hide the rejection reason on load + // Initial handling of rejectionReasonFormGroup display if (statusSelect.value != 'rejected') rejectionReasonFormGroup.style.display = 'none'; - // Listen to status changes and toggle rejection reason + // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage statusSelect.addEventListener('change', function() { - rejectionReasonFormGroup.style.display = statusSelect.value !== 'rejected' ? 'none' : 'block'; + if (statusSelect.value == 'rejected') { + rejectionReasonFormGroup.style.display = 'block'; + sessionStorage.removeItem('hideRejectionReason'); + } else { + rejectionReasonFormGroup.style.display = 'none'; + sessionStorage.setItem('hideRejectionReason', 'true'); + } }); } + // Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage + const observer = new PerformanceObserver((list) => { + list.getEntries().forEach((entry) => { + if (entry.type === "back_forward") { + if (sessionStorage.getItem('hideRejectionReason')) + document.querySelector('.field-rejection_reason').style.display = 'none'; + else + document.querySelector('.field-rejection_reason').style.display = 'block'; + } + }); + }); + observer.observe({ type: "navigation" }); })(); diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 484898be4..779861c67 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -699,23 +699,6 @@ class TestDomainApplicationAdmin(MockEppLib): application.refresh_from_db() self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED) - def test_save_model_clear_rejected_reason(self): - """When transitioning from rejected on a domain request, - the rejected_reason is cleared.""" - - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED) - application.rejected_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE - application.save() - - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.approve() - - application.refresh_from_db() - self.assertEqual(application.rejected_reason, None) - - def test_save_model_sends_withdrawn_email(self): """When transitioning to withdrawn on a domain request, an email is sent out every time.""" From 7e9899dd6d0f92868eda3603091874b5c8f017d4 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 20 Feb 2024 15:44:45 -0500 Subject: [PATCH 09/13] lint and cleanup --- src/registrar/tests/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 8a7c1c0a4..893678848 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -582,8 +582,8 @@ class TestDomainApplicationAdmin(MockEppLib): application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR ) self.assert_email_is_accurate( - "Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain " - "on behalf of Testorg", + "Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov " + "domain on behalf of Testorg", 0, EMAIL, ) From ad591086bed23f7550ad2e625ca4e121b9c44a31 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 20 Feb 2024 17:26:30 -0500 Subject: [PATCH 10/13] Add rejection reasons filter --- src/registrar/admin.py | 2 +- src/registrar/tests/test_admin.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c75588a27..1b13d8e74 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -779,7 +779,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): ] # Filters - list_filter = ("status", "organization_type", InvestigatorFilter) + list_filter = ("status", "organization_type", "rejection_reason", InvestigatorFilter) # Search search_fields = [ diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 893678848..69e756026 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1065,7 +1065,12 @@ class TestDomainApplicationAdmin(MockEppLib): # Grab the current list of table filters readonly_fields = self.admin.get_list_filter(request) - expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) + expected_fields = ( + "status", + "organization_type", + "rejection_reason", + DomainApplicationAdmin.InvestigatorFilter, + ) self.assertEqual(readonly_fields, expected_fields) From d9b56957c5ea956130dee050fbd97004d2891c28 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 22 Feb 2024 19:32:25 -0500 Subject: [PATCH 11/13] add the other rejection reason --- src/registrar/assets/js/get-gov-admin.js | 4 +++ ...0070_domainapplication_rejection_reason.py | 1 + src/registrar/models/domain_application.py | 1 + .../emails/status_change_rejected.txt | 16 +++++++++-- src/registrar/tests/test_admin.py | 28 +++++++++++++++++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 803443bc9..9239a0488 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -339,6 +339,10 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, } // Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage + + // When you navigate using forward/back after changing status but not saving, when you land back on the DA page the + // status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide + // accurately for this edge case, we use cache and test for the back/forward navigation. const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (entry.type === "back_forward") { diff --git a/src/registrar/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py index defeb4d95..60dc8ac73 100644 --- a/src/registrar/migrations/0070_domainapplication_rejection_reason.py +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -27,6 +27,7 @@ class Migration(migrations.Migration): ), ("organization_eligibility", "Organization isn't eligible for a .gov"), ("naming_requirements", "Naming requirements not met"), + ("other", "Other"), ], null=True, ), diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index ca6a996ce..609b9df33 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -363,6 +363,7 @@ class DomainApplication(TimeStampedModel): ) ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met" + OTHER = "other", "Other" # #### Internal fields about the application ##### status = FSMField( diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index 4beacf25a..3dae38c0d 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -8,8 +8,9 @@ REQUEST RECEIVED ON: {{ application.submission_date|date }} STATUS: Rejected ---------------------------------------------------------------- - -REJECTION REASON{% if application.rejection_reason == 'domain_purpose' %} +{% if application.rejection_reason != 'other' %} +REJECTION REASON{% endif %} +{% if application.rejection_reason == 'domain_purpose' %} Your domain request was rejected because the purpose you provided did not meet our requirements. You didn’t provide enough information about how you intend to use the domain. @@ -63,6 +64,17 @@ general public. Learn more about naming requirements for your type of organizati YOU CAN SUBMIT A NEW REQUEST We encourage you to request a domain that meets our requirements. If you have questions or want to discuss potential domain names, reply to this email. +{% elif application.rejection_reason == 'other' %} +YOU CAN SUBMIT A NEW REQUEST +If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request. + +Learn more about: +- Eligibility for a .gov domain +- Choosing a .gov domain name + + +NEED ASSISTANCE? +If you have questions about this domain request or need help choosing a new domain name, reply to this email. {% endif %} THANK YOU diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 69e756026..4e449ca5d 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -712,6 +712,34 @@ class TestDomainApplicationAdmin(MockEppLib): self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_save_model_sends_rejected_email_other(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is other.""" + + with less_console_noise(): + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name + self.transition_state_and_send_email( + application, + DomainApplication.ApplicationStatus.REJECTED, + DomainApplication.RejectionReasons.OTHER, + ) + self.assert_email_is_accurate( + "Choosing a .gov domain name", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self): """ When transitioning to rejected without a rejection reason, admin throws a user friendly message. From 0dc66303bfb0f60aa8fd4f360836be65074c973b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 22 Feb 2024 20:13:55 -0500 Subject: [PATCH 12/13] lint --- src/registrar/tests/test_admin.py | 105 +----------------------------- 1 file changed, 1 insertion(+), 104 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 4225f82c5..341a8fac9 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -905,9 +905,7 @@ class TestDomainApplicationAdmin(MockEppLib): DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.OTHER, ) - self.assert_email_is_accurate( - "Choosing a .gov domain name", 0, EMAIL - ) + self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) # Approve @@ -2065,107 +2063,6 @@ class AuditedAdminTest(TestCase): "{} is not ordered alphabetically".format(field.name), ) - def test_alphabetically_sorted_fk_fields_domain_information(self): - tested_fields = [ - DomainInformation.authorizing_official.field, - DomainInformation.submitter.field, - # DomainInformation.creator.field, - (DomainInformation.domain.field, ["name"]), - (DomainInformation.domain_application.field, ["requested_domain__name"]), - ] - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("information") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInformation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - isOtherOrderfield: bool = isinstance(field, tuple) - field_obj = None - if isOtherOrderfield: - sorted_fields = field[1] - field_obj = field[0] - else: - sorted_fields = ["first_name", "last_name"] - field_obj = field - # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper(model_admin, request, field_obj.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field_obj, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for obj in current_sort_order: - last = None - if not isOtherOrderfield: - first = obj.first_name - last = obj.last_name - elif field_obj == DomainInformation.domain.field: - first = obj.name - elif field_obj == DomainInformation.domain_application.field: - first = obj.requested_domain.name - - name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field_obj.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_invitation(self): - tested_fields = [DomainInvitation.domain.field] - - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("invitation") - - # Create a mock request - request = self.factory.post("/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)) - - model_admin = AuditedAdmin(DomainInvitation, self.site) - - sorted_fields = [] - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - sorted_fields = ["name"] - # We want both of these to be lists, as it is richer test wise. - - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) - current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - first = contact.name - last = None - - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple is not None: - current_sort_order_coerced_type.append(name_tuple) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) - def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): """Handles edge cases for test cases""" if first_name is None: From d860bbdb8b5714296305d0091c16bdeeac901213 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 26 Feb 2024 17:28:29 -0500 Subject: [PATCH 13/13] Tweak email templates, model copy --- ...0070_domainapplication_rejection_reason.py | 22 +++++++------------ src/registrar/models/domain_application.py | 18 +++++++-------- .../emails/status_change_rejected.txt | 12 +++++----- src/registrar/tests/test_admin.py | 4 ++-- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/registrar/migrations/0070_domainapplication_rejection_reason.py b/src/registrar/migrations/0070_domainapplication_rejection_reason.py index 60dc8ac73..d559973e2 100644 --- a/src/registrar/migrations/0070_domainapplication_rejection_reason.py +++ b/src/registrar/migrations/0070_domainapplication_rejection_reason.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-02-16 19:36 +# Generated by Django 4.2.7 on 2024-02-26 22:12 from django.db import migrations, models @@ -15,19 +15,13 @@ class Migration(migrations.Migration): field=models.TextField( blank=True, choices=[ - ("domain_purpose", "Domain purpose requirements not met"), - ("requestor", "Requestor isn't authorized to make the request"), - ( - "second_domain_reasoning", - "Organization already has a domain and does not provide sufficient reasoning for a second domain", - ), - ( - "contacts_or_organization_legitimacy", - "Research could not corroborate legitimacy of contacts or organization", - ), - ("organization_eligibility", "Organization isn't eligible for a .gov"), - ("naming_requirements", "Naming requirements not met"), - ("other", "Other"), + ("purpose_not_met", "Purpose requirements not met"), + ("requestor_not_eligible", "Requestor not eligible to make request"), + ("org_has_domain", "Org already has a .gov domain"), + ("contacts_not_verified", "Org contacts couldn't be verified"), + ("org_not_eligible", "Org not eligible for a .gov domain"), + ("naming_not_met", "Naming requirements not met"), + ("other", "Other/Unspecified"), ], null=True, ), diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 11b456337..1d0a9b332 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -352,19 +352,19 @@ class DomainApplication(TimeStampedModel): AGENCY_CHOICES = [(v, v) for v in AGENCIES] class RejectionReasons(models.TextChoices): - DOMAIN_PURPOSE = "domain_purpose", "Domain purpose requirements not met" - REQUESTOR = "requestor", "Requestor isn't authorized to make the request" + DOMAIN_PURPOSE = "purpose_not_met", "Purpose requirements not met" + REQUESTOR = "requestor_not_eligible", "Requestor not eligible to make request" SECOND_DOMAIN_REASONING = ( - "second_domain_reasoning", - "Organization already has a domain and does not provide sufficient reasoning for a second domain", + "org_has_domain", + "Org already has a .gov domain", ) CONTACTS_OR_ORGANIZATION_LEGITIMACY = ( - "contacts_or_organization_legitimacy", - "Research could not corroborate legitimacy of contacts or organization", + "contacts_not_verified", + "Org contacts couldn't be verified", ) - ORGANIZATION_ELIGIBILITY = "organization_eligibility", "Organization isn't eligible for a .gov" - NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met" - OTHER = "other", "Other" + ORGANIZATION_ELIGIBILITY = "org_not_eligible", "Org not eligible for a .gov domain" + NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met" + OTHER = "other", "Other/Unspecified" # #### Internal fields about the application ##### status = FSMField( diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index b9c89be07..0ece4b8ed 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -9,7 +9,7 @@ STATUS: Rejected ---------------------------------------------------------------- {% if application.rejection_reason != 'other' %} -REJECTION REASON{% endif %}{% if application.rejection_reason == 'domain_purpose' %} +REJECTION REASON{% endif %}{% if application.rejection_reason == 'purpose_not_met' %} Your domain request was rejected because the purpose you provided did not meet our requirements. You didn’t provide enough information about how you intend to use the domain. @@ -18,7 +18,7 @@ Learn more about: - Eligibility for a .gov domain - What you can and can’t do with .gov domains -If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'requestor' %} +If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'requestor_not_eligible' %} Your domain request was rejected because we don’t believe you’re eligible to request a .gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be working on behalf of a government organization, to request a .gov domain. @@ -26,7 +26,7 @@ working on behalf of a government organization, to request a .gov domain. DEMONSTRATE ELIGIBILITY If you can provide more information that demonstrates your eligibility, or you want to -discuss further, reply to this email.{% elif application.rejection_reason == 'second_domain_reasoning' %} +discuss further, reply to this email.{% elif application.rejection_reason == 'org_has_domain' %} Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our practice is to approve one domain per online service per government organization. We evaluate additional requests on a case-by-case basis. You did not provide sufficient @@ -35,9 +35,9 @@ justification for an additional domain. Read more about our practice of approving one domain per online service . -If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'contacts_or_organization_legitimacy' %} +If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'contacts_not_verified' %} Your domain request was rejected because we could not verify the organizational -contacts you provided. If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'organization_eligibility' %} +contacts you provided. If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'org_not_eligible' %} Your domain request was rejected because we determined that {{ application.organization_name }} is not eligible for a .gov domain. .Gov domains are only available to official U.S.-based government organizations. @@ -48,7 +48,7 @@ If you can provide documentation that demonstrates your eligibility, reply to th This can include links to (or copies of) your authorizing legislation, your founding charter or bylaws, or other similar documentation. Without this, we can’t approve a .gov domain for your organization. Learn more about eligibility for .gov domains -.{% elif application.rejection_reason == 'naming_requirements' %} +.{% elif application.rejection_reason == 'naming_not_met' %} Your domain request was rejected because it does not meet our naming requirements. Domains should uniquely identify a government organization and be clear to the general public. Learn more about naming requirements for your type of organization diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f88721649..7bef239fc 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -784,7 +784,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - def test_save_model_sends_rejected_email_domain_purpose(self): + def test_save_model_sends_rejected_email_purpose_not_met(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is domain purpose.""" @@ -843,7 +843,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - def test_save_model_sends_rejected_email_second_domain_reasoning(self): + def test_save_model_sends_rejected_email_org_has_domain(self): """When transitioning to rejected on a domain request, an email is sent explaining why when the reason is second domain."""