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",