diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 44babc0b1..6f4c7e75c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -217,6 +217,7 @@ class DomainRequestAdminForm(forms.ModelForm): status = cleaned_data.get("status") investigator = cleaned_data.get("investigator") rejection_reason = cleaned_data.get("rejection_reason") + action_needed_reason = cleaned_data.get("action_needed_reason") # Get the old status initial_status = self.initial.get("status", None) @@ -240,6 +241,8 @@ class DomainRequestAdminForm(forms.ModelForm): # If the status is rejected, a rejection reason must exist if status == DomainRequest.DomainRequestStatus.REJECTED: self._check_for_valid_rejection_reason(rejection_reason) + elif status == DomainRequest.DomainRequestStatus.ACTION_NEEDED: + self._check_for_valid_action_needed_reason(action_needed_reason) return cleaned_data @@ -263,6 +266,18 @@ class DomainRequestAdminForm(forms.ModelForm): return is_valid + def _check_for_valid_action_needed_reason(self, action_needed_reason) -> bool: + """ + Checks if the action_needed_reason field is not none. + Adds form errors on failure. + """ + is_valid = action_needed_reason is not None and action_needed_reason != "" + if not is_valid: + error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_ACTION_NEEDED_REASON) + self.add_error("action_needed_reason", error_message) + + return is_valid + def _check_for_valid_investigator(self, investigator) -> bool: """ Checks if the investigator field is not none, and is staff. @@ -1166,6 +1181,8 @@ class DomainInvitationAdmin(ListHeaderAdmin): # error. readonly_fields = ["status"] + autocomplete_fields = ["domain"] + change_form_template = "django/admin/email_clipboard_change_form.html" # Select domain invitations to change -> Domain invitations @@ -1466,6 +1483,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "fields": [ "status", "rejection_reason", + "action_needed_reason", "investigator", "creator", "submitter", @@ -1482,6 +1500,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "authorizing_official", "other_contacts", "no_other_contacts_rationale", + "cisa_representative_first_name", + "cisa_representative_last_name", "cisa_representative_email", ] }, @@ -1557,6 +1577,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "no_other_contacts_rationale", "anything_else", "is_policy_acknowledged", + "cisa_representative_first_name", + "cisa_representative_last_name", "cisa_representative_email", ] autocomplete_fields = [ @@ -1668,6 +1690,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # 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. error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_REJECTION_REASON) + elif obj.status == models.DomainRequest.DomainRequestStatus.ACTION_NEEDED and not obj.action_needed_reason: + error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_ACTION_NEEDED_REASON) else: # This is an fsm in model which will throw an error if the # transition condition is violated, so we roll back the diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index dea584ba0..bf38aca3d 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -333,42 +333,66 @@ function initializeWidgetOnList(list, parentId) { */ (function (){ let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') + let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason'); - if (rejectionReasonFormGroup) { + if (rejectionReasonFormGroup && actionNeededReasonFormGroup) { let statusSelect = document.getElementById('id_status') + let isRejected = statusSelect.value == "rejected" + let isActionNeeded = statusSelect.value == "action needed" // Initial handling of rejectionReasonFormGroup display - if (statusSelect.value != 'rejected') - rejectionReasonFormGroup.style.display = 'none'; + showOrHideObject(rejectionReasonFormGroup, show=isRejected) + showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded) // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage statusSelect.addEventListener('change', function() { - if (statusSelect.value == 'rejected') { - rejectionReasonFormGroup.style.display = 'block'; - sessionStorage.removeItem('hideRejectionReason'); - } else { - rejectionReasonFormGroup.style.display = 'none'; - sessionStorage.setItem('hideRejectionReason', 'true'); - } + // Show the rejection reason field if the status is rejected. + // Then track if its shown or hidden in our session cache. + isRejected = statusSelect.value == "rejected" + showOrHideObject(rejectionReasonFormGroup, show=isRejected) + addOrRemoveSessionBoolean("showRejectionReason", add=isRejected) + + isActionNeeded = statusSelect.value == "action needed" + showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded) + addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded) }); + + // 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") { + let showRejectionReason = sessionStorage.getItem("showRejectionReason") !== null + showOrHideObject(rejectionReasonFormGroup, show=showRejectionReason) + + let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null + showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason) + } + }); + }); + observer.observe({ type: "navigation" }); } - // Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage + // Adds or removes the display-none class to object depending on the value of boolean show + function showOrHideObject(object, show){ + if (show){ + object.classList.remove("display-none"); + }else { + object.classList.add("display-none"); + } + } - // When you navigate using forward/back after changing status but not saving, when you land back on the DA page the - // status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide - // accurately for this edge case, we use cache and test for the back/forward navigation. - const observer = new PerformanceObserver((list) => { - list.getEntries().forEach((entry) => { - if (entry.type === "back_forward") { - if (sessionStorage.getItem('hideRejectionReason')) - document.querySelector('.field-rejection_reason').style.display = 'none'; - else - document.querySelector('.field-rejection_reason').style.display = 'block'; - } - }); - }); - observer.observe({ type: "navigation" }); + // Adds or removes a boolean from our session + function addOrRemoveSessionBoolean(name, add){ + if (add) { + sessionStorage.setItem(name, "true"); + }else { + sessionStorage.removeItem(name); + } + } })(); /** An IIFE for toggling the submit bar on domain request forms diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index fc176ce82..61afd6b57 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -648,20 +648,27 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm): class CisaRepresentativeForm(BaseDeletableRegistrarForm): + cisa_representative_first_name = forms.CharField( + label="First name / given name", + error_messages={"required": "Enter the first name / given name of the CISA regional representative."}, + ) + cisa_representative_last_name = forms.CharField( + label="Last name / family name", + error_messages={"required": "Enter the last name / family name of the CISA regional representative."}, + ) cisa_representative_email = forms.EmailField( - required=True, + label="Your representative’s email (optional)", max_length=None, - label="Your representative’s email", + required=False, + error_messages={ + "invalid": ("Enter your representative’s email address in the required format, like name@example.com."), + }, validators=[ MaxLengthValidator( 320, message="Response must be less than 320 characters.", ) ], - error_messages={ - "invalid": ("Enter your email address in the required format, like name@example.com."), - "required": ("Enter the email address of your CISA regional representative."), - }, ) diff --git a/src/registrar/migrations/0100_domainrequest_action_needed_reason.py b/src/registrar/migrations/0100_domainrequest_action_needed_reason.py new file mode 100644 index 000000000..acd0ae2a2 --- /dev/null +++ b/src/registrar/migrations/0100_domainrequest_action_needed_reason.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.10 on 2024-06-12 14:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0099_federalagency_federal_type"), + ] + + operations = [ + migrations.AddField( + model_name="domainrequest", + name="action_needed_reason", + field=models.TextField( + blank=True, + choices=[ + ("eligibility_unclear", "Unclear organization eligibility"), + ("questionable_authorizing_official", "Questionable authorizing official"), + ("already_has_domains", "Already has domains"), + ("bad_name", "Doesn’t meet naming requirements"), + ("other", "Other (no auto-email sent)"), + ], + null=True, + ), + ), + ] diff --git a/src/registrar/migrations/0101_domaininformation_cisa_representative_first_name_and_more.py b/src/registrar/migrations/0101_domaininformation_cisa_representative_first_name_and_more.py new file mode 100644 index 000000000..89f24a5e1 --- /dev/null +++ b/src/registrar/migrations/0101_domaininformation_cisa_representative_first_name_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.10 on 2024-06-12 20:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0100_domainrequest_action_needed_reason"), + ] + + operations = [ + migrations.AddField( + model_name="domaininformation", + name="cisa_representative_first_name", + field=models.CharField( + blank=True, db_index=True, null=True, verbose_name="CISA regional representative first name" + ), + ), + migrations.AddField( + model_name="domaininformation", + name="cisa_representative_last_name", + field=models.CharField( + blank=True, db_index=True, null=True, verbose_name="CISA regional representative last name" + ), + ), + migrations.AddField( + model_name="domaininformation", + name="has_anything_else_text", + field=models.BooleanField( + blank=True, help_text="Determines if the user has a anything_else or not", null=True + ), + ), + migrations.AddField( + model_name="domaininformation", + name="has_cisa_representative", + field=models.BooleanField( + blank=True, help_text="Determines if the user has a representative email or not", null=True + ), + ), + migrations.AddField( + model_name="domainrequest", + name="cisa_representative_first_name", + field=models.CharField( + blank=True, db_index=True, null=True, verbose_name="CISA regional representative first name" + ), + ), + migrations.AddField( + model_name="domainrequest", + name="cisa_representative_last_name", + field=models.CharField( + blank=True, db_index=True, null=True, verbose_name="CISA regional representative last name" + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="cisa_representative_email", + field=models.EmailField( + blank=True, max_length=320, null=True, verbose_name="CISA regional representative email" + ), + ), + migrations.AlterField( + model_name="domainrequest", + name="cisa_representative_email", + field=models.EmailField( + blank=True, max_length=320, null=True, verbose_name="CISA regional representative email" + ), + ), + ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index bc35d4e30..62db04ac8 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -214,13 +214,45 @@ class DomainInformation(TimeStampedModel): verbose_name="Additional details", ) + # This is a drop-in replacement for a has_anything_else_text() function. + # In order to track if the user has clicked the yes/no field (while keeping a none default), we need + # a tertiary state. We should not display this in /admin. + has_anything_else_text = models.BooleanField( + null=True, + blank=True, + help_text="Determines if the user has a anything_else or not", + ) + cisa_representative_email = models.EmailField( null=True, blank=True, - verbose_name="CISA regional representative", + verbose_name="CISA regional representative email", max_length=320, ) + cisa_representative_first_name = models.CharField( + null=True, + blank=True, + verbose_name="CISA regional representative first name", + db_index=True, + ) + + cisa_representative_last_name = models.CharField( + null=True, + blank=True, + verbose_name="CISA regional representative last name", + db_index=True, + ) + + # This is a drop-in replacement for an has_cisa_representative() function. + # In order to track if the user has clicked the yes/no field (while keeping a none default), we need + # a tertiary state. We should not display this in /admin. + has_cisa_representative = models.BooleanField( + null=True, + blank=True, + help_text="Determines if the user has a representative email or not", + ) + is_policy_acknowledged = models.BooleanField( null=True, blank=True, @@ -241,6 +273,30 @@ class DomainInformation(TimeStampedModel): except Exception: return "" + def sync_yes_no_form_fields(self): + """Some yes/no forms use a db field to track whether it was checked or not. + We handle that here for def save(). + """ + # This ensures that if we have prefilled data, the form is prepopulated + if self.cisa_representative_first_name is not None or self.cisa_representative_last_name is not None: + self.has_cisa_representative = ( + self.cisa_representative_first_name != "" and self.cisa_representative_last_name != "" + ) + + # This check is required to ensure that the form doesn't start out checked + if self.has_cisa_representative is not None: + self.has_cisa_representative = ( + self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None + ) and (self.cisa_representative_last_name != "" and self.cisa_representative_last_name is not None) + + # This ensures that if we have prefilled data, the form is prepopulated + if self.anything_else is not None: + self.has_anything_else_text = self.anything_else != "" + + # This check is required to ensure that the form doesn't start out checked. + if self.has_anything_else_text is not None: + self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None + def sync_organization_type(self): """ Updates the organization_type (without saving) to match @@ -275,6 +331,7 @@ class DomainInformation(TimeStampedModel): def save(self, *args, **kwargs): """Save override for custom properties""" + self.sync_yes_no_form_fields() self.sync_organization_type() super().save(*args, **kwargs) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 46df4bc30..1ad8ae7b3 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1,6 +1,5 @@ from __future__ import annotations from typing import Union - import logging from django.apps import apps @@ -250,6 +249,15 @@ class DomainRequest(TimeStampedModel): NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met" OTHER = "other", "Other/Unspecified" + class ActionNeededReasons(models.TextChoices): + """Defines common action needed reasons for domain requests""" + + ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility") + QUESTIONABLE_AUTHORIZING_OFFICIAL = ("questionable_authorizing_official", "Questionable authorizing official") + ALREADY_HAS_DOMAINS = ("already_has_domains", "Already has domains") + BAD_NAME = ("bad_name", "Doesn’t meet naming requirements") + OTHER = ("other", "Other (no auto-email sent)") + # #### Internal fields about the domain request ##### status = FSMField( choices=DomainRequestStatus.choices, # possible states as an array of constants @@ -263,6 +271,12 @@ class DomainRequest(TimeStampedModel): blank=True, ) + action_needed_reason = models.TextField( + choices=ActionNeededReasons.choices, + null=True, + blank=True, + ) + federal_agency = models.ForeignKey( "registrar.FederalAgency", on_delete=models.PROTECT, @@ -463,10 +477,24 @@ class DomainRequest(TimeStampedModel): cisa_representative_email = models.EmailField( null=True, blank=True, - verbose_name="CISA regional representative", + verbose_name="CISA regional representative email", max_length=320, ) + cisa_representative_first_name = models.CharField( + null=True, + blank=True, + verbose_name="CISA regional representative first name", + db_index=True, + ) + + cisa_representative_last_name = models.CharField( + null=True, + blank=True, + verbose_name="CISA regional representative last name", + db_index=True, + ) + # This is a drop-in replacement for an has_cisa_representative() function. # In order to track if the user has clicked the yes/no field (while keeping a none default), we need # a tertiary state. We should not display this in /admin. @@ -525,6 +553,16 @@ class DomainRequest(TimeStampedModel): # Actually updates the organization_type field org_type_helper.create_or_update_organization_type() + def _cache_status_and_action_needed_reason(self): + """Maintains a cache of properties so we can avoid a DB call""" + self._cached_action_needed_reason = self.action_needed_reason + self._cached_status = self.status + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Store original values for caching purposes. Used to compare them on save. + self._cache_status_and_action_needed_reason() + def save(self, *args, **kwargs): """Save override for custom properties""" self.sync_organization_type() @@ -532,20 +570,38 @@ class DomainRequest(TimeStampedModel): super().save(*args, **kwargs) + # Handle the action needed email. We send one when moving to action_needed, + # but we don't send one when we are _already_ in the state and change the reason. + self.sync_action_needed_reason() + + # Update the cached values after saving + self._cache_status_and_action_needed_reason() + + def sync_action_needed_reason(self): + """Checks if we need to send another action needed email""" + was_already_action_needed = self._cached_status == self.DomainRequestStatus.ACTION_NEEDED + reason_exists = self._cached_action_needed_reason is not None and self.action_needed_reason is not None + reason_changed = self._cached_action_needed_reason != self.action_needed_reason + if was_already_action_needed and (reason_exists and reason_changed): + # We don't send emails out in state "other" + if self.action_needed_reason != self.ActionNeededReasons.OTHER: + self._send_action_needed_reason_email() + def sync_yes_no_form_fields(self): """Some yes/no forms use a db field to track whether it was checked or not. We handle that here for def save(). """ - # This ensures that if we have prefilled data, the form is prepopulated - if self.cisa_representative_email is not None: - self.has_cisa_representative = self.cisa_representative_email != "" + if self.cisa_representative_first_name is not None or self.cisa_representative_last_name is not None: + self.has_cisa_representative = ( + self.cisa_representative_first_name != "" and self.cisa_representative_last_name != "" + ) # This check is required to ensure that the form doesn't start out checked if self.has_cisa_representative is not None: self.has_cisa_representative = ( - self.cisa_representative_email != "" and self.cisa_representative_email is not None - ) + self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None + ) and (self.cisa_representative_last_name != "" and self.cisa_representative_last_name is not None) # This ensures that if we have prefilled data, the form is prepopulated if self.anything_else is not None: @@ -583,7 +639,7 @@ class DomainRequest(TimeStampedModel): logger.error(f"Can't query an approved domain while attempting {called_from}") def _send_status_update_email( - self, new_status, email_template, email_template_subject, send_email=True, bcc_address="" + self, new_status, email_template, email_template_subject, send_email=True, bcc_address="", wrap_email=False ): """Send a status update email to the submitter. @@ -610,6 +666,7 @@ class DomainRequest(TimeStampedModel): self.submitter.email, context={"domain_request": self}, bcc_address=bcc_address, + wrap_email=wrap_email, ) logger.info(f"The {new_status} email sent to: {self.submitter.email}") except EmailSendingError: @@ -693,9 +750,10 @@ class DomainRequest(TimeStampedModel): if self.status == self.DomainRequestStatus.APPROVED: self.delete_and_clean_up_domain("in_review") - - if self.status == self.DomainRequestStatus.REJECTED: + elif self.status == self.DomainRequestStatus.REJECTED: self.rejection_reason = None + elif self.status == self.DomainRequestStatus.ACTION_NEEDED: + self.action_needed_reason = None literal = DomainRequest.DomainRequestStatus.IN_REVIEW # Check if the tuple exists, then grab its value @@ -713,7 +771,7 @@ class DomainRequest(TimeStampedModel): target=DomainRequestStatus.ACTION_NEEDED, conditions=[domain_is_not_active, investigator_exists_and_is_staff], ) - def action_needed(self): + def action_needed(self, send_email=True): """Send back an domain request that is under investigation or rejected. This action is logged. @@ -725,8 +783,7 @@ class DomainRequest(TimeStampedModel): if self.status == self.DomainRequestStatus.APPROVED: self.delete_and_clean_up_domain("reject_with_prejudice") - - if self.status == self.DomainRequestStatus.REJECTED: + elif self.status == self.DomainRequestStatus.REJECTED: self.rejection_reason = None literal = DomainRequest.DomainRequestStatus.ACTION_NEEDED @@ -734,6 +791,46 @@ class DomainRequest(TimeStampedModel): action_needed = literal if literal is not None else "Action Needed" logger.info(f"A status change occurred. {self} was changed to '{action_needed}'") + # Send out an email if an action needed reason exists + if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER: + self._send_action_needed_reason_email(send_email) + + def _send_action_needed_reason_email(self, send_email=True): + """Sends out an automatic email for each valid action needed reason provided""" + + # Store the filenames of the template and template subject + email_template_name: str = "" + email_template_subject_name: str = "" + + # Check for the "type" of action needed reason. + can_send_email = True + match self.action_needed_reason: + # Add to this match if you need to pass in a custom filename for these templates. + case self.ActionNeededReasons.OTHER, _: + # Unknown and other are default cases - do nothing + can_send_email = False + + # Assumes that the template name matches the action needed reason if nothing is specified. + # This is so you can override if you need, or have this taken care of for you. + if not email_template_name and not email_template_subject_name: + email_template_name = f"{self.action_needed_reason}.txt" + email_template_subject_name = f"{self.action_needed_reason}_subject.txt" + + bcc_address = "" + if settings.IS_PRODUCTION: + bcc_address = settings.DEFAULT_FROM_EMAIL + + # If we can, try to send out an email as long as send_email=True + if can_send_email: + self._send_status_update_email( + new_status="action needed", + email_template=f"emails/action_needed_reasons/{email_template_name}", + email_template_subject=f"emails/action_needed_reasons/{email_template_subject_name}", + send_email=send_email, + bcc_address=bcc_address, + wrap_email=True, + ) + @transition( field="status", source=[ @@ -782,6 +879,8 @@ class DomainRequest(TimeStampedModel): if self.status == self.DomainRequestStatus.REJECTED: self.rejection_reason = None + elif self.status == self.DomainRequestStatus.ACTION_NEEDED: + self.action_needed_reason = None # == Send out an email == # self._send_status_update_email( @@ -900,11 +999,12 @@ class DomainRequest(TimeStampedModel): def has_additional_details(self) -> bool: """Combines the has_anything_else_text and has_cisa_representative fields, then returns if this domain request has either of them.""" - # Split out for linter - has_details = False - if self.has_anything_else_text or self.has_cisa_representative: - has_details = True + # Split out for linter + has_details = True + + if self.has_anything_else_text is None or self.has_cisa_representative is None: + has_details = False return has_details def is_federal(self) -> Union[bool, None]: @@ -1013,14 +1113,19 @@ class DomainRequest(TimeStampedModel): return True return False - def _cisa_rep_and_email_check(self): - # Has a CISA rep + email is NOT empty or NOT an empty string OR doesn't have CISA rep - return ( + def _cisa_rep_check(self): + # Either does not have a CISA rep, OR has a CISA rep + both first name + # and last name are NOT empty and are NOT an empty string + to_return = ( self.has_cisa_representative is True - and self.cisa_representative_email is not None - and self.cisa_representative_email != "" + and self.cisa_representative_first_name is not None + and self.cisa_representative_first_name != "" + and self.cisa_representative_last_name is not None + and self.cisa_representative_last_name != "" ) or self.has_cisa_representative is False + return to_return + def _anything_else_radio_button_and_text_field_check(self): # Anything else boolean is True + filled text field and it's not an empty string OR the boolean is No return ( @@ -1028,7 +1133,7 @@ class DomainRequest(TimeStampedModel): ) or self.has_anything_else_text is False def _is_additional_details_complete(self): - return self._cisa_rep_and_email_check() and self._anything_else_radio_button_and_text_field_check() + return self._cisa_rep_check() and self._anything_else_radio_button_and_text_field_check() def _is_policy_acknowledgement_complete(self): return self.is_policy_acknowledged is not None diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index c92de3c9d..09068b675 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -69,8 +69,8 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% block after_help_text %} {% if field.field.name == "status" and original_object.history.count > 0 %} -