diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 9a92542b1..0ecec92c6 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -369,7 +369,7 @@ 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 + * status select and to show/hide the rejection reason */ (function (){ let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 587b95305..a8e6008ef 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -193,6 +193,65 @@ function clearValidators(el) { toggleInputValidity(el, true); } +/** Hookup listeners for yes/no togglers for form fields + * Parameters: + * - radioButtonName: The "name=" value for the radio buttons being used as togglers + * - elementIdToShowIfYes: The Id of the element (eg. a div) to show if selected value of the given + * radio button is true (hides this element if false) + * - elementIdToShowIfNo: The Id of the element (eg. a div) to show if selected value of the given + * radio button is false (hides this element if true) + * **/ +function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToShowIfNo) { + // Get the radio buttons + let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]'); + + function handleRadioButtonChange() { + // Check the value of the selected radio button + // Attempt to find the radio button element that is checked + let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked'); + + // Check if the element exists before accessing its value + let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; + + switch (selectedValue) { + case 'True': + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1); + break; + + case 'False': + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2); + break; + + default: + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0); + } + } + + if (radioButtons.length) { + // Add event listener to each radio button + radioButtons.forEach(function (radioButton) { + radioButton.addEventListener('change', handleRadioButtonChange); + }); + + // initialize + handleRadioButtonChange(); + } +} + +// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle +function toggleTwoDomElements(ele1, ele2, index) { + let element1 = document.getElementById(ele1); + let element2 = document.getElementById(ele2); + if (element1 || element2) { + // Toggle display based on the index + if (element1) {element1.style.display = index === 1 ? 'block' : 'none';} + if (element2) {element2.style.display = index === 2 ? 'block' : 'none';} + } + else { + console.error('Unable to find elements to toggle'); + } +} + // <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> // Event handlers. @@ -712,57 +771,29 @@ function hideDeletedForms() { } })(); -// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle -function toggleTwoDomElements(ele1, ele2, index) { - let element1 = document.getElementById(ele1); - let element2 = document.getElementById(ele2); - if (element1 && element2) { - // Toggle display based on the index - element1.style.display = index === 1 ? 'block' : 'none'; - element2.style.display = index === 2 ? 'block' : 'none'; - } else { - console.error('One or both elements not found.'); - } -} /** * An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms * */ (function otherContactsFormListener() { - // Get the radio buttons - let radioButtons = document.querySelectorAll('input[name="other_contacts-has_other_contacts"]'); - - function handleRadioButtonChange() { - // Check the value of the selected radio button - // Attempt to find the radio button element that is checked - let radioButtonChecked = document.querySelector('input[name="other_contacts-has_other_contacts"]:checked'); - - // Check if the element exists before accessing its value - let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; - - switch (selectedValue) { - case 'True': - toggleTwoDomElements('other-employees', 'no-other-employees', 1); - break; - - case 'False': - toggleTwoDomElements('other-employees', 'no-other-employees', 2); - break; - - default: - toggleTwoDomElements('other-employees', 'no-other-employees', 0); - } - } - - if (radioButtons.length) { - // Add event listener to each radio button - radioButtons.forEach(function (radioButton) { - radioButton.addEventListener('change', handleRadioButtonChange); - }); - - // initialize - handleRadioButtonChange(); - } + HookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees') })(); + +/** + * An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly + * + */ +(function anythingElseFormListener() { + HookupYesNoListener("anything_else-has_anything_else_text",'anything-else', null) +})(); + + +/** + * An IIFE that listens to the yes/no radio buttons on the CISA representatives form and toggles form field visibility accordingly + * + */ +(function cisaRepresentativesFormListener() { + HookupYesNoListener("anything_else-has_cisa_representative",'cisa-representative', null) +})(); \ No newline at end of file diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 3918fa087..4cc044771 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -46,7 +46,7 @@ for step, view in [ (Step.PURPOSE, views.Purpose), (Step.YOUR_CONTACT, views.YourContact), (Step.OTHER_CONTACTS, views.OtherContacts), - (Step.ANYTHING_ELSE, views.AnythingElse), + (Step.ANYTHING_ELSE, views.AdditionalDetails), (Step.REQUIREMENTS, views.Requirements), (Step.REVIEW, views.Review), ]: diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 1efc028f6..9193015e4 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -757,28 +757,15 @@ OtherContactsFormSet = forms.formset_factory( formset=BaseOtherContactsFormSet, ) - -class NoOtherContactsForm(RegistrarForm): - no_other_contacts_rationale = forms.CharField( - required=True, - # label has to end in a space to get the label_suffix to show - label=("No other employees rationale"), - widget=forms.Textarea(), - validators=[ - MaxLengthValidator( - 1000, - message="Response must be less than 1000 characters.", - ) - ], - error_messages={"required": ("Rationale for no other employees is required.")}, - ) - +class BaseDeletableRegistrarForm(RegistrarForm): + """Adds special validation and delete functionality. + Used by forms that are tied to a Yes/No form.""" def __init__(self, *args, **kwargs): self.form_data_marked_for_deletion = False super().__init__(*args, **kwargs) def mark_form_for_deletion(self): - """Marks no_other_contacts form for deletion. + """Marks this form for deletion. This changes behavior of validity checks and to_database methods.""" self.form_data_marked_for_deletion = True @@ -822,8 +809,53 @@ class NoOtherContactsForm(RegistrarForm): setattr(obj, name, value) obj.save() +class NoOtherContactsForm(BaseDeletableRegistrarForm): + no_other_contacts_rationale = forms.CharField( + required=True, + # label has to end in a space to get the label_suffix to show + label=("No other employees rationale"), + widget=forms.Textarea(), + validators=[ + MaxLengthValidator( + 1000, + message="Response must be less than 1000 characters.", + ) + ], + error_messages={"required": ("Rationale for no other employees is required.")}, + ) -class AnythingElseForm(RegistrarForm): +class CisaRepresentativeForm(BaseDeletableRegistrarForm): + cisa_representative_email = forms.EmailField( + required=False, + label="Are you working with a CISA representative?", #TODO-NL: (design check) - is this the right label? + ) + +class CisaRepresentativeYesNoForm(RegistrarForm): + def __init__(self, *args, **kwargs): + """Extend the initialization of the form from RegistrarForm __init__""" + super().__init__(*args, **kwargs) + # set the initial value based on attributes of domain request + if self.domain_request: + if self.domain_request.has_cisa_representative(): + initial_value = True + else: + initial_value = False + else: + # No pre-selection for new domain requests + initial_value = None + + self.fields["has_cisa_representative"] = forms.TypedChoiceField( + coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None + choices=((True, "Yes"), (False, "No")), + initial=initial_value, + widget=forms.RadioSelect, + error_messages={ + "required": "This question is required.", + }, + ) + + +class AdditionalDetailsForm(BaseDeletableRegistrarForm): anything_else = forms.CharField( required=False, label="Anything else?", @@ -836,6 +868,29 @@ class AnythingElseForm(RegistrarForm): ], ) +class AdditionalDetailsYesNoForm(RegistrarForm): + def __init__(self, *args, **kwargs): + """Extend the initialization of the form from RegistrarForm __init__""" + super().__init__(*args, **kwargs) + # set the initial value based on attributes of domain request + if self.domain_request: + if self.domain_request.has_anything_else_text(): + initial_value = True + else: + initial_value = False + else: + # No pre-selection for new domain requests + initial_value = None + + self.fields["has_anything_else_text"] = forms.TypedChoiceField( + coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None + choices=((True, "Yes"), (False, "No")), + initial=initial_value, + widget=forms.RadioSelect, + error_messages={ + "required": "This question is required.", #TODO-NL: (design check) - is this required? + }, + ) class RequirementsForm(RegistrarForm): is_policy_acknowledged = forms.BooleanField( diff --git a/src/registrar/migrations/0082_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0082_domaininformation_cisa_representative_email_and_more.py new file mode 100644 index 000000000..b6be96eaa --- /dev/null +++ b/src/registrar/migrations/0082_domaininformation_cisa_representative_email_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.10 on 2024-04-10 22:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0081_create_groups_v10"), + ] + + operations = [ + migrations.AddField( + model_name="domaininformation", + name="cisa_representative_email", + field=models.EmailField(blank=True, db_index=True, max_length=254, null=True), + ), + migrations.AddField( + model_name="domainrequest", + name="cisa_representative_email", + field=models.EmailField(blank=True, db_index=True, max_length=254, null=True), + ), + ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index b5755a3c9..689aadc8a 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -198,6 +198,12 @@ class DomainInformation(TimeStampedModel): help_text="Anything else?", ) + cisa_representative_email = models.EmailField( + null=True, + blank=True, + db_index=True, + ) + is_policy_acknowledged = models.BooleanField( null=True, blank=True, diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index f4581de93..a9c4164d6 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -566,6 +566,12 @@ class DomainRequest(TimeStampedModel): help_text="Anything else?", ) + cisa_representative_email = models.EmailField( + null=True, + blank=True, + db_index=True, + ) + is_policy_acknowledged = models.BooleanField( null=True, blank=True, @@ -923,6 +929,14 @@ class DomainRequest(TimeStampedModel): def has_other_contacts(self) -> bool: """Does this domain request have other contacts listed?""" return self.other_contacts.exists() + + def has_anything_else_text(self) -> bool: + """Does this domain request have an 'anything else?' entry""" + return self.anything_else != "" and self.anything_else != None #TODO-NL: how to handle falsy strings again? + + def has_cisa_representative(self) -> bool: + """Does this domain request have cisa representative?""" + return self.cisa_representative_email != "" and self.cisa_representative_email != None def is_federal(self) -> Union[bool, None]: """Is this domain request for a federal agency? diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html new file mode 100644 index 000000000..430d47472 --- /dev/null +++ b/src/registrar/templates/domain_request_additional_details.html @@ -0,0 +1,56 @@ +{% extends 'domain_request_form.html' %} +{% load static field_helpers %} + +{% block form_instructions %} +
These questions are required (*).
+{% endblock %} + +{% block form_required_fields_help_text %} +{# commented out so it does not appear at this point on this page #} +{% endblock %} + + +{% block form_fields %} + + +Your representative’s email (*)
+ {% input_with_errors forms.1.cisa_representative_email %} + {# forms.1 is a form for inputting the e-mail of a cisa representative #} + +