Updated Anything Else page to Additional Details page

This commit is contained in:
CocoByte 2024-04-10 16:20:57 -06:00
parent 6d13614521
commit 8f27aa1010
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
10 changed files with 314 additions and 71 deletions

View file

@ -369,7 +369,7 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
} }
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request /** 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 (){ (function (){
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')

View file

@ -193,6 +193,65 @@ function clearValidators(el) {
toggleInputValidity(el, true); 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. // 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 * An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
* *
*/ */
(function otherContactsFormListener() { (function otherContactsFormListener() {
// Get the radio buttons HookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees')
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();
}
})(); })();
/**
* 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)
})();

View file

@ -46,7 +46,7 @@ for step, view in [
(Step.PURPOSE, views.Purpose), (Step.PURPOSE, views.Purpose),
(Step.YOUR_CONTACT, views.YourContact), (Step.YOUR_CONTACT, views.YourContact),
(Step.OTHER_CONTACTS, views.OtherContacts), (Step.OTHER_CONTACTS, views.OtherContacts),
(Step.ANYTHING_ELSE, views.AnythingElse), (Step.ANYTHING_ELSE, views.AdditionalDetails),
(Step.REQUIREMENTS, views.Requirements), (Step.REQUIREMENTS, views.Requirements),
(Step.REVIEW, views.Review), (Step.REVIEW, views.Review),
]: ]:

View file

@ -757,28 +757,15 @@ OtherContactsFormSet = forms.formset_factory(
formset=BaseOtherContactsFormSet, formset=BaseOtherContactsFormSet,
) )
class BaseDeletableRegistrarForm(RegistrarForm):
class NoOtherContactsForm(RegistrarForm): """Adds special validation and delete functionality.
no_other_contacts_rationale = forms.CharField( Used by forms that are tied to a Yes/No form."""
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.")},
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.form_data_marked_for_deletion = False self.form_data_marked_for_deletion = False
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def mark_form_for_deletion(self): 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 This changes behavior of validity checks and to_database
methods.""" methods."""
self.form_data_marked_for_deletion = True self.form_data_marked_for_deletion = True
@ -822,8 +809,53 @@ class NoOtherContactsForm(RegistrarForm):
setattr(obj, name, value) setattr(obj, name, value)
obj.save() 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( anything_else = forms.CharField(
required=False, required=False,
label="Anything else?", 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): class RequirementsForm(RegistrarForm):
is_policy_acknowledged = forms.BooleanField( is_policy_acknowledged = forms.BooleanField(

View file

@ -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),
),
]

View file

@ -198,6 +198,12 @@ class DomainInformation(TimeStampedModel):
help_text="Anything else?", help_text="Anything else?",
) )
cisa_representative_email = models.EmailField(
null=True,
blank=True,
db_index=True,
)
is_policy_acknowledged = models.BooleanField( is_policy_acknowledged = models.BooleanField(
null=True, null=True,
blank=True, blank=True,

View file

@ -566,6 +566,12 @@ class DomainRequest(TimeStampedModel):
help_text="Anything else?", help_text="Anything else?",
) )
cisa_representative_email = models.EmailField(
null=True,
blank=True,
db_index=True,
)
is_policy_acknowledged = models.BooleanField( is_policy_acknowledged = models.BooleanField(
null=True, null=True,
blank=True, blank=True,
@ -923,6 +929,14 @@ class DomainRequest(TimeStampedModel):
def has_other_contacts(self) -> bool: def has_other_contacts(self) -> bool:
"""Does this domain request have other contacts listed?""" """Does this domain request have other contacts listed?"""
return self.other_contacts.exists() 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]: def is_federal(self) -> Union[bool, None]:
"""Is this domain request for a federal agency? """Is this domain request for a federal agency?

View file

@ -0,0 +1,56 @@
{% extends 'domain_request_form.html' %}
{% load static field_helpers %}
{% block form_instructions %}
<p><i>These questions are required (*).</i></p>
{% endblock %}
{% block form_required_fields_help_text %}
{# commented out so it does not appear at this point on this page #}
{% endblock %}
<!-- TODO-NL: (refactor) Breakup into two separate components-->
{% block form_fields %}
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Are you working with anyone from CISA regions on your domain request?</h2>
<p>.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has 10 regions that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.</p>
</legend>
<!-- Toggle -->
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.0.has_cisa_representative %}
{% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
<!-- TODO-NL: Hookup forms.0 to yes/no form for cisa representative (backend def)-->
</fieldset>
<div id="cisa-representative" class="cisa-representative-form">
<p>Your representatives email (*)</p>
{% input_with_errors forms.1.cisa_representative_email %}
{# forms.1 is a form for inputting the e-mail of a cisa representative #}
<!-- TODO-NL: Hookup forms.1 to cisa representative form (backend def) -->
</div>
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Is there anything else youd like us to know about your domain request?</h2>
</legend>
<!-- Toggle -->
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.2.has_anything_else_text %}
{% endwith %}
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
<!-- TODO-NL: Hookup forms.2 to yes/no form for anything else form (backend def)-->
</fieldset>
<div id="anything-else">
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.3.anything_else %}
{% endwith %}
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
<!-- TODO-NL: Hookup forms.3 to anything else form (backend def) -->
</div>
{% endblock %}

View file

@ -15,7 +15,7 @@ from registrar.forms.domain_request_wizard import (
RequirementsForm, RequirementsForm,
TribalGovernmentForm, TribalGovernmentForm,
PurposeForm, PurposeForm,
AnythingElseForm, AdditionalDetailsForm,
AboutYourOrganizationForm, AboutYourOrganizationForm,
) )
from registrar.forms.domain import ContactForm from registrar.forms.domain import ContactForm
@ -274,7 +274,7 @@ class TestFormValidation(MockEppLib):
def test_anything_else_form_about_your_organization_character_count_invalid(self): def test_anything_else_form_about_your_organization_character_count_invalid(self):
"""Response must be less than 2000 characters.""" """Response must be less than 2000 characters."""
form = AnythingElseForm( form = AdditionalDetailsForm(
data={ data={
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami" "anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
"shankle, drumstick doner chicken landjaeger turkey andouille." "shankle, drumstick doner chicken landjaeger turkey andouille."

View file

@ -580,10 +580,68 @@ class OtherContacts(DomainRequestWizard):
all_forms_valid = False all_forms_valid = False
return all_forms_valid return all_forms_valid
#DONE-NL: rename this to "Additional Details" (note: this is a find-replace job. VS will not refactor properly)
class AdditionalDetails(DomainRequestWizard):
class AnythingElse(DomainRequestWizard): # TODO-NL: Delete this old (original code for anything else)
template_name = "domain_request_anything_else.html" # template_name = "domain_request_anything_else.html"
forms = [forms.AnythingElseForm] # forms = [forms.AdditionalDetailsForm]
template_name = "domain_request_additional_details.html"
# OLD: forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]
# TODO-NL: (refactor) -- move form hookups into respective areas
forms = [forms.CisaRepresentativeYesNoForm, forms.CisaRepresentativeForm, forms.AdditionalDetailsYesNoForm, forms.AdditionalDetailsForm]
# TODO-NL: (refactor) -- move validation into respective areas
def is_valid(self, forms: list) -> bool:
# Validate Cisa Representative
"""Overrides default behavior defined in DomainRequestWizard.
Depending on value in yes_no forms, marks corresponding data
for deletion. Then validates all forms.
"""
cisa_representative_email_yes_no_form = forms[0]
cisa_representative_email_form = forms[1]
anything_else_yes_no_form = forms[2]
anything_else_form = forms[3]
# ------- Validate cisa representative -------
cisa_rep_portion_is_valid = True
# test first for yes_no_form validity
if cisa_representative_email_yes_no_form.is_valid():
# test for existing data
if not cisa_representative_email_yes_no_form.cleaned_data.get("has_cisa_representative"):
# mark the cisa_representative_email_form for deletion
cisa_representative_email_form.mark_form_for_deletion()
else:
cisa_rep_portion_is_valid = cisa_representative_email_form.is_valid()
else:
# if yes no form is invalid, no choice has been made
# mark the cisa_representative_email_form for deletion
cisa_representative_email_form.mark_form_for_deletion()
cisa_rep_portion_is_valid = False
# ------- Validate anything else -------
anything_else_portion_is_valid = True
# test first for yes_no_form validity
if anything_else_yes_no_form.is_valid():
# test for existing data
if not anything_else_yes_no_form.cleaned_data.get("has_anything_else_text"):
# mark the anything_else_form for deletion
anything_else_form.mark_form_for_deletion()
else:
anything_else_portion_is_valid = cisa_representative_email_form.is_valid()
else:
# if yes no form is invalid, no choice has been made
# mark the anything_else_form for deletion
anything_else_form.mark_form_for_deletion()
anything_else_portion_is_valid = False
# ------- Return combined validation result -------
all_forms_valid = cisa_rep_portion_is_valid and anything_else_portion_is_valid
return all_forms_valid
class Requirements(DomainRequestWizard): class Requirements(DomainRequestWizard):