From 8f27aa101044bf5024e2875a3181ccbb1adcd7e6 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 10 Apr 2024 16:20:57 -0600 Subject: [PATCH 01/49] Updated Anything Else page to Additional Details page --- src/registrar/assets/js/get-gov-admin.js | 2 +- src/registrar/assets/js/get-gov.js | 123 +++++++++++------- src/registrar/config/urls.py | 2 +- src/registrar/forms/domain_request_wizard.py | 91 ++++++++++--- ...tion_cisa_representative_email_and_more.py | 23 ++++ src/registrar/models/domain_information.py | 6 + src/registrar/models/domain_request.py | 14 ++ .../domain_request_additional_details.html | 56 ++++++++ src/registrar/tests/test_forms.py | 4 +- src/registrar/views/domain_request.py | 64 ++++++++- 10 files changed, 314 insertions(+), 71 deletions(-) create mode 100644 src/registrar/migrations/0082_domaininformation_cisa_representative_email_and_more.py create mode 100644 src/registrar/templates/domain_request_additional_details.html 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 %} +
+ +

Are you working with anyone from CISA regions on your domain request?

+

.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.

+
+ + + {% 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 #} + +
+ +
+

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 #} + +
+ + +
+ +

Is there anything else you’d like us to know about your domain request?

+
+ + + {% 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 #} + +
+ +
+ {% 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 #} + +
+{% endblock %} diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index 4b8904f0c..c72c70e98 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -15,7 +15,7 @@ from registrar.forms.domain_request_wizard import ( RequirementsForm, TribalGovernmentForm, PurposeForm, - AnythingElseForm, + AdditionalDetailsForm, AboutYourOrganizationForm, ) 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): """Response must be less than 2000 characters.""" - form = AnythingElseForm( + form = AdditionalDetailsForm( data={ "anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami" "shankle, drumstick doner chicken landjaeger turkey andouille." diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 244b47602..9889b9b8a 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -580,10 +580,68 @@ class OtherContacts(DomainRequestWizard): all_forms_valid = False 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): - template_name = "domain_request_anything_else.html" - forms = [forms.AnythingElseForm] + # TODO-NL: Delete this old (original code for anything else) + # template_name = "domain_request_anything_else.html" + # 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): From e2f975f04fc3deee057df6e35fb101b6850fa361 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 10 Apr 2024 19:07:28 -0600 Subject: [PATCH 02/49] Fix migrations. Update Steps (side menu) --- src/registrar/config/urls.py | 2 +- ...domaininformation_cisa_representative_email_and_more.py} | 4 ++-- src/registrar/templates/domain_request_review.html | 2 +- src/registrar/views/domain_request.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/registrar/migrations/{0082_domaininformation_cisa_representative_email_and_more.py => 0084_domaininformation_cisa_representative_email_and_more.py} (82%) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 4cc044771..720034150 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.AdditionalDetails), + (Step.ADDITIONAL_DETAILS, views.AdditionalDetails), (Step.REQUIREMENTS, views.Requirements), (Step.REVIEW, views.Review), ]: diff --git a/src/registrar/migrations/0082_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py similarity index 82% rename from src/registrar/migrations/0082_domaininformation_cisa_representative_email_and_more.py rename to src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py index b6be96eaa..83b717b00 100644 --- a/src/registrar/migrations/0082_domaininformation_cisa_representative_email_and_more.py +++ b/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-10 22:19 +# Generated by Django 4.2.10 on 2024-04-11 00:43 from django.db import migrations, models @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("registrar", "0081_create_groups_v10"), + ("registrar", "0083_alter_contact_email_alter_publiccontact_email"), ] operations = [ diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index 227fc0cea..aeba9955c 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -155,7 +155,7 @@ {% endif %} - {% if step == Step.ANYTHING_ELSE %} + {% if step == Step.ADDITIONAL_DETAILS %} {% namespaced_url 'domain-request' step as domain_request_url %} {% with title=form_titles|get_item:step value=domain_request.anything_else|default:"No" %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 9889b9b8a..12043903b 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -45,7 +45,7 @@ class Step(StrEnum): PURPOSE = "purpose" YOUR_CONTACT = "your_contact" OTHER_CONTACTS = "other_contacts" - ANYTHING_ELSE = "anything_else" + ADDITIONAL_DETAILS = "anything_else" REQUIREMENTS = "requirements" REVIEW = "review" @@ -91,7 +91,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): Step.PURPOSE: _("Purpose of your domain"), Step.YOUR_CONTACT: _("Your contact information"), Step.OTHER_CONTACTS: _("Other employees from your organization"), - Step.ANYTHING_ELSE: _("Anything else?"), + Step.ADDITIONAL_DETAILS: _("Additional Details"), Step.REQUIREMENTS: _("Requirements for operating a .gov domain"), Step.REVIEW: _("Review and submit your domain request"), } @@ -366,7 +366,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): or self.domain_request.no_other_contacts_rationale is not None ), "anything_else": ( - self.domain_request.anything_else is not None or self.domain_request.is_policy_acknowledged is not None + (self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email) or self.domain_request.is_policy_acknowledged is not None ), "requirements": self.domain_request.is_policy_acknowledged is not None, "review": self.domain_request.is_policy_acknowledged is not None, From 3dfdb3bfa1621b51e1eef7b58c8b415967c26c3d Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 11 Apr 2024 13:24:17 -0600 Subject: [PATCH 03/49] Fixed summary sections to meet design requirements --- .../templates/domain_request_review.html | 5 +- .../templates/domain_request_status.html | 4 +- .../includes/summary_additional_details.html | 50 +++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/registrar/templates/includes/summary_additional_details.html diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index aeba9955c..09a930710 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -156,10 +156,7 @@ {% if step == Step.ADDITIONAL_DETAILS %} - {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.anything_else|default:"No" %} - {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} - {% endwith %} + {% include "includes/summary_additional_details.html" with domainRequest=DomainRequest %} {% endif %} diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index d3c1eab6d..cf9e7ffe3 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -115,8 +115,8 @@ {% else %} {% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %} {% endif %} - - {% include "includes/summary_item.html" with title='Anything else?' value=DomainRequest.anything_else|default:"No" heading_level=heading_level %} + + {% include "includes/summary_additional_details.html" with domainRequest=DomainRequest %} {% endwith %} diff --git a/src/registrar/templates/includes/summary_additional_details.html b/src/registrar/templates/includes/summary_additional_details.html new file mode 100644 index 000000000..e10b90384 --- /dev/null +++ b/src/registrar/templates/includes/summary_additional_details.html @@ -0,0 +1,50 @@ +{% load static url_helpers %} + +{# TODO (future ticket?): create a template that allows us to easily display nested forms. +It is a little complex because we will have to formulate how to aggregate form pairings. +(eg. yes/no radio forms plus the information they toggle need to be linked somehow +and condense down into one subsection)} + +
+ +
+
+

+ Additional Details +

+ + {% if domainRequest %} +
+
+ CISA regions representative +
+
+ {% if domainRequest.has_cisa_representative %} + domainRequest.cisa_representative_email + {% else %} + (none) + {% endif %} +
+
+ Anything else +
+
+ {% if domainRequest.has_anything_else_text %} + domainRequest.anything_else + {% else %} + No + {% endif %} +
+
+ {% else %} + ERROR Please contact technical support/dev + {% endif %} +
+
+
From 44c74c7c867015ad1ff09ca8a43fda47c5aea751 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 11 Apr 2024 14:04:15 -0600 Subject: [PATCH 04/49] Linted --- src/registrar/forms/domain_request_wizard.py | 13 ++++++++++--- src/registrar/models/domain_request.py | 8 ++++---- src/registrar/views/domain_request.py | 17 +++++++++++------ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 6f27876eb..6ddb68a74 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -778,9 +778,11 @@ OtherContactsFormSet = forms.formset_factory( formset=BaseOtherContactsFormSet, ) + 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) @@ -830,6 +832,7 @@ class BaseDeletableRegistrarForm(RegistrarForm): setattr(obj, name, value) obj.save() + class NoOtherContactsForm(BaseDeletableRegistrarForm): no_other_contacts_rationale = forms.CharField( required=True, @@ -845,12 +848,14 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm): error_messages={"required": ("Rationale for no other employees is required.")}, ) + 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? + 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__""" @@ -871,7 +876,7 @@ class CisaRepresentativeYesNoForm(RegistrarForm): initial=initial_value, widget=forms.RadioSelect, error_messages={ - "required": "This question is required.", + "required": "This question is required.", }, ) @@ -889,6 +894,7 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm): ], ) + class AdditionalDetailsYesNoForm(RegistrarForm): def __init__(self, *args, **kwargs): """Extend the initialization of the form from RegistrarForm __init__""" @@ -909,10 +915,11 @@ class AdditionalDetailsYesNoForm(RegistrarForm): initial=initial_value, widget=forms.RadioSelect, error_messages={ - "required": "This question is required.", #TODO-NL: (design check) - is this required? + "required": "This question is required.", # TODO-NL: (design check) - is this required? }, ) + class RequirementsForm(RegistrarForm): is_policy_acknowledged = forms.BooleanField( label="I read and agree to the requirements for operating a .gov domain.", diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 56dec0e44..13d2ae725 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1037,14 +1037,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? - + return self.anything_else != "" and self.anything_else is not None + def has_cisa_representative(self) -> bool: """Does this domain request have cisa representative?""" - return self.cisa_representative_email != "" and self.cisa_representative_email != None + return self.cisa_representative_email != "" and self.cisa_representative_email is not None def is_federal(self) -> Union[bool, None]: """Is this domain request for a federal agency? diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 12043903b..07f3f77f1 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -366,7 +366,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): or self.domain_request.no_other_contacts_rationale is not None ), "anything_else": ( - (self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email) or self.domain_request.is_policy_acknowledged is not None + (self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email) + or self.domain_request.is_policy_acknowledged is not None ), "requirements": self.domain_request.is_policy_acknowledged is not None, "review": self.domain_request.is_policy_acknowledged is not None, @@ -580,7 +581,8 @@ class OtherContacts(DomainRequestWizard): all_forms_valid = False return all_forms_valid -#DONE-NL: rename this to "Additional Details" (note: this is a find-replace job. VS will not refactor properly) + +# DONE-NL: rename this to "Additional Details" (note: this is a find-replace job. VS will not refactor properly) class AdditionalDetails(DomainRequestWizard): # TODO-NL: Delete this old (original code for anything else) @@ -590,7 +592,12 @@ class AdditionalDetails(DomainRequestWizard): 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] + forms = [ + forms.CisaRepresentativeYesNoForm, + forms.CisaRepresentativeForm, + forms.AdditionalDetailsYesNoForm, + forms.AdditionalDetailsForm, + ] # TODO-NL: (refactor) -- move validation into respective areas def is_valid(self, forms: list) -> bool: @@ -605,7 +612,7 @@ class AdditionalDetails(DomainRequestWizard): anything_else_yes_no_form = forms[2] anything_else_form = forms[3] - # ------- Validate cisa representative ------- + # ------- 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(): @@ -621,7 +628,6 @@ class AdditionalDetails(DomainRequestWizard): 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 @@ -638,7 +644,6 @@ class AdditionalDetails(DomainRequestWizard): 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 From 9d872d065cdf93a254b60133133d719bf31f48ba Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 11 Apr 2024 15:46:04 -0600 Subject: [PATCH 05/49] Cleanup. Admin updates --- src/registrar/admin.py | 2 +- ...tion_cisa_representative_email_and_more.py | 24 ++++++++++++++++--- src/registrar/models/domain_information.py | 2 ++ src/registrar/models/domain_request.py | 5 ++++ .../domain_request_anything_else.html | 19 --------------- .../includes/summary_additional_details.html | 6 ++--- 6 files changed, 32 insertions(+), 26 deletions(-) delete mode 100644 src/registrar/templates/domain_request_anything_else.html diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8e483ddb8..e1d344458 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1132,7 +1132,7 @@ class DomainRequestAdmin(ListHeaderAdmin): }, ), (".gov domain", {"fields": ["requested_domain", "alternative_domains"]}), - ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), + ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale", "cisa_representative_email"]}), ("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}), ( "Type of organization", diff --git a/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py index 83b717b00..7f0d0b71e 100644 --- a/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py +++ b/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-11 00:43 +# Generated by Django 4.2.10 on 2024-04-11 21:40 from django.db import migrations, models @@ -13,11 +13,29 @@ class Migration(migrations.Migration): migrations.AddField( model_name="domaininformation", name="cisa_representative_email", - field=models.EmailField(blank=True, db_index=True, max_length=254, null=True), + field=models.EmailField( + blank=True, db_index=True, max_length=254, null=True, verbose_name="CISA region representative" + ), ), migrations.AddField( model_name="domainrequest", name="cisa_representative_email", - field=models.EmailField(blank=True, db_index=True, max_length=254, null=True), + field=models.EmailField( + blank=True, db_index=True, max_length=254, null=True, verbose_name="CISA region representative" + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="anything_else", + field=models.TextField( + blank=True, help_text="Anything else?", null=True, verbose_name="Additional Details" + ), + ), + migrations.AlterField( + model_name="domainrequest", + name="anything_else", + field=models.TextField( + blank=True, help_text="Anything else?", null=True, verbose_name="Additional Details" + ), ), ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 1aa9ea6d2..dc7cae9fd 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -213,12 +213,14 @@ class DomainInformation(TimeStampedModel): null=True, blank=True, help_text="Anything else?", + verbose_name="Additional Details", ) cisa_representative_email = models.EmailField( null=True, blank=True, db_index=True, + verbose_name="CISA region representative", ) is_policy_acknowledged = models.BooleanField( diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 13d2ae725..731034606 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -644,12 +644,14 @@ class DomainRequest(TimeStampedModel): null=True, blank=True, help_text="Anything else?", + verbose_name="Additional Details", ) cisa_representative_email = models.EmailField( null=True, blank=True, db_index=True, + verbose_name="CISA region representative", ) is_policy_acknowledged = models.BooleanField( @@ -1045,6 +1047,9 @@ class DomainRequest(TimeStampedModel): def has_cisa_representative(self) -> bool: """Does this domain request have cisa representative?""" return self.cisa_representative_email != "" and self.cisa_representative_email is not None + + def has_additional_details(self) -> bool: + return self.has_anything_else_text() or self.has_cisa_representative() def is_federal(self) -> Union[bool, None]: """Is this domain request for a federal agency? diff --git a/src/registrar/templates/domain_request_anything_else.html b/src/registrar/templates/domain_request_anything_else.html deleted file mode 100644 index dbeb10cac..000000000 --- a/src/registrar/templates/domain_request_anything_else.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'domain_request_form.html' %} -{% load field_helpers %} - -{% block form_instructions %} -

Is there anything else you’d like us to know about your domain request?

- -

This question is optional.

-{% endblock %} - -{% block form_required_fields_help_text %} -{# commented out so it does not appear on this page #} -{% endblock %} - - -{% block form_fields %} - {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} - {% input_with_errors forms.0.anything_else %} - {% endwith %} -{% endblock %} diff --git a/src/registrar/templates/includes/summary_additional_details.html b/src/registrar/templates/includes/summary_additional_details.html index e10b90384..1009f4ff0 100644 --- a/src/registrar/templates/includes/summary_additional_details.html +++ b/src/registrar/templates/includes/summary_additional_details.html @@ -1,9 +1,9 @@ {% load static url_helpers %} -{# TODO (future ticket?): create a template that allows us to easily display nested forms. +
@@ -19,7 +19,7 @@ and condense down into one subsection)} Additional Details - {% if domainRequest %} + {% if domainRequest is not none %}
CISA regions representative From 3c1c888356faf9b0eb91880ce65b56374ce30145 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 15 Apr 2024 11:33:21 -0600 Subject: [PATCH 06/49] Fix form field w/ refactor --- src/api/views.py | 1 + src/registrar/forms/domain_request_wizard.py | 12 ++++++++++-- ...information_cisa_representative_email_and_more.py | 6 +++--- src/registrar/models/domain_information.py | 1 + src/registrar/models/domain_request.py | 1 + .../templates/domain_request_additional_details.html | 1 - src/registrar/tests/test_admin.py | 1 + 7 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 2199e15ac..c7b4b2e70 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -72,6 +72,7 @@ def check_domain_available(domain): given domain doesn't end with .gov, ".gov" is added when looking for a match. If check fails, throws a RegistryError. """ + return True Domain = apps.get_model("registrar.Domain") if domain.endswith(".gov"): diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 6ddb68a74..78ddd8531 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -851,8 +851,16 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm): 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? + required=True, + max_length=None, + error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, + label="Your representative’s email", + validators=[ + MaxLengthValidator( + 320, + message="Response must be less than 320 characters.", + ) + ], ) diff --git a/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py index 7f0d0b71e..ae645cf31 100644 --- a/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py +++ b/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-11 21:40 +# Generated by Django 4.2.10 on 2024-04-15 17:30 from django.db import migrations, models @@ -14,14 +14,14 @@ class Migration(migrations.Migration): model_name="domaininformation", name="cisa_representative_email", field=models.EmailField( - blank=True, db_index=True, max_length=254, null=True, verbose_name="CISA region representative" + blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" ), ), migrations.AddField( model_name="domainrequest", name="cisa_representative_email", field=models.EmailField( - blank=True, db_index=True, max_length=254, null=True, verbose_name="CISA region representative" + blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" ), ), migrations.AlterField( diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index dc7cae9fd..07e01bddb 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -221,6 +221,7 @@ class DomainInformation(TimeStampedModel): blank=True, db_index=True, verbose_name="CISA region representative", + max_length=320, ) is_policy_acknowledged = models.BooleanField( diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 731034606..563a1e9b0 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -652,6 +652,7 @@ class DomainRequest(TimeStampedModel): blank=True, db_index=True, verbose_name="CISA region representative", + max_length=320, ) is_policy_acknowledged = models.BooleanField( diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html index 430d47472..44a725552 100644 --- a/src/registrar/templates/domain_request_additional_details.html +++ b/src/registrar/templates/domain_request_additional_details.html @@ -26,7 +26,6 @@
-

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 #} diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index bd365718f..1df10d98d 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1678,6 +1678,7 @@ class TestDomainRequestAdmin(MockEppLib): "purpose", "no_other_contacts_rationale", "anything_else", + "cisa_representative_email", "is_policy_acknowledged", "submission_date", "notes", From 7013766d39e634e6cf72d0b958486339da766ed4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 15 Apr 2024 11:47:42 -0600 Subject: [PATCH 07/49] Fix merge conflict --- ...5_domaininformation_cisa_representative_email_and_more.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/registrar/migrations/{0084_domaininformation_cisa_representative_email_and_more.py => 0085_domaininformation_cisa_representative_email_and_more.py} (90%) diff --git a/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py similarity index 90% rename from src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py rename to src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py index ae645cf31..71089c520 100644 --- a/src/registrar/migrations/0084_domaininformation_cisa_representative_email_and_more.py +++ b/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-15 17:30 +# Generated by Django 4.2.10 on 2024-04-15 17:47 from django.db import migrations, models @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("registrar", "0083_alter_contact_email_alter_publiccontact_email"), + ("registrar", "0084_create_groups_v11"), ] operations = [ From 09b4e7487bdb2947566e032ed2eab79ff486689e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:27:35 -0600 Subject: [PATCH 08/49] Remove accidental return True --- src/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/views.py b/src/api/views.py index c7b4b2e70..b36b3ee72 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -72,7 +72,7 @@ def check_domain_available(domain): given domain doesn't end with .gov, ".gov" is added when looking for a match. If check fails, throws a RegistryError. """ - return True + Domain = apps.get_model("registrar.Domain") if domain.endswith(".gov"): From 6e33dcfbe1a75d1a9d65baeb66ae518568c3df83 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:28:26 -0600 Subject: [PATCH 09/49] Linting --- src/registrar/admin.py | 12 +++++++++++- src/registrar/models/domain_request.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 68bc0a4a4..5f6c71f45 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1172,7 +1172,17 @@ class DomainRequestAdmin(ListHeaderAdmin): }, ), (".gov domain", {"fields": ["requested_domain", "alternative_domains"]}), - ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale", "cisa_representative_email"]}), + ( + "Contacts", + { + "fields": [ + "authorizing_official", + "other_contacts", + "no_other_contacts_rationale", + "cisa_representative_email", + ] + }, + ), ("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}), ( "Type of organization", diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 563a1e9b0..3fce90aa4 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1048,7 +1048,7 @@ class DomainRequest(TimeStampedModel): def has_cisa_representative(self) -> bool: """Does this domain request have cisa representative?""" return self.cisa_representative_email != "" and self.cisa_representative_email is not None - + def has_additional_details(self) -> bool: return self.has_anything_else_text() or self.has_cisa_representative() From 78c0f470f2d781861e723068c300050a9ab5f47a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:37:50 -0600 Subject: [PATCH 10/49] Make anything else required --- src/registrar/forms/domain_request_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 78ddd8531..0f3b0c3f3 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -891,7 +891,7 @@ class CisaRepresentativeYesNoForm(RegistrarForm): class AdditionalDetailsForm(BaseDeletableRegistrarForm): anything_else = forms.CharField( - required=False, + required=True, label="Anything else?", widget=forms.Textarea(), validators=[ From 8a9727e4c0cbadb9cac5e81242c07cbfdbd00442 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 15 Apr 2024 18:08:06 -0600 Subject: [PATCH 11/49] test fix (also fixed error in summary page) --- .../templates/domain_request_review.html | 2 +- .../includes/summary_additional_details.html | 4 +-- src/registrar/tests/test_views_request.py | 25 ++++++++++++------- src/registrar/views/domain_request.py | 4 +-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index 09a930710..569fd717c 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -156,7 +156,7 @@ {% if step == Step.ADDITIONAL_DETAILS %} - {% include "includes/summary_additional_details.html" with domainRequest=DomainRequest %} + {% include "includes/summary_additional_details.html" with domainRequest=domain_request %} {% endif %} diff --git a/src/registrar/templates/includes/summary_additional_details.html b/src/registrar/templates/includes/summary_additional_details.html index 1009f4ff0..344f747b2 100644 --- a/src/registrar/templates/includes/summary_additional_details.html +++ b/src/registrar/templates/includes/summary_additional_details.html @@ -26,7 +26,7 @@ and condense down into one subsection) -->
{% if domainRequest.has_cisa_representative %} - domainRequest.cisa_representative_email + {{domainRequest.cisa_representative_email}} {% else %} (none) {% endif %} @@ -36,7 +36,7 @@ and condense down into one subsection) -->
{% if domainRequest.has_anything_else_text %} - domainRequest.anything_else + {{domainRequest.anything_else}} {% else %} No {% endif %} diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index a4cb210bc..80c7fb40a 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -356,33 +356,39 @@ class DomainRequestTests(TestWithUser, WebTest): # the post request should return a redirect to the next form in # the domain request page self.assertEqual(other_contacts_result.status_code, 302) - self.assertEqual(other_contacts_result["Location"], "/request/anything_else/") + self.assertEqual(other_contacts_result["Location"], "/request/additional_details/") num_pages_tested += 1 - # ---- ANYTHING ELSE PAGE ---- + # ---- ADDITIONAL DETAILS PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - anything_else_page = other_contacts_result.follow() - anything_else_form = anything_else_page.forms[0] + additional_details_page = other_contacts_result.follow() + additional_details_form = additional_details_page.forms[0] - anything_else_form["anything_else-anything_else"] = "Nothing else." + # load inputs with test data + + additional_details_form["additional_details-has_cisa_representative"] = "True" + additional_details_form["additional_details-has_anything_else_text"] = "True" + additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com" + additional_details_form["additional_details-anything_else"] = "Nothing else." # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - anything_else_result = anything_else_form.submit() + additional_details_result = additional_details_form.submit() # validate that data from this step are being saved domain_request = DomainRequest.objects.get() # there's only one + self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com") self.assertEqual(domain_request.anything_else, "Nothing else.") # the post request should return a redirect to the next form in # the domain request page - self.assertEqual(anything_else_result.status_code, 302) - self.assertEqual(anything_else_result["Location"], "/request/requirements/") + self.assertEqual(additional_details_result.status_code, 302) + self.assertEqual(additional_details_result["Location"], "/request/requirements/") num_pages_tested += 1 # ---- REQUIREMENTS PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - requirements_page = anything_else_result.follow() + requirements_page = additional_details_result.follow() requirements_form = requirements_page.forms[0] requirements_form["requirements-is_policy_acknowledged"] = True @@ -434,6 +440,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertContains(review_page, "Another Tester") self.assertContains(review_page, "testy2@town.com") self.assertContains(review_page, "(201) 555-5557") + self.assertContains(review_page, "FakeEmail@gmail.com") self.assertContains(review_page, "Nothing else.") # We can't test the modal itself as it relies on JS for init and triggering, diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 07f3f77f1..dc4e3b24c 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -45,7 +45,7 @@ class Step(StrEnum): PURPOSE = "purpose" YOUR_CONTACT = "your_contact" OTHER_CONTACTS = "other_contacts" - ADDITIONAL_DETAILS = "anything_else" + ADDITIONAL_DETAILS = "additional_details" REQUIREMENTS = "requirements" REVIEW = "review" @@ -365,7 +365,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): self.domain_request.other_contacts.exists() or self.domain_request.no_other_contacts_rationale is not None ), - "anything_else": ( + "additional_details": ( (self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email) or self.domain_request.is_policy_acknowledged is not None ), From 26832a611f9a1d3a976f0e3d73ac57411ab4ab2c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 15 Apr 2024 18:14:31 -0600 Subject: [PATCH 12/49] linted --- src/registrar/tests/test_views_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 80c7fb40a..a84d6c374 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -366,7 +366,7 @@ class DomainRequestTests(TestWithUser, WebTest): additional_details_form = additional_details_page.forms[0] # load inputs with test data - + additional_details_form["additional_details-has_cisa_representative"] = "True" additional_details_form["additional_details-has_anything_else_text"] = "True" additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com" From 6ee8e05923c360ec634e0412123589928a5fa938 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 16 Apr 2024 08:34:13 -0600 Subject: [PATCH 13/49] Fix javascript --- src/registrar/assets/js/get-gov.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index a8e6008ef..69f916b7f 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -786,7 +786,7 @@ function hideDeletedForms() { * */ (function anythingElseFormListener() { - HookupYesNoListener("anything_else-has_anything_else_text",'anything-else', null) + HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null) })(); @@ -795,5 +795,5 @@ function hideDeletedForms() { * */ (function cisaRepresentativesFormListener() { - HookupYesNoListener("anything_else-has_cisa_representative",'cisa-representative', null) + HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null) })(); \ No newline at end of file From 46c0d846b6cecec2c6c23d2d85ed21f77376ff44 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:17:39 -0600 Subject: [PATCH 14/49] Fix bug with anything else form --- src/registrar/views/domain_request.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index dc4e3b24c..6bdec0ab9 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -585,13 +585,8 @@ class OtherContacts(DomainRequestWizard): # DONE-NL: rename this to "Additional Details" (note: this is a find-replace job. VS will not refactor properly) class AdditionalDetails(DomainRequestWizard): - # TODO-NL: Delete this old (original code for anything else) - # template_name = "domain_request_anything_else.html" - # 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, @@ -599,7 +594,6 @@ class AdditionalDetails(DomainRequestWizard): forms.AdditionalDetailsForm, ] - # TODO-NL: (refactor) -- move validation into respective areas def is_valid(self, forms: list) -> bool: # Validate Cisa Representative @@ -637,7 +631,7 @@ class AdditionalDetails(DomainRequestWizard): # 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() + anything_else_portion_is_valid = anything_else_form.is_valid() else: # if yes no form is invalid, no choice has been made # mark the anything_else_form for deletion From 1fa44a61f131f8a39fa43675207707553d15266e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:54:22 -0600 Subject: [PATCH 15/49] Code cleanup --- src/api/views.py | 1 - src/registrar/forms/domain_request_wizard.py | 53 ++++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index b36b3ee72..2199e15ac 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -72,7 +72,6 @@ def check_domain_available(domain): given domain doesn't end with .gov, ".gov" is added when looking for a match. If check fails, throws a RegistryError. """ - Domain = apps.get_model("registrar.Domain") if domain.endswith(".gov"): diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 0f3b0c3f3..3be5c4cc8 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -864,13 +864,19 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm): ) -class CisaRepresentativeYesNoForm(RegistrarForm): +class BaseYesNoForm(RegistrarForm): + """Used for forms with a yes/no form with a hidden input on toggle""" + + form_is_checked = None + typed_choice_field_name = None + 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(): + if self.form_is_checked: initial_value = True else: initial_value = False @@ -878,8 +884,8 @@ class CisaRepresentativeYesNoForm(RegistrarForm): # 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 + self.fields[self.typed_choice_field_name] = forms.TypedChoiceField( + coerce=lambda x: x.lower() == "true" if x is not None else None, choices=((True, "Yes"), (False, "No")), initial=initial_value, widget=forms.RadioSelect, @@ -889,6 +895,16 @@ class CisaRepresentativeYesNoForm(RegistrarForm): ) +class CisaRepresentativeYesNoForm(BaseYesNoForm): + """Yes/no toggle for the CISA regions question on additional details""" + + # Note that these can be set in __init__ if you need more fine-grained control + form_is_checked = property( + lambda self: self.domain_request.has_cisa_representative() if self.domain_request else False + ) + typed_choice_field_name = "has_cisa_representative" + + class AdditionalDetailsForm(BaseDeletableRegistrarForm): anything_else = forms.CharField( required=True, @@ -903,29 +919,14 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm): ) -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 +class AdditionalDetailsYesNoForm(BaseYesNoForm): + """Yes/no toggle for the anything else question on additional details""" - 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? - }, - ) + # Note that these can be set in __init__ if you need more fine-grained control + form_is_checked = property( + lambda self: self.domain_request.has_anything_else_text() if self.domain_request else False + ) + typed_choice_field_name = "has_anything_else_text" class RequirementsForm(RegistrarForm): From d35fabcbce24dc3046c1fa83e9a3c084512ab1c7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:37:05 -0600 Subject: [PATCH 16/49] Generalize BaseYesNoForm + centralize logic into helper This commit is important for two reasons: 1. It keeps things consistent in our code base 2. It moves the ever growing list of wizard base classes into a consistent location --- src/registrar/forms/domain_request_wizard.py | 238 ++---------------- .../forms/utility/wizard_form_helper.py | 226 +++++++++++++++++ 2 files changed, 249 insertions(+), 215 deletions(-) create mode 100644 src/registrar/forms/utility/wizard_form_helper.py diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 3be5c4cc8..dc2fc3a84 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -1,15 +1,13 @@ from __future__ import annotations # allows forward references in annotations -from itertools import zip_longest import logging -from typing import Callable from api.views import DOMAIN_API_MESSAGES from phonenumber_field.formfields import PhoneNumberField # type: ignore from django import forms from django.core.validators import RegexValidator, MaxLengthValidator from django.utils.safestring import mark_safe -from django.db.models.fields.related import ForeignObjectRel +from registrar.forms.utility.wizard_form_helper import RegistrarForm, RegistrarFormSet, BaseYesNoForm from registrar.models import Contact, DomainRequest, DraftDomain, Domain from registrar.templatetags.url_helpers import public_site_url from registrar.utility.enums import ValidationReturnType @@ -17,157 +15,6 @@ from registrar.utility.enums import ValidationReturnType logger = logging.getLogger(__name__) -class RegistrarForm(forms.Form): - """ - A common set of methods and configuration. - - The registrar's domain request is several pages of "steps". - Each step is an HTML form containing one or more Django "forms". - - Subclass this class to create new forms. - """ - - def __init__(self, *args, **kwargs): - kwargs.setdefault("label_suffix", "") - # save a reference to a domain request object - self.domain_request = kwargs.pop("domain_request", None) - super(RegistrarForm, self).__init__(*args, **kwargs) - - def to_database(self, obj: DomainRequest | Contact): - """ - Adds this form's cleaned data to `obj` and saves `obj`. - - Does nothing if form is not valid. - """ - if not self.is_valid(): - return - for name, value in self.cleaned_data.items(): - setattr(obj, name, value) - obj.save() - - @classmethod - def from_database(cls, obj: DomainRequest | Contact | None): - """Returns a dict of form field values gotten from `obj`.""" - if obj is None: - return {} - return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore - - -class RegistrarFormSet(forms.BaseFormSet): - """ - As with RegistrarForm, a common set of methods and configuration. - - Subclass this class to create new formsets. - """ - - def __init__(self, *args, **kwargs): - # save a reference to an domain_request object - self.domain_request = kwargs.pop("domain_request", None) - super(RegistrarFormSet, self).__init__(*args, **kwargs) - # quick workaround to ensure that the HTML `required` - # attribute shows up on required fields for any forms - # in the formset which have data already (stated another - # way: you can leave a form in the formset blank, but - # if you opt to fill it out, you must fill it out _right_) - for index in range(self.initial_form_count()): - self.forms[index].use_required_attribute = True - - def should_delete(self, cleaned): - """Should this entry be deleted from the database?""" - raise NotImplementedError - - def pre_update(self, db_obj, cleaned): - """Code to run before an item in the formset is saved.""" - for key, value in cleaned.items(): - setattr(db_obj, key, value) - - def pre_create(self, db_obj, cleaned): - """Code to run before an item in the formset is created in the database.""" - return cleaned - - def to_database(self, obj: DomainRequest): - """ - Adds this form's cleaned data to `obj` and saves `obj`. - - Does nothing if form is not valid. - - Hint: Subclass should call `self._to_database(...)`. - """ - raise NotImplementedError - - def _to_database( - self, - obj: DomainRequest, - join: str, - should_delete: Callable, - pre_update: Callable, - pre_create: Callable, - ): - """ - Performs the actual work of saving. - - Has hooks such as `should_delete` and `pre_update` by which the - subclass can control behavior. Add more hooks whenever needed. - """ - if not self.is_valid(): - return - obj.save() - - query = getattr(obj, join).order_by("created_at").all() # order matters - - # get the related name for the join defined for the db_obj for this form. - # the related name will be the reference on a related object back to db_obj - related_name = "" - field = obj._meta.get_field(join) - if isinstance(field, ForeignObjectRel) and callable(field.related_query_name): - related_name = field.related_query_name() - elif hasattr(field, "related_query_name") and callable(field.related_query_name): - related_name = field.related_query_name() - - # the use of `zip` pairs the forms in the formset with the - # related objects gotten from the database -- there should always be - # at least as many forms as database entries: extra forms means new - # entries, but fewer forms is _not_ the correct way to delete items - # (likely a client-side error or an attempt at data tampering) - for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None): - cleaned = post_data.cleaned_data if post_data is not None else {} - - # matching database object exists, update it - if db_obj is not None and cleaned: - if should_delete(cleaned): - if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name): - # Remove the specific relationship without deleting the object - getattr(db_obj, related_name).remove(self.domain_request) - else: - # If there are no other relationships, delete the object - db_obj.delete() - else: - if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name): - # create a new db_obj and disconnect existing one - getattr(db_obj, related_name).remove(self.domain_request) - kwargs = pre_create(db_obj, cleaned) - getattr(obj, join).create(**kwargs) - else: - pre_update(db_obj, cleaned) - db_obj.save() - - # no matching database object, create it - # make sure not to create a database object if cleaned has 'delete' attribute - elif db_obj is None and cleaned and not cleaned.get("DELETE", False): - kwargs = pre_create(db_obj, cleaned) - getattr(obj, join).create(**kwargs) - - @classmethod - def on_fetch(cls, query): - """Code to run when fetching formset's objects from the database.""" - return query.values() - - @classmethod - def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable): - """Returns a dict of form field values gotten from `obj`.""" - return on_fetch(getattr(obj, join).order_by("created_at")) # order matters - - class OrganizationTypeForm(RegistrarForm): generic_org_type = forms.ChoiceField( # use the long names in the domain request form @@ -588,28 +435,24 @@ class YourContactForm(RegistrarForm): ) -class OtherContactsYesNoForm(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 and self.domain_request.has_other_contacts(): - initial_value = True - elif self.domain_request and self.domain_request.has_rationale(): - initial_value = False +class OtherContactsYesNoForm(BaseYesNoForm): + """The yes/no field for the OtherContacts form.""" + + form_choices = ((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)")) + field_name = "has_other_contacts" + + @property + def form_is_checked(self): + """ + Determines the initial checked state of the form based on the domain_request's attributes. + """ + if self.domain_request.has_other_contacts(): + return True + elif self.domain_request.has_rationale(): + return False else: # No pre-selection for new domain requests - initial_value = None - - self.fields["has_other_contacts"] = forms.TypedChoiceField( - coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None - choices=((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)")), - initial=initial_value, - widget=forms.RadioSelect, - error_messages={ - "required": "This question is required.", - }, - ) + return None class OtherContactsForm(RegistrarForm): @@ -864,45 +707,12 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm): ) -class BaseYesNoForm(RegistrarForm): - """Used for forms with a yes/no form with a hidden input on toggle""" - - form_is_checked = None - typed_choice_field_name = None - - 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.form_is_checked: - initial_value = True - else: - initial_value = False - else: - # No pre-selection for new domain requests - initial_value = None - - self.fields[self.typed_choice_field_name] = forms.TypedChoiceField( - coerce=lambda x: x.lower() == "true" if x is not None else None, - choices=((True, "Yes"), (False, "No")), - initial=initial_value, - widget=forms.RadioSelect, - error_messages={ - "required": "This question is required.", - }, - ) - - class CisaRepresentativeYesNoForm(BaseYesNoForm): """Yes/no toggle for the CISA regions question on additional details""" - # Note that these can be set in __init__ if you need more fine-grained control - form_is_checked = property( - lambda self: self.domain_request.has_cisa_representative() if self.domain_request else False - ) - typed_choice_field_name = "has_cisa_representative" + # Note that these can be set as functions/init if you need more fine-grained control + form_is_checked = property(lambda self: self.domain_request.has_cisa_representative()) + field_name = "has_cisa_representative" class AdditionalDetailsForm(BaseDeletableRegistrarForm): @@ -922,11 +732,9 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm): class AdditionalDetailsYesNoForm(BaseYesNoForm): """Yes/no toggle for the anything else question on additional details""" - # Note that these can be set in __init__ if you need more fine-grained control - form_is_checked = property( - lambda self: self.domain_request.has_anything_else_text() if self.domain_request else False - ) - typed_choice_field_name = "has_anything_else_text" + # Note that these can be set as functions/init if you need more fine-grained control + form_is_checked = property(lambda self: self.domain_request.has_anything_else_text()) + field_name = "has_anything_else_text" class RequirementsForm(RegistrarForm): diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py new file mode 100644 index 000000000..2916e0393 --- /dev/null +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -0,0 +1,226 @@ +"""Containers helpers and base classes for the domain_request_wizard.py file""" + +from itertools import zip_longest +from typing import Callable +from django.db.models.fields.related import ForeignObjectRel +from django import forms + +from registrar.models import DomainRequest, Contact + + +class RegistrarForm(forms.Form): + """ + A common set of methods and configuration. + + The registrar's domain request is several pages of "steps". + Each step is an HTML form containing one or more Django "forms". + + Subclass this class to create new forms. + """ + + def __init__(self, *args, **kwargs): + kwargs.setdefault("label_suffix", "") + # save a reference to a domain request object + self.domain_request = kwargs.pop("domain_request", None) + super(RegistrarForm, self).__init__(*args, **kwargs) + + def to_database(self, obj: DomainRequest | Contact): + """ + Adds this form's cleaned data to `obj` and saves `obj`. + + Does nothing if form is not valid. + """ + if not self.is_valid(): + return + for name, value in self.cleaned_data.items(): + setattr(obj, name, value) + obj.save() + + @classmethod + def from_database(cls, obj: DomainRequest | Contact | None): + """Returns a dict of form field values gotten from `obj`.""" + if obj is None: + return {} + return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore + + +class RegistrarFormSet(forms.BaseFormSet): + """ + As with RegistrarForm, a common set of methods and configuration. + + Subclass this class to create new formsets. + """ + + def __init__(self, *args, **kwargs): + # save a reference to an domain_request object + self.domain_request = kwargs.pop("domain_request", None) + super(RegistrarFormSet, self).__init__(*args, **kwargs) + # quick workaround to ensure that the HTML `required` + # attribute shows up on required fields for any forms + # in the formset which have data already (stated another + # way: you can leave a form in the formset blank, but + # if you opt to fill it out, you must fill it out _right_) + for index in range(self.initial_form_count()): + self.forms[index].use_required_attribute = True + + def should_delete(self, cleaned): + """Should this entry be deleted from the database?""" + raise NotImplementedError + + def pre_update(self, db_obj, cleaned): + """Code to run before an item in the formset is saved.""" + for key, value in cleaned.items(): + setattr(db_obj, key, value) + + def pre_create(self, db_obj, cleaned): + """Code to run before an item in the formset is created in the database.""" + return cleaned + + def to_database(self, obj: DomainRequest): + """ + Adds this form's cleaned data to `obj` and saves `obj`. + + Does nothing if form is not valid. + + Hint: Subclass should call `self._to_database(...)`. + """ + raise NotImplementedError + + def _to_database( + self, + obj: DomainRequest, + join: str, + should_delete: Callable, + pre_update: Callable, + pre_create: Callable, + ): + """ + Performs the actual work of saving. + + Has hooks such as `should_delete` and `pre_update` by which the + subclass can control behavior. Add more hooks whenever needed. + """ + if not self.is_valid(): + return + obj.save() + + query = getattr(obj, join).order_by("created_at").all() # order matters + + # get the related name for the join defined for the db_obj for this form. + # the related name will be the reference on a related object back to db_obj + related_name = "" + field = obj._meta.get_field(join) + if isinstance(field, ForeignObjectRel) and callable(field.related_query_name): + related_name = field.related_query_name() + elif hasattr(field, "related_query_name") and callable(field.related_query_name): + related_name = field.related_query_name() + + # the use of `zip` pairs the forms in the formset with the + # related objects gotten from the database -- there should always be + # at least as many forms as database entries: extra forms means new + # entries, but fewer forms is _not_ the correct way to delete items + # (likely a client-side error or an attempt at data tampering) + for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None): + cleaned = post_data.cleaned_data if post_data is not None else {} + + # matching database object exists, update it + if db_obj is not None and cleaned: + if should_delete(cleaned): + if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name): + # Remove the specific relationship without deleting the object + getattr(db_obj, related_name).remove(self.domain_request) + else: + # If there are no other relationships, delete the object + db_obj.delete() + else: + if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name): + # create a new db_obj and disconnect existing one + getattr(db_obj, related_name).remove(self.domain_request) + kwargs = pre_create(db_obj, cleaned) + getattr(obj, join).create(**kwargs) + else: + pre_update(db_obj, cleaned) + db_obj.save() + + # no matching database object, create it + # make sure not to create a database object if cleaned has 'delete' attribute + elif db_obj is None and cleaned and not cleaned.get("DELETE", False): + kwargs = pre_create(db_obj, cleaned) + getattr(obj, join).create(**kwargs) + + @classmethod + def on_fetch(cls, query): + """Code to run when fetching formset's objects from the database.""" + return query.values() + + @classmethod + def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable): + """Returns a dict of form field values gotten from `obj`.""" + return on_fetch(getattr(obj, join).order_by("created_at")) # order matters + + +class BaseYesNoForm(RegistrarForm): + """ + Base class used for forms with a yes/no form with a hidden input on toggle. + Use this class when you need something similar to the AdditionalDetailsYesNoForm. + + Attributes: + form_is_checked (bool): Determines the default state (checked or not) of the Yes/No toggle. + field_name (str): Specifies the form field name that the Yes/No toggle controls. + required_error_message (str): Custom error message displayed when the field is required but not provided. + form_choices (tuple): Defines the choice options for the form field, defaulting to Yes/No choices. + + Usage: + Subclass this form to implement specific Yes/No fields in various parts of the application, customizing + `form_is_checked` and `field_name` as necessary for the context. + """ + + form_is_checked: bool + + # What field does the yes/no button hook to? + # For instance, this could be "has_other_contacts" + field_name: str + + required_error_message = "This question is required." + + # Default form choice mapping. Default is suitable for most cases. + form_choices = ((True, "Yes"), (False, "No")) + + def __init__(self, *args, **kwargs): + """Extend the initialization of the form from RegistrarForm __init__""" + super().__init__(*args, **kwargs) + + self.fields[self.field_name] = self.get_typed_choice_field() + + def get_typed_choice_field(self): + """ + Creates a TypedChoiceField for the form with specified initial value and choices. + Returns: + TypedChoiceField: A Django form field specifically configured for selecting between + predefined choices with type coercion and custom error messages. + """ + choice_field = forms.TypedChoiceField( + coerce=lambda x: x.lower() == "true" if x is not None else None, + choices=self.form_choices, + initial=self.get_initial_value(), + widget=forms.RadioSelect, + error_messages={ + "required": self.required_error_message, + }, + ) + + return choice_field + + def get_initial_value(self): + """ + Determines the initial value for TypedChoiceField. + More directly, this controls the "initial" field on forms.TypedChoiceField. + + Returns: + bool | None: The initial value for the form field. If the domain request is set, + this will always return the value of self.form_is_checked. + Otherwise, None will be returned as a new domain request can't start out checked. + """ + # No pre-selection for new domain requests + initial_value = self.form_is_checked if self.domain_request else None + return initial_value From 43160df81b6e6b259f76dd1d19658f1292c69bcf Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:43:02 -0600 Subject: [PATCH 17/49] Move BaseDeletableRegistrarForm --- src/registrar/forms/domain_request_wizard.py | 61 ++----------------- .../forms/utility/wizard_form_helper.py | 54 ++++++++++++++++ 2 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index dc2fc3a84..4e2c8425d 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -7,7 +7,12 @@ from django import forms from django.core.validators import RegexValidator, MaxLengthValidator from django.utils.safestring import mark_safe -from registrar.forms.utility.wizard_form_helper import RegistrarForm, RegistrarFormSet, BaseYesNoForm +from registrar.forms.utility.wizard_form_helper import ( + RegistrarForm, + RegistrarFormSet, + BaseYesNoForm, + BaseDeletableRegistrarForm, +) from registrar.models import Contact, DomainRequest, DraftDomain, Domain from registrar.templatetags.url_helpers import public_site_url from registrar.utility.enums import ValidationReturnType @@ -622,60 +627,6 @@ OtherContactsFormSet = forms.formset_factory( ) -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 this form for deletion. - This changes behavior of validity checks and to_database - methods.""" - self.form_data_marked_for_deletion = True - - def clean(self): - """ - This method overrides the default behavior for forms. - This cleans the form after field validation has already taken place. - In this override, remove errors associated with the form if form data - is marked for deletion. - """ - - if self.form_data_marked_for_deletion: - # clear any errors raised by the form fields - # (before this clean() method is run, each field - # performs its own clean, which could result in - # errors that we wish to ignore at this point) - # - # NOTE: we cannot just clear() the errors list. - # That causes problems. - for field in self.fields: - if field in self.errors: - del self.errors[field] - - return self.cleaned_data - - def to_database(self, obj): - """ - This method overrides the behavior of RegistrarForm. - If form data is marked for deletion, set relevant fields - to None before saving. - Do nothing if form is not valid. - """ - if not self.is_valid(): - return - if self.form_data_marked_for_deletion: - for field_name, _ in self.fields.items(): - setattr(obj, field_name, None) - else: - for name, value in self.cleaned_data.items(): - setattr(obj, name, value) - obj.save() - - class NoOtherContactsForm(BaseDeletableRegistrarForm): no_other_contacts_rationale = forms.CharField( required=True, diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 2916e0393..2ae50f908 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -159,6 +159,60 @@ class RegistrarFormSet(forms.BaseFormSet): return on_fetch(getattr(obj, join).order_by("created_at")) # order matters +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 this form for deletion. + This changes behavior of validity checks and to_database + methods.""" + self.form_data_marked_for_deletion = True + + def clean(self): + """ + This method overrides the default behavior for forms. + This cleans the form after field validation has already taken place. + In this override, remove errors associated with the form if form data + is marked for deletion. + """ + + if self.form_data_marked_for_deletion: + # clear any errors raised by the form fields + # (before this clean() method is run, each field + # performs its own clean, which could result in + # errors that we wish to ignore at this point) + # + # NOTE: we cannot just clear() the errors list. + # That causes problems. + for field in self.fields: + if field in self.errors: + del self.errors[field] + + return self.cleaned_data + + def to_database(self, obj): + """ + This method overrides the behavior of RegistrarForm. + If form data is marked for deletion, set relevant fields + to None before saving. + Do nothing if form is not valid. + """ + if not self.is_valid(): + return + if self.form_data_marked_for_deletion: + for field_name, _ in self.fields.items(): + setattr(obj, field_name, None) + else: + for name, value in self.cleaned_data.items(): + setattr(obj, name, value) + obj.save() + + class BaseYesNoForm(RegistrarForm): """ Base class used for forms with a yes/no form with a hidden input on toggle. From 644ca4638ad75e7f6ef7e38f694d2a6ffda60c3b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:56:58 -0600 Subject: [PATCH 18/49] Overzealous linter --- src/registrar/forms/domain_request_wizard.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 4e2c8425d..789d4498a 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -662,7 +662,7 @@ class CisaRepresentativeYesNoForm(BaseYesNoForm): """Yes/no toggle for the CISA regions question on additional details""" # Note that these can be set as functions/init if you need more fine-grained control - form_is_checked = property(lambda self: self.domain_request.has_cisa_representative()) + form_is_checked = property(lambda self: self.domain_request.has_cisa_representative()) # type: ignore field_name = "has_cisa_representative" @@ -683,8 +683,8 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm): class AdditionalDetailsYesNoForm(BaseYesNoForm): """Yes/no toggle for the anything else question on additional details""" - # Note that these can be set as functions/init if you need more fine-grained control - form_is_checked = property(lambda self: self.domain_request.has_anything_else_text()) + # Note that these can be set as functions/init if you need more fine-grained control. + form_is_checked = property(lambda self: self.domain_request.has_anything_else_text()) # type: ignore field_name = "has_anything_else_text" From 2122ff8239b19260a3229b15542df440b5edebe0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:00:56 -0600 Subject: [PATCH 19/49] Rename additional details --- src/registrar/models/domain_information.py | 2 +- src/registrar/models/domain_request.py | 2 +- .../templates/includes/summary_additional_details.html | 2 +- src/registrar/views/domain_request.py | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 07e01bddb..ec2a4ca22 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -213,7 +213,7 @@ class DomainInformation(TimeStampedModel): null=True, blank=True, help_text="Anything else?", - verbose_name="Additional Details", + verbose_name="Additional details", ) cisa_representative_email = models.EmailField( diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 3fce90aa4..02324ce61 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -644,7 +644,7 @@ class DomainRequest(TimeStampedModel): null=True, blank=True, help_text="Anything else?", - verbose_name="Additional Details", + verbose_name="Additional details", ) cisa_representative_email = models.EmailField( diff --git a/src/registrar/templates/includes/summary_additional_details.html b/src/registrar/templates/includes/summary_additional_details.html index 344f747b2..233734e7d 100644 --- a/src/registrar/templates/includes/summary_additional_details.html +++ b/src/registrar/templates/includes/summary_additional_details.html @@ -16,7 +16,7 @@ and condense down into one subsection) --> margin-top-0 margin-bottom-05 padding-right-1" > - Additional Details + Additional details {% if domainRequest is not none %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 6bdec0ab9..f93976138 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -91,7 +91,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): Step.PURPOSE: _("Purpose of your domain"), Step.YOUR_CONTACT: _("Your contact information"), Step.OTHER_CONTACTS: _("Other employees from your organization"), - Step.ADDITIONAL_DETAILS: _("Additional Details"), + Step.ADDITIONAL_DETAILS: _("Additional details"), Step.REQUIREMENTS: _("Requirements for operating a .gov domain"), Step.REVIEW: _("Review and submit your domain request"), } @@ -582,7 +582,6 @@ class OtherContacts(DomainRequestWizard): 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): template_name = "domain_request_additional_details.html" From 29406f05077bb68f27bff792c68c7b579cbc8ceb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:04:01 -0600 Subject: [PATCH 20/49] Delete 0085_domaininformation_cisa_representative_email_and_more.py --- ...tion_cisa_representative_email_and_more.py | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py diff --git a/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py deleted file mode 100644 index 71089c520..000000000 --- a/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.2.10 on 2024-04-15 17:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("registrar", "0084_create_groups_v11"), - ] - - operations = [ - migrations.AddField( - model_name="domaininformation", - name="cisa_representative_email", - field=models.EmailField( - blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" - ), - ), - migrations.AddField( - model_name="domainrequest", - name="cisa_representative_email", - field=models.EmailField( - blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="anything_else", - field=models.TextField( - blank=True, help_text="Anything else?", null=True, verbose_name="Additional Details" - ), - ), - migrations.AlterField( - model_name="domainrequest", - name="anything_else", - field=models.TextField( - blank=True, help_text="Anything else?", null=True, verbose_name="Additional Details" - ), - ), - ] From 7e7919691e1767c7ad81aeb8541e26cbaf5acd83 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:17:54 -0600 Subject: [PATCH 21/49] Add migration --- ...tion_cisa_representative_email_and_more.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py diff --git a/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py new file mode 100644 index 000000000..be43a1969 --- /dev/null +++ b/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.10 on 2024-04-18 17:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0084_create_groups_v11"), + ] + + operations = [ + migrations.AddField( + model_name="domaininformation", + name="cisa_representative_email", + field=models.EmailField( + blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" + ), + ), + migrations.AddField( + model_name="domainrequest", + name="cisa_representative_email", + field=models.EmailField( + blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="anything_else", + field=models.TextField( + blank=True, help_text="Anything else?", null=True, verbose_name="Additional details" + ), + ), + migrations.AlterField( + model_name="domainrequest", + name="anything_else", + field=models.TextField( + blank=True, help_text="Anything else?", null=True, verbose_name="Additional details" + ), + ), + ] From 03be457e372c528d729f58dea19ebfe7b58a8e50 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:14:10 -0600 Subject: [PATCH 22/49] Add correct error messages, allow default state to be none --- src/registrar/forms/domain_request_wizard.py | 16 +++++-- .../forms/utility/wizard_form_helper.py | 2 + ...request_has_anything_else_text_and_more.py | 27 +++++++++++ src/registrar/models/domain_request.py | 45 +++++++++++++++---- 4 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 src/registrar/migrations/0086_domainrequest_has_anything_else_text_and_more.py diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 789d4498a..95d1f9a4a 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -647,7 +647,6 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm): cisa_representative_email = forms.EmailField( required=True, max_length=None, - error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, label="Your representative’s email", validators=[ MaxLengthValidator( @@ -655,14 +654,17 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm): 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."), + }, ) class CisaRepresentativeYesNoForm(BaseYesNoForm): """Yes/no toggle for the CISA regions question on additional details""" - # Note that these can be set as functions/init if you need more fine-grained control - form_is_checked = property(lambda self: self.domain_request.has_cisa_representative()) # type: ignore + form_is_checked = property(lambda self: self.domain_request.has_cisa_representative) field_name = "has_cisa_representative" @@ -677,6 +679,12 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm): message="Response must be less than 2000 characters.", ) ], + error_messages={ + "required": ( + "Provide additional details you’d like us to know. " + "If you have nothing to add, select “No.”" + ) + }, ) @@ -684,7 +692,7 @@ class AdditionalDetailsYesNoForm(BaseYesNoForm): """Yes/no toggle for the anything else question on additional details""" # Note that these can be set as functions/init if you need more fine-grained control. - form_is_checked = property(lambda self: self.domain_request.has_anything_else_text()) # type: ignore + form_is_checked = property(lambda self: self.domain_request.has_anything_else_text) # type: ignore field_name = "has_anything_else_text" diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 2ae50f908..f15382b44 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -263,6 +263,8 @@ class BaseYesNoForm(RegistrarForm): }, ) + print(f"wjat are the error messages? {choice_field.error_messages}") + return choice_field def get_initial_value(self): diff --git a/src/registrar/migrations/0086_domainrequest_has_anything_else_text_and_more.py b/src/registrar/migrations/0086_domainrequest_has_anything_else_text_and_more.py new file mode 100644 index 000000000..5b1d24711 --- /dev/null +++ b/src/registrar/migrations/0086_domainrequest_has_anything_else_text_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.10 on 2024-04-18 17:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0085_domaininformation_cisa_representative_email_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="domainrequest", + 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="domainrequest", + name="has_cisa_representative", + field=models.BooleanField( + blank=True, help_text="Determines if the user has a representative email or not", null=True + ), + ), + ] diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 1e8091c44..0b6e9a2b4 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -647,6 +647,15 @@ class DomainRequest(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, @@ -655,6 +664,15 @@ class DomainRequest(TimeStampedModel): max_length=320, ) + # 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, @@ -707,8 +725,25 @@ class DomainRequest(TimeStampedModel): def save(self, *args, **kwargs): """Save override for custom properties""" self.sync_organization_type() + self.sync_yes_no_form_fields() + super().save(*args, **kwargs) + 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 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 + ) + + if self.anything_else is not None: + self.has_anything_else_text = ( + self.anything_else != "" and self.anything_else is not None + ) + def __str__(self): try: if self.requested_domain and self.requested_domain.name: @@ -1047,16 +1082,8 @@ class DomainRequest(TimeStampedModel): """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 is not None - - def has_cisa_representative(self) -> bool: - """Does this domain request have cisa representative?""" - return self.cisa_representative_email != "" and self.cisa_representative_email is not None - def has_additional_details(self) -> bool: - return self.has_anything_else_text() or self.has_cisa_representative() + return self.has_anything_else_text() or self.has_cisa_representative def is_federal(self) -> Union[bool, None]: """Is this domain request for a federal agency? From 2c0987d0485a891f766cdbdd822c00c795590c8e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:14:34 -0600 Subject: [PATCH 23/49] Update wizard_form_helper.py --- src/registrar/forms/utility/wizard_form_helper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index f15382b44..2ae50f908 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -263,8 +263,6 @@ class BaseYesNoForm(RegistrarForm): }, ) - print(f"wjat are the error messages? {choice_field.error_messages}") - return choice_field def get_initial_value(self): From 229e08070350347eb05f14dfcd9c1c374bf885de Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:21:47 -0600 Subject: [PATCH 24/49] Consolidate migrations --- ...tion_cisa_representative_email_and_more.py | 16 ++++++++++- ...request_has_anything_else_text_and_more.py | 27 ------------------- 2 files changed, 15 insertions(+), 28 deletions(-) delete mode 100644 src/registrar/migrations/0086_domainrequest_has_anything_else_text_and_more.py diff --git a/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py index be43a1969..bc146aef0 100644 --- a/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py +++ b/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-18 17:17 +# Generated by Django 4.2.10 on 2024-04-18 18:21 from django.db import migrations, models @@ -24,6 +24,20 @@ class Migration(migrations.Migration): blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" ), ), + migrations.AddField( + model_name="domainrequest", + 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="domainrequest", + name="has_cisa_representative", + field=models.BooleanField( + blank=True, help_text="Determines if the user has a representative email or not", null=True + ), + ), migrations.AlterField( model_name="domaininformation", name="anything_else", diff --git a/src/registrar/migrations/0086_domainrequest_has_anything_else_text_and_more.py b/src/registrar/migrations/0086_domainrequest_has_anything_else_text_and_more.py deleted file mode 100644 index 5b1d24711..000000000 --- a/src/registrar/migrations/0086_domainrequest_has_anything_else_text_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.2.10 on 2024-04-18 17:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("registrar", "0085_domaininformation_cisa_representative_email_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="domainrequest", - 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="domainrequest", - name="has_cisa_representative", - field=models.BooleanField( - blank=True, help_text="Determines if the user has a representative email or not", null=True - ), - ), - ] From 9931fcc4d3a4c7ce924f03b5f86649926a2ad1a1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:36:11 -0600 Subject: [PATCH 25/49] Fix tests / lint --- src/registrar/forms/domain_request_wizard.py | 5 ++--- src/registrar/models/domain_request.py | 6 ++---- src/registrar/tests/test_admin.py | 2 ++ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 95d1f9a4a..752ad7f35 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -680,9 +680,8 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm): ) ], error_messages={ - "required": ( - "Provide additional details you’d like us to know. " - "If you have nothing to add, select “No.”" + "required": ( + "Provide additional details you’d like us to know. " "If you have nothing to add, select “No.”" ) }, ) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 0b6e9a2b4..32c0ec0c6 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -738,11 +738,9 @@ class DomainRequest(TimeStampedModel): self.has_cisa_representative = ( self.cisa_representative_email != "" and self.cisa_representative_email is not None ) - + if self.anything_else is not None: - self.has_anything_else_text = ( - self.anything_else != "" and self.anything_else is not None - ) + self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None def __str__(self): try: diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index abc6b76aa..3293fc47c 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1883,7 +1883,9 @@ class TestDomainRequestAdmin(MockEppLib): "purpose", "no_other_contacts_rationale", "anything_else", + "has_anything_else_text", "cisa_representative_email", + "has_cisa_representative", "is_policy_acknowledged", "submission_date", "notes", From 7b19e29cf6800edb5c55187d8243dcb91bbb2c3d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 19 Apr 2024 12:58:42 -0600 Subject: [PATCH 26/49] Some unit tests --- src/registrar/models/domain_information.py | 1 - src/registrar/models/domain_request.py | 3 +- src/registrar/tests/common.py | 2 + src/registrar/tests/test_views_request.py | 48 +++++++++++++++++++++- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index c61f1e2a2..7d71915ba 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -219,7 +219,6 @@ class DomainInformation(TimeStampedModel): cisa_representative_email = models.EmailField( null=True, blank=True, - db_index=True, verbose_name="CISA region representative", max_length=320, ) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 32c0ec0c6..6da51d485 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -659,7 +659,6 @@ class DomainRequest(TimeStampedModel): cisa_representative_email = models.EmailField( null=True, blank=True, - db_index=True, verbose_name="CISA region representative", max_length=320, ) @@ -739,7 +738,7 @@ class DomainRequest(TimeStampedModel): self.cisa_representative_email != "" and self.cisa_representative_email is not None ) - if self.anything_else is not None: + 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 __str__(self): diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 07dc08f8a..97e620813 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -857,6 +857,8 @@ def completed_domain_request( creator=user, status=status, investigator=investigator, + has_cisa_representative=False, + has_anything_else_text=False, ) if has_about_your_organization: domain_request_kwargs["about_your_organization"] = "e-Government" diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index a84d6c374..3d9dec37c 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -724,14 +724,26 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) - def test_yes_no_form_inits_blank_for_new_domain_request(self): + def test_yes_no_contact_form_inits_blank_for_new_domain_request(self): """On the Other Contacts page, the yes/no form gets initialized with nothing selected for new domain requests""" other_contacts_page = self.app.get(reverse("domain-request:other_contacts")) other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) - def test_yes_no_form_inits_yes_for_domain_request_with_other_contacts(self): + def test_yes_no_additional_form_inits_blank_for_new_domain_request(self): + """On the Additional Details page, the yes/no form gets initialized with nothing selected for + new domain requests""" + additional_details_page = self.app.get(reverse("domain-request:additional_details")) + additional_form = additional_details_page.forms[0] + + # Check the cisa representative yes/no field + self.assertEquals(additional_form["additional_details-has_cisa_representative"].value, None) + + # Check the anything else yes/no field + self.assertEquals(additional_form["additional_details-has_anything_else_text"].value, None) + + def test_yes_no_contact_form_inits_yes_for_domain_request_with_other_contacts(self): """On the Other Contacts page, the yes/no form gets initialized with YES selected if the domain request has other contacts""" # Domain Request has other contacts by default @@ -751,6 +763,38 @@ class DomainRequestTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + def test_yes_no_additional_form_inits_yes_for_cisa_representative_and_anything_else(self): + """On the Additional Details page, the yes/no form gets initialized with YES selected + for both yes/no radios if the domain request has a value for cisa_representative and + anything_else""" + + domain_request = completed_domain_request(user=self.user, has_anything_else=True) + domain_request.cisa_representative_email="test@igorville.gov" + domain_request.anything_else="1234" + domain_request.save() + + # prime the form by visiting /edit + self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_page = self.app.get(reverse("domain-request:additional_details")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_form = additional_details_page.forms[0] + + # Check the cisa representative yes/no field + yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value + self.assertEquals(yes_no_cisa, True) + + # Check the anything else yes/no field + yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value + self.assertEquals(yes_no_anything_else, True) + def test_yes_no_form_inits_no_for_domain_request_with_no_other_contacts_rationale(self): """On the Other Contacts page, the yes/no form gets initialized with NO selected if the domain request has no other contacts""" From eee745b0ca4a8b0faf272faecf60397e414df3d1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 08:08:10 -0600 Subject: [PATCH 27/49] Fix logic - WIP --- ...ion_cisa_representative_email_and_more.py} | 12 +- src/registrar/models/domain_request.py | 10 ++ src/registrar/tests/common.py | 2 - src/registrar/tests/test_views_request.py | 144 +++++++++++++++++- 4 files changed, 154 insertions(+), 14 deletions(-) rename src/registrar/migrations/{0085_domaininformation_cisa_representative_email_and_more.py => 0087_domaininformation_cisa_representative_email_and_more.py} (77%) diff --git a/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py similarity index 77% rename from src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py rename to src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py index bc146aef0..1e53867d8 100644 --- a/src/registrar/migrations/0085_domaininformation_cisa_representative_email_and_more.py +++ b/src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-18 18:21 +# Generated by Django 4.2.10 on 2024-04-22 13:16 from django.db import migrations, models @@ -6,23 +6,19 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("registrar", "0084_create_groups_v11"), + ("registrar", "0086_domaininformation_updated_federal_agency_and_more"), ] operations = [ migrations.AddField( model_name="domaininformation", name="cisa_representative_email", - field=models.EmailField( - blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" - ), + field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA region representative"), ), migrations.AddField( model_name="domainrequest", name="cisa_representative_email", - field=models.EmailField( - blank=True, db_index=True, max_length=320, null=True, verbose_name="CISA region representative" - ), + field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA region representative"), ), migrations.AddField( model_name="domainrequest", diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index fb7221c80..bfeb33c8e 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -745,12 +745,22 @@ class DomainRequest(TimeStampedModel): """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 != "" + # 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 ) + # 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 diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 82fa4c061..6dd88c1c1 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -856,8 +856,6 @@ def completed_domain_request( creator=user, status=status, investigator=investigator, - has_cisa_representative=False, - has_anything_else_text=False, ) if has_about_your_organization: domain_request_kwargs["about_your_organization"] = "e-Government" diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 3d9dec37c..352b8d0de 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -743,7 +743,7 @@ class DomainRequestTests(TestWithUser, WebTest): # Check the anything else yes/no field self.assertEquals(additional_form["additional_details-has_anything_else_text"].value, None) - def test_yes_no_contact_form_inits_yes_for_domain_request_with_other_contacts(self): + def test_yes_no_form_inits_yes_for_domain_request_with_other_contacts(self): """On the Other Contacts page, the yes/no form gets initialized with YES selected if the domain request has other contacts""" # Domain Request has other contacts by default @@ -763,7 +763,7 @@ class DomainRequestTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - def test_yes_no_additional_form_inits_yes_for_cisa_representative_and_anything_else(self): + def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self): """On the Additional Details page, the yes/no form gets initialized with YES selected for both yes/no radios if the domain request has a value for cisa_representative and anything_else""" @@ -789,11 +789,11 @@ class DomainRequestTests(TestWithUser, WebTest): # Check the cisa representative yes/no field yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value - self.assertEquals(yes_no_cisa, True) + self.assertEquals(yes_no_cisa, "True") # Check the anything else yes/no field yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value - self.assertEquals(yes_no_anything_else, True) + self.assertEquals(yes_no_anything_else, "True") def test_yes_no_form_inits_no_for_domain_request_with_no_other_contacts_rationale(self): """On the Other Contacts page, the yes/no form gets initialized with NO selected if the @@ -816,6 +816,142 @@ class DomainRequestTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") + + def test_yes_no_form_for_domain_request_with_no_cisa_representative_and_anything_else(self): + """On the Additional details page, the form preselects "no" when has_cisa_representative + and anything_else is no""" + + domain_request = completed_domain_request(user=self.user, has_anything_else=False) + + # Unlike the other contacts form, the no button is tracked with these boolean fields. + # This means that we should expect this to correlate with the no button. + domain_request.has_anything_else_text = False + domain_request.has_cisa_representative = False + domain_request.save() + + # prime the form by visiting /edit + self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_page = self.app.get(reverse("domain-request:additional_details")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_form = additional_details_page.forms[0] + + # Check the cisa representative yes/no field + yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value + self.assertEquals(yes_no_cisa, "False") + + # Check the anything else yes/no field + yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value + self.assertEquals(yes_no_anything_else, "False") + + def test_submitting_additional_details_deletes_cisa_representative_and_anything_else(self): + """When a user submits the Additional Details form with no selected for all fields, + the domain request's data gets wiped when submitted""" + domain_request = completed_domain_request(name="nocisareps.gov", user=self.user) + domain_request.cisa_representative_email = "fake@faketown.gov" + domain_request.save() + + # Make sure we have the data we need for the test + self.assertEqual(domain_request.anything_else, "There is more") + self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov") + + # prime the form by visiting /edit + self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_page = self.app.get(reverse("domain-request:additional_details")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_form = additional_details_page.forms[0] + + # Check the cisa representative yes/no field + yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value + self.assertEquals(yes_no_cisa, "True") + + # Check the anything else yes/no field + yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value + self.assertEquals(yes_no_anything_else, "True") + + # Set fields to false + additional_details_form["additional_details-has_cisa_representative"] = "False" + additional_details_form["additional_details-has_anything_else_text"] = "False" + + # Submit the form + additional_details_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the anything_else and cisa_representative have been deleted from the DB + domain_request = DomainRequest.objects.get(requested_domain__name="nocisareps.gov") + + # Check that our data has been cleared + self.assertEqual(domain_request.anything_else, None) + self.assertEqual(domain_request.cisa_representative_email, None) + + # Double check the yes/no fields + self.assertEqual(domain_request.has_anything_else_text, False) + self.assertEqual(domain_request.has_cisa_representative, False) + + def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self): + """When a user submits the Additional Details form, + the domain request's data gets submitted""" + domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False) + + # Make sure we have the data we need for the test + self.assertEqual(domain_request.anything_else, None) + self.assertEqual(domain_request.cisa_representative_email, None) + self.assertEqual(domain_request.has_anything_else_text, False) + self.assertEqual(domain_request.has_cisa_representative, False) + + # prime the form by visiting /edit + self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_page = self.app.get(reverse("domain-request:additional_details")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_form = additional_details_page.forms[0] + + # Check the cisa representative yes/no field + yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value + self.assertEquals(yes_no_cisa, "True") + + # Check the anything else yes/no field + yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value + self.assertEquals(yes_no_anything_else, "True") + + # Set fields to false + additional_details_form["additional_details-has_cisa_representative"] = "False" + additional_details_form["additional_details-has_anything_else_text"] = "False" + + # Submit the form + additional_details_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the anything_else and cisa_representative exist in the db + domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov") + + self.assertEqual(domain_request.anything_else, "There is more") + self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov") + def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): """When a user submits the Other Contacts form with other contacts selected, the domain request's From 8ae6e3e30eabb9b7430d6aa3dcb37883dddccb86 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 08:27:32 -0600 Subject: [PATCH 28/49] Add additional unit tests --- src/registrar/tests/test_views_request.py | 123 ++++++++++++++++++---- 1 file changed, 105 insertions(+), 18 deletions(-) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 352b8d0de..16234490a 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -769,8 +769,8 @@ class DomainRequestTests(TestWithUser, WebTest): anything_else""" domain_request = completed_domain_request(user=self.user, has_anything_else=True) - domain_request.cisa_representative_email="test@igorville.gov" - domain_request.anything_else="1234" + domain_request.cisa_representative_email = "test@igorville.gov" + domain_request.anything_else = "1234" domain_request.save() # prime the form by visiting /edit @@ -816,7 +816,7 @@ class DomainRequestTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") - + def test_yes_no_form_for_domain_request_with_no_cisa_representative_and_anything_else(self): """On the Additional details page, the form preselects "no" when has_cisa_representative and anything_else is no""" @@ -912,8 +912,10 @@ class DomainRequestTests(TestWithUser, WebTest): # Make sure we have the data we need for the test self.assertEqual(domain_request.anything_else, None) self.assertEqual(domain_request.cisa_representative_email, None) - self.assertEqual(domain_request.has_anything_else_text, False) - self.assertEqual(domain_request.has_cisa_representative, False) + + # These fields should not be selected at all, since we haven't initialized the form yet + self.assertEqual(domain_request.has_anything_else_text, None) + self.assertEqual(domain_request.has_cisa_representative, None) # prime the form by visiting /edit self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) @@ -929,17 +931,11 @@ class DomainRequestTests(TestWithUser, WebTest): additional_details_form = additional_details_page.forms[0] - # Check the cisa representative yes/no field - yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value - self.assertEquals(yes_no_cisa, "True") - - # Check the anything else yes/no field - yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value - self.assertEquals(yes_no_anything_else, "True") - - # Set fields to false - additional_details_form["additional_details-has_cisa_representative"] = "False" - additional_details_form["additional_details-has_anything_else_text"] = "False" + # Set fields to true, and set data on those fields + additional_details_form["additional_details-has_cisa_representative"] = "True" + additional_details_form["additional_details-has_anything_else_text"] = "True" + additional_details_form["additional_details-cisa_representative_email"] = "test@faketest.gov" + additional_details_form["additional_details-anything_else"] = "redandblue" # Submit the form additional_details_form.submit() @@ -949,9 +945,100 @@ class DomainRequestTests(TestWithUser, WebTest): # Verify that the anything_else and cisa_representative exist in the db domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov") - self.assertEqual(domain_request.anything_else, "There is more") - self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov") + self.assertEqual(domain_request.anything_else, "redandblue") + self.assertEqual(domain_request.cisa_representative_email, "test@faketest.gov") + self.assertEqual(domain_request.has_cisa_representative, True) + self.assertEqual(domain_request.has_anything_else_text, True) + + def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self): + """Applicants with a cisa representative must provide a value""" + domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False) + + # prime the form by visiting /edit + self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_page = self.app.get(reverse("domain-request:additional_details")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_form = additional_details_page.forms[0] + + # Set fields to true, and set data on those fields + additional_details_form["additional_details-has_cisa_representative"] = "True" + additional_details_form["additional_details-has_anything_else_text"] = "False" + + # Submit the form + response = additional_details_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + self.assertContains(response, "Enter the email address of your CISA regional representative.") + + def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self): + """Applicants with a anything else must provide a value""" + domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False) + + # prime the form by visiting /edit + self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_page = self.app.get(reverse("domain-request:additional_details")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_form = additional_details_page.forms[0] + + # Set fields to true, and set data on those fields + additional_details_form["additional_details-has_cisa_representative"] = "False" + additional_details_form["additional_details-has_anything_else_text"] = "True" + + # Submit the form + response = additional_details_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + expected_message = "Provide additional details you’d like us to know. If you have nothing to add, select “No.”" + self.assertContains(response, expected_message) + + def test_additional_details_form_fields_required(self): + """When a user submits the Additional Details form without checking the + has_cisa_representative and has_anything_else_text fields, the form should deny this action""" + domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False) + + self.assertEqual(domain_request.has_anything_else_text, None) + self.assertEqual(domain_request.has_additional_details, None) + + # prime the form by visiting /edit + self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_page = self.app.get(reverse("domain-request:additional_details")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + additional_details_form = additional_details_page.forms[0] + + # Submit the form + response = additional_details_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # We expect to see this twice for both fields + self.assertContains(response, "This question is required.", count=2) def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): """When a user submits the Other Contacts form with other contacts selected, the domain request's From 438095e669348c242672bf193f8ba1f553c58a74 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:18:17 -0600 Subject: [PATCH 29/49] PR suggestions --- src/registrar/admin.py | 1 + src/registrar/assets/js/get-gov.js | 24 +++++---- .../templates/domain_request_review.html | 14 +++++- .../templates/domain_request_status.html | 13 ++++- .../includes/summary_additional_details.html | 50 ------------------- .../templates/includes/summary_item.html | 3 ++ 6 files changed, 43 insertions(+), 62 deletions(-) delete mode 100644 src/registrar/templates/includes/summary_additional_details.html diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 72ba29db4..cc19d4a10 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1285,6 +1285,7 @@ class DomainRequestAdmin(ListHeaderAdmin): "no_other_contacts_rationale", "anything_else", "is_policy_acknowledged", + "cisa_representative_email", ] autocomplete_fields = [ "approved_domain", diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 3c1b1099f..4cefd798b 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -213,17 +213,21 @@ function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToS // Check if the element exists before accessing its value let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; - switch (selectedValue) { - case 'True': - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1); - break; + if (elementIdToShowIfYes && elementIdToShowIfNo){ + switch (selectedValue) { + case 'True': + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1); + break; - case 'False': - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2); - break; + case 'False': + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2); + break; - default: - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0); + default: + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0); + } + }else { + console.log("No elements to show") } } @@ -833,4 +837,4 @@ function hideDeletedForms() { */ (function cisaRepresentativesFormListener() { HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null) -})(); \ No newline at end of file +})(); diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index 569fd717c..ce061b321 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -156,7 +156,19 @@ {% if step == Step.ADDITIONAL_DETAILS %} - {% include "includes/summary_additional_details.html" with domainRequest=domain_request %} + {% namespaced_url 'domain-request' step as domain_request_url %} + {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %} + {% include "includes/summary_item.html" with title=title sub_header_text='CISA regions representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url %} + {% endwith %} + +

Anything else

+
    + {% if domain_request.has_anything_else_text %} + {{domain_request.anything_else}} + {% else %} + No + {% endif %} +
{% endif %} diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index cf9e7ffe3..2be2cc44b 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -116,7 +116,18 @@ {% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %} {% endif %} - {% include "includes/summary_additional_details.html" with domainRequest=DomainRequest %} + {% if DomainRequest.has_cisa_representative or domain_request.has_anything_else_text %} + {% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regions representative' value=domain_request.cisa_representative_email heading_level=heading_level %} + +

Anything else

+
    + {% if domain_request.has_anything_else_text %} + {{domain_request.anything_else}} + {% else %} + No + {% endif %} +
+ {% endif %} {% endwith %} diff --git a/src/registrar/templates/includes/summary_additional_details.html b/src/registrar/templates/includes/summary_additional_details.html deleted file mode 100644 index 233734e7d..000000000 --- a/src/registrar/templates/includes/summary_additional_details.html +++ /dev/null @@ -1,50 +0,0 @@ -{% load static url_helpers %} - - - -
- -
-
-

- Additional details -

- - {% if domainRequest is not none %} -
-
- CISA regions representative -
-
- {% if domainRequest.has_cisa_representative %} - {{domainRequest.cisa_representative_email}} - {% else %} - (none) - {% endif %} -
-
- Anything else -
-
- {% if domainRequest.has_anything_else_text %} - {{domainRequest.anything_else}} - {% else %} - No - {% endif %} -
-
- {% else %} - ERROR Please contact technical support/dev - {% endif %} -
-
-
diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 7c0d801ad..0988068df 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -21,6 +21,9 @@ {% else %} {% endif %} + {% if sub_header_text %} +

{{ sub_header_text }}

+ {% endif %} {% if address %} {% include "includes/organization_address.html" with organization=value %} {% elif contact %} From 4edb6ea988fb40c3215098933b35a099d47b41c6 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:29:30 -0600 Subject: [PATCH 30/49] Fix test --- src/registrar/tests/test_admin.py | 1 + src/registrar/tests/test_views_request.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 61e2a255f..98e5b2818 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1923,6 +1923,7 @@ class TestDomainRequestAdmin(MockEppLib): "no_other_contacts_rationale", "anything_else", "is_policy_acknowledged", + "cisa_representative_email", ] self.assertEqual(readonly_fields, expected_fields) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 16234490a..460900e28 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -1016,7 +1016,7 @@ class DomainRequestTests(TestWithUser, WebTest): domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False) self.assertEqual(domain_request.has_anything_else_text, None) - self.assertEqual(domain_request.has_additional_details, None) + self.assertEqual(domain_request.has_cisa_representative, None) # prime the form by visiting /edit self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) From e581d3b2caf8278bed441402d0b07689871c08e1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:55:05 -0600 Subject: [PATCH 31/49] Fix js, linting --- src/registrar/assets/js/get-gov.js | 22 +++++++++------------- src/registrar/models/domain_request.py | 9 ++++++++- src/registrar/tests/test_views_request.py | 5 +++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 4cefd798b..e7260ee21 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -213,21 +213,17 @@ function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToS // Check if the element exists before accessing its value let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; - if (elementIdToShowIfYes && elementIdToShowIfNo){ - switch (selectedValue) { - case 'True': - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1); - break; + switch (selectedValue) { + case 'True': + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1); + break; - case 'False': - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2); - break; + case 'False': + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2); + break; - default: - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0); - } - }else { - console.log("No elements to show") + default: + toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0); } } diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index bfeb33c8e..02d07c9b2 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1103,7 +1103,14 @@ class DomainRequest(TimeStampedModel): return self.other_contacts.exists() def has_additional_details(self) -> bool: - return self.has_anything_else_text() or self.has_cisa_representative + """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 + + return has_details def is_federal(self) -> Union[bool, None]: """Is this domain request for a federal agency? diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 460900e28..19be5ce74 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -1037,8 +1037,9 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # We expect to see this twice for both fields - self.assertContains(response, "This question is required.", count=2) + # We expect to see this twice for both fields. This results in a count of 4 + # due to screen reader information / html. + self.assertContains(response, "This question is required.", count=4) def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): """When a user submits the Other Contacts form with other contacts selected, the domain request's From c9f511e39713eed59294a70e5fa8dd18c27f4b2a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:08:16 -0600 Subject: [PATCH 32/49] Add type ignore Overzealous linter --- src/registrar/forms/domain_request_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 752ad7f35..8d74f6f35 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -664,7 +664,7 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm): class CisaRepresentativeYesNoForm(BaseYesNoForm): """Yes/no toggle for the CISA regions question on additional details""" - form_is_checked = property(lambda self: self.domain_request.has_cisa_representative) + form_is_checked = property(lambda self: self.domain_request.has_cisa_representative) # type: ignore field_name = "has_cisa_representative" From 31787007c9f40f9dd84b54f17e708038b8abb652 Mon Sep 17 00:00:00 2001 From: Kristina Yin Date: Mon, 22 Apr 2024 09:14:26 -0700 Subject: [PATCH 33/49] add Christina to fixtures list --- src/registrar/fixtures_users.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index f03fb025d..c12e5c36d 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -92,6 +92,12 @@ class UserFixture: "last_name": "Chin", "email": "szu.chin@associates.cisa.dhs.gov", }, + { + "username": "1ca4fc9a-9358-4518-b4eb-cad7a96c05b8", + "first_name": "Christina", + "last_name": "Burnett", + "email": "christina.burnett@cisa.dhs.gov", + }, ] STAFF = [ From 97ccefba0d5679d4716619fdb1f671ccc0061a35 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:26:43 -0600 Subject: [PATCH 34/49] PR changes --- .../domain_request_additional_details.html | 2 +- .../templates/domain_request_review.html | 6 +++--- .../templates/domain_request_status.html | 20 +++++++++---------- .../templates/includes/summary_item.html | 6 ++++++ 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html index 44a725552..12c74aa47 100644 --- a/src/registrar/templates/domain_request_additional_details.html +++ b/src/registrar/templates/domain_request_additional_details.html @@ -14,7 +14,7 @@

Are you working with anyone from CISA regions on your domain request?

-

.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.

+

.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.

diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index ce061b321..7c4c293fc 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -158,15 +158,15 @@ {% if step == Step.ADDITIONAL_DETAILS %} {% namespaced_url 'domain-request' step as domain_request_url %} {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %} - {% include "includes/summary_item.html" with title=title sub_header_text='CISA regions representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url %} + {% include "includes/summary_item.html" with title=title sub_header_text='CISA regions representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %} {% endwith %}

Anything else

    - {% if domain_request.has_anything_else_text %} + {% if domain_request.anything_else %} {{domain_request.anything_else}} {% else %} - No + No {% endif %}
{% endif %} diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index 2be2cc44b..1c7f74948 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -116,17 +116,17 @@ {% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %} {% endif %} - {% if DomainRequest.has_cisa_representative or domain_request.has_anything_else_text %} - {% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regions representative' value=domain_request.cisa_representative_email heading_level=heading_level %} - -

Anything else

-
    - {% if domain_request.has_anything_else_text %} - {{domain_request.anything_else}} - {% else %} + {# We always show this field even if None #} + {% if DomainRequest %} + {% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regions representative' value=DomainRequest.cisa_representative_email custom_text_for_value_none='No' heading_level=heading_level %} +

    Anything else

    +
      + {% if DomainRequest.anything_else %} + {{DomainRequest.anything_else}} + {% else %} No - {% endif %} -
    + {% endif %} +
{% endif %} {% endwith %} diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 0988068df..a2f328e1f 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -42,6 +42,10 @@
{% endfor %}
+ {% elif custom_text_for_value_none %} +

+ {{ custom_text_for_value_none }} +

{% else %}

None @@ -95,6 +99,8 @@

{% if value %} {{ value }} + {% elif custom_text_for_value_none %} + {{ custom_text_for_value_none }} {% else %} None {% endif %} From 558a27d7d9ad02cad352b38c25607579c6814c8c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:15:08 -0600 Subject: [PATCH 35/49] PR suggestions --- ..._domaininformation_cisa_representative_email_and_more.py | 6 +++--- src/registrar/models/domain_information.py | 2 +- src/registrar/models/domain_request.py | 2 +- .../templates/domain_request_additional_details.html | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py index 1e53867d8..db42348fe 100644 --- a/src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py +++ b/src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-22 13:16 +# Generated by Django 4.2.10 on 2024-04-22 19:12 from django.db import migrations, models @@ -13,12 +13,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name="domaininformation", name="cisa_representative_email", - field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA region representative"), + field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA regional representative"), ), migrations.AddField( model_name="domainrequest", name="cisa_representative_email", - field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA region representative"), + field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA regional representative"), ), migrations.AddField( model_name="domainrequest", diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index d6fee3f79..63f99e0e4 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -232,7 +232,7 @@ class DomainInformation(TimeStampedModel): cisa_representative_email = models.EmailField( null=True, blank=True, - verbose_name="CISA region representative", + verbose_name="CISA regional representative", max_length=320, ) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 02d07c9b2..cf946d293 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -671,7 +671,7 @@ class DomainRequest(TimeStampedModel): cisa_representative_email = models.EmailField( null=True, blank=True, - verbose_name="CISA region representative", + verbose_name="CISA regional representative", max_length=320, ) diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html index 12c74aa47..50e9ce97f 100644 --- a/src/registrar/templates/domain_request_additional_details.html +++ b/src/registrar/templates/domain_request_additional_details.html @@ -2,7 +2,7 @@ {% load static field_helpers %} {% block form_instructions %} -

These questions are required (*).

+ These questions are required (*). {% endblock %} {% block form_required_fields_help_text %} @@ -14,7 +14,7 @@

Are you working with anyone from CISA regions on your domain request?

-

.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.

+

.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.

From f4e4739923991c537792968f1b31d2d475d80b4b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:22:42 -0600 Subject: [PATCH 36/49] Change regional rep --- src/registrar/templates/domain_request_review.html | 2 +- src/registrar/templates/domain_request_status.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index 7c4c293fc..5f359e95f 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -158,7 +158,7 @@ {% if step == Step.ADDITIONAL_DETAILS %} {% namespaced_url 'domain-request' step as domain_request_url %} {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %} - {% include "includes/summary_item.html" with title=title sub_header_text='CISA regions representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %} + {% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %} {% endwith %}

Anything else

diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index 1c7f74948..0ea16e3a3 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -118,7 +118,7 @@ {# We always show this field even if None #} {% if DomainRequest %} - {% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regions representative' value=DomainRequest.cisa_representative_email custom_text_for_value_none='No' heading_level=heading_level %} + {% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regional representative' value=DomainRequest.cisa_representative_email custom_text_for_value_none='No' heading_level=heading_level %}

Anything else

    {% if DomainRequest.anything_else %} From 2e83dc38d2664fce4a8b7abd180ae67a1ab839e8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:41:42 -0600 Subject: [PATCH 37/49] Change helper text --- src/registrar/templates/domain_request_additional_details.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html index 50e9ce97f..e13d3c7ee 100644 --- a/src/registrar/templates/domain_request_additional_details.html +++ b/src/registrar/templates/domain_request_additional_details.html @@ -13,7 +13,7 @@ {% block form_fields %}
    -

    Are you working with anyone from CISA regions on your domain request?

    +

    Are you working with a CISA regional representative on your domain request?

    .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.

    From 87f2dea92cb547f92fee768986b9d0b0c3df6953 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:18:31 -0600 Subject: [PATCH 38/49] Fixed width bottom table --- src/registrar/assets/sass/_theme/_tables.scss | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 0d58b5878..3f6c56447 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -108,8 +108,24 @@ padding: units(2) units(2) units(2) 0; } - th:first-of-type { - padding-left: 0; + th:nth-of-type(1) { + width: 200px; + } + + th:nth-of-type(2) { + width: 158px; + } + + th:nth-of-type(3) { + width: 120px; + } + + th:nth-of-type(4) { + width: 95px; + } + + th:nth-of-type(5) { + width: 85px; } thead tr:first-child th:first-child { From fdadd68b969edd37cda9a3739a15a01688053d34 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:26:43 -0600 Subject: [PATCH 39/49] More widths --- src/registrar/assets/sass/_theme/_tables.scss | 60 ++++++++++++------- src/registrar/templates/home.html | 4 +- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 3f6c56447..7214ffee0 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -108,28 +108,48 @@ padding: units(2) units(2) units(2) 0; } - th:nth-of-type(1) { - width: 200px; - } - - th:nth-of-type(2) { - width: 158px; - } - - th:nth-of-type(3) { - width: 120px; - } - - th:nth-of-type(4) { - width: 95px; - } - - th:nth-of-type(5) { - width: 85px; - } - thead tr:first-child th:first-child { border-top: none; } } } + +.dotgov-table__domain-requests { + th:nth-of-type(1) { + width: 200px; + } + + th:nth-of-type(2) { + width: 158px; + } + + th:nth-of-type(3) { + width: 120px; + } + + th:nth-of-type(4) { + width: 95px; + } + + th:nth-of-type(5) { + width: 85px; + } +} + +.dotgov-table__registered-domains { + th:nth-of-type(1) { + width: 200px; + } + + th:nth-of-type(2) { + width: 158px; + } + + th:nth-of-type(3) { + width: 215px; + } + + th:nth-of-type(4) { + width: 95px; + } +} diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index ea9276b9f..5c1bc893a 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -26,7 +26,7 @@

    Domains

    {% if domains %} - +
    @@ -104,7 +104,7 @@

    Domain requests

    {% if domain_requests %} -
    Your registered domains
    +
    From f589dfaa8e4dd8023bf44b9594584cbef7de601d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:59:42 -0600 Subject: [PATCH 40/49] max width --- src/registrar/assets/sass/_theme/_tables.scss | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 7214ffee0..37536d2be 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -113,43 +113,46 @@ } } } +@media (max-width: 1040px){ + .dotgov-table__domain-requests { + th:nth-of-type(1) { + width: 200px; + } -.dotgov-table__domain-requests { - th:nth-of-type(1) { - width: 200px; - } + th:nth-of-type(2) { + width: 158px; + } - th:nth-of-type(2) { - width: 158px; - } + th:nth-of-type(3) { + width: 120px; + } - th:nth-of-type(3) { - width: 120px; - } + th:nth-of-type(4) { + width: 95px; + } - th:nth-of-type(4) { - width: 95px; - } - - th:nth-of-type(5) { - width: 85px; + th:nth-of-type(5) { + width: 85px; + } } } -.dotgov-table__registered-domains { - th:nth-of-type(1) { - width: 200px; - } +@media (max-width: 1040px){ + .dotgov-table__registered-domains { + th:nth-of-type(1) { + width: 200px; + } - th:nth-of-type(2) { - width: 158px; - } + th:nth-of-type(2) { + width: 158px; + } - th:nth-of-type(3) { - width: 215px; - } + th:nth-of-type(3) { + width: 215px; + } - th:nth-of-type(4) { - width: 95px; + th:nth-of-type(4) { + width: 95px; + } } -} +} \ No newline at end of file From 94a9ab1d03116b3b8ce77794f694a468f40ac2aa Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:13:22 -0600 Subject: [PATCH 41/49] Change max width to min width --- src/registrar/assets/sass/_theme/_tables.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 37536d2be..5dc69e149 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -113,7 +113,7 @@ } } } -@media (max-width: 1040px){ +@media (min-width: 1040px){ .dotgov-table__domain-requests { th:nth-of-type(1) { width: 200px; @@ -137,7 +137,7 @@ } } -@media (max-width: 1040px){ +@media (min-width: 1040px){ .dotgov-table__registered-domains { th:nth-of-type(1) { width: 200px; From 938147f8a912384fc9471af8a0ee44193ba335ae Mon Sep 17 00:00:00 2001 From: Kristina Yin Date: Wed, 24 Apr 2024 11:27:31 -0700 Subject: [PATCH 42/49] add sandbox ID for Christina --- src/registrar/fixtures_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index c12e5c36d..4bc9caf30 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -93,7 +93,7 @@ class UserFixture: "email": "szu.chin@associates.cisa.dhs.gov", }, { - "username": "1ca4fc9a-9358-4518-b4eb-cad7a96c05b8", + "username": "66bb1a5a-a091-4d7f-a6cf-4d772b4711c7", "first_name": "Christina", "last_name": "Burnett", "email": "christina.burnett@cisa.dhs.gov", From 3ea97e44d292c6c331e1cad7f2c444fbedc53744 Mon Sep 17 00:00:00 2001 From: Kristina Yin Date: Wed, 24 Apr 2024 11:30:55 -0700 Subject: [PATCH 43/49] adding Christina's analyst account --- src/registrar/fixtures_users.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index 4bc9caf30..b1369ee8a 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -168,6 +168,12 @@ class UserFixture: "last_name": "Chin-Analyst", "email": "szu.chin@ecstech.com", }, + { + "username": "22f88aa5-3b54-4b1f-9c57-201fb02ddba7", + "first_name": "Christina-Analyst", + "last_name": "Burnett-Analyst", + "email": "christina.burnett@gwe.cisa.dhs.gov", + }, ] def load_users(cls, users, group_name): From a8c795187315f811ce0cbe6e30a325ea5359548f Mon Sep 17 00:00:00 2001 From: Kristina Yin <140533113+kristinacyin@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:41:20 -0700 Subject: [PATCH 44/49] Add missing bracket --- src/registrar/fixtures_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index cc16f2145..44704f721 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -99,7 +99,7 @@ class UserFixture: "last_name": "Burnett", "email": "christina.burnett@cisa.dhs.gov", }, - + { "username": "012f844d-8a0f-4225-9d82-cbf87bff1d3e", "first_name": "Riley", "last_name": "Orr", From f7871c086c6ef95df2322314f94da8bb5630b516 Mon Sep 17 00:00:00 2001 From: Kristina Yin <140533113+kristinacyin@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:56:48 -0700 Subject: [PATCH 45/49] Added missing bracket --- src/registrar/fixtures_users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index 44704f721..9137ba86b 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -181,6 +181,7 @@ class UserFixture: "last_name": "Burnett-Analyst", "email": "christina.burnett@gwe.cisa.dhs.gov", }, + { "username": "d9839768-0c17-4fa2-9c8e-36291eef5c11", "first_name": "Alex-Analyst", "last_name": "Mcelya-Analyst", From b249cd425f406604a14bdba32ad3142634ac60ce Mon Sep 17 00:00:00 2001 From: Kristina Yin <140533113+kristinacyin@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:07:33 -0700 Subject: [PATCH 46/49] remove trailing whitespace --- src/registrar/fixtures_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index 9137ba86b..d01cb48e5 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -98,7 +98,7 @@ class UserFixture: "first_name": "Christina", "last_name": "Burnett", "email": "christina.burnett@cisa.dhs.gov", - }, + }, { "username": "012f844d-8a0f-4225-9d82-cbf87bff1d3e", "first_name": "Riley", From 5e628c925145957a98d8ed15c6172c4eeefe37cc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:45:03 -0600 Subject: [PATCH 47/49] Fix migrations --- ...nformation_cisa_representative_email_and_more.py} | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) rename src/registrar/migrations/{0087_domaininformation_cisa_representative_email_and_more.py => 0088_domaininformation_cisa_representative_email_and_more.py} (76%) diff --git a/src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0088_domaininformation_cisa_representative_email_and_more.py similarity index 76% rename from src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py rename to src/registrar/migrations/0088_domaininformation_cisa_representative_email_and_more.py index db42348fe..95450fb3d 100644 --- a/src/registrar/migrations/0087_domaininformation_cisa_representative_email_and_more.py +++ b/src/registrar/migrations/0088_domaininformation_cisa_representative_email_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-22 19:12 +# Generated by Django 4.2.10 on 2024-04-25 16:44 from django.db import migrations, models @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("registrar", "0086_domaininformation_updated_federal_agency_and_more"), + ("registrar", "0087_alter_domain_deleted_alter_domain_expiration_date_and_more"), ] operations = [ @@ -37,15 +37,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="domaininformation", name="anything_else", - field=models.TextField( - blank=True, help_text="Anything else?", null=True, verbose_name="Additional details" - ), + field=models.TextField(blank=True, null=True, verbose_name="Additional details"), ), migrations.AlterField( model_name="domainrequest", name="anything_else", - field=models.TextField( - blank=True, help_text="Anything else?", null=True, verbose_name="Additional details" - ), + field=models.TextField(blank=True, null=True, verbose_name="Additional details"), ), ] From 8bfa8a4c57188089e9dd86ecd84b315bd8deb8d5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:11:28 -0600 Subject: [PATCH 48/49] Update docker-compose.yml --- src/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docker-compose.yml b/src/docker-compose.yml index c69c21192..31f65bcd0 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -108,7 +108,7 @@ services: - pa11y owasp: - image: owasp/zap2docker-stable + image: softwaresecurityproject/zap-bare command: zap-baseline.py -t http://app:8080 -c zap.conf -I -r zap_report.html volumes: - .:/zap/wrk/ From 4c93459a79c8a284b1c90f3b8775da265a2b4a70 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:27:00 -0600 Subject: [PATCH 49/49] Change image url --- src/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 31f65bcd0..1a9064ac8 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -108,7 +108,7 @@ services: - pa11y owasp: - image: softwaresecurityproject/zap-bare + image: ghcr.io/zaproxy/zaproxy:stable command: zap-baseline.py -t http://app:8080 -c zap.conf -I -r zap_report.html volumes: - .:/zap/wrk/
    Your domain requests