From b022b13706de0dd85b11e897f2525614db1a79b8 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Thu, 6 Mar 2025 11:56:41 -0600 Subject: [PATCH] additional details forms --- src/registrar/assets/src/js/getgov/main.js | 3 +- src/registrar/forms/domain_request_wizard.py | 2 +- .../domainrequestwizard/additional_details.py | 85 +++++++++++++++++++ ..._domainrequest_feb_eop_contact_and_more.py | 30 +++++++ src/registrar/models/domain_request.py | 13 +++ ...lio_domain_request_additional_details.html | 60 +++++++++++-- src/registrar/tests/test_admin_request.py | 2 + src/registrar/tests/test_views_request.py | 38 ++++++++- src/registrar/views/domain_request.py | 42 ++++++++- 9 files changed, 260 insertions(+), 15 deletions(-) create mode 100644 src/registrar/forms/domainrequestwizard/additional_details.py create mode 100644 src/registrar/migrations/0143_domainrequest_feb_eop_contact_and_more.py diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js index 139c8484a..724f8b9d0 100644 --- a/src/registrar/assets/src/js/getgov/main.js +++ b/src/registrar/assets/src/js/getgov/main.js @@ -25,6 +25,8 @@ nameserversFormListener(); hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees'); hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null); hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null); +hookupYesNoListener("portfolio_additional_details-working_with_eop", "eop-contact-container", null); +hookupYesNoListener("portfolio_additional_details-has_anything_else_text", 'anything-else-details-container', null); hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container"); hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks); @@ -32,7 +34,6 @@ hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choic hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null); hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null); - initializeUrbanizationToggle(); userProfileListener(); diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 7cbb159b4..8c89a35ec 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -599,7 +599,7 @@ class DotGovDomainForm(RegistrarForm): return_type=ValidationReturnType.FORM_VALIDATION_ERROR, ) return validated - + def is_valid(self): return super().is_valid() diff --git a/src/registrar/forms/domainrequestwizard/additional_details.py b/src/registrar/forms/domainrequestwizard/additional_details.py new file mode 100644 index 000000000..8ae0629d5 --- /dev/null +++ b/src/registrar/forms/domainrequestwizard/additional_details.py @@ -0,0 +1,85 @@ +from django import forms +from django.core.validators import MaxLengthValidator +from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm +from registrar.models.contact import Contact + + +class WorkingWithEOPYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm): + """ + Form for determining if the Federal Executive Branch (FEB) agency is working with the + Executive Office of the President (EOP) on the domain request. + """ + + field_name = "working_with_eop" + + @property + def form_is_checked(self): + """ + Determines the initial checked state of the form based on the domain_request's attributes. + """ + return self.domain_request.working_with_eop + + +class EOPContactForm(BaseDeletableRegistrarForm): + """ + Form for contact information of the representative of the + Executive Office of the President (EOP) that the Federal + Executive Branch (FEB) agency is working with. + """ + + field_name = "eop_contact" + + first_name = forms.CharField( + label="First name / given name", + error_messages={"required": "Enter the first name / given name of this contact."}, + required=True, + ) + last_name = forms.CharField( + label="Last name / family name", + error_messages={"required": "Enter the last name / family name of this contact."}, + required=True, + ) + email = forms.EmailField( + label="Email", + max_length=None, + error_messages={ + "required": ("Enter an email address in the required format, like name@example.com."), + "invalid": ("Enter an email address in the required format, like name@example.com."), + }, + validators=[ + MaxLengthValidator( + 320, + message="Response must be less than 320 characters.", + ) + ], + required=True, + help_text="Enter an email address in the required format, like name@example.com.", + ) + + @classmethod + def from_database(cls, obj): + # if not obj.eop_contact: + # return {} + # return { + # "first_name": obj.feb_eop_contact.first_name, + # "last_name": obj.feb_eop_contact.last_name, + # "email": obj.feb_eop_contact.email, + # } + return {} + + def to_database(self, obj): + if not self.is_valid(): + return + obj.eop_contact = Contact.objects.create( + first_name=self.cleaned_data["first_name"], + last_name=self.cleaned_data["last_name"], + email=self.cleaned_data["email"], + ) + obj.save() + + +class FEBAnythingElseYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm): + """Yes/no toggle for the anything else question on additional details""" + + 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/migrations/0143_domainrequest_feb_eop_contact_and_more.py b/src/registrar/migrations/0143_domainrequest_feb_eop_contact_and_more.py new file mode 100644 index 000000000..7001e47d6 --- /dev/null +++ b/src/registrar/migrations/0143_domainrequest_feb_eop_contact_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.17 on 2025-03-05 15:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0142_domainrequest_feb_purpose_choice_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="domainrequest", + name="eop_contact", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="eop_contact", + to="registrar.contact", + ), + ), + migrations.AddField( + model_name="domainrequest", + name="working_with_eop", + field=models.BooleanField(blank=True, null=True), + ), + ] diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index b7aaff65d..b181094aa 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -523,6 +523,19 @@ class DomainRequest(TimeStampedModel): choices=FEBPurposeChoices.choices, ) + working_with_eop = models.BooleanField( + null=True, + blank=True, + ) + + eop_contact = models.ForeignKey( + "registrar.Contact", + null=True, + blank=True, + related_name="eop_contact", + on_delete=models.PROTECT, + ) + # This field is alternately used for generic domain purpose explanations # and for explanations of the specific purpose chosen with feb_purpose_choice # by a Federal Executive Branch agency. diff --git a/src/registrar/templates/portfolio_domain_request_additional_details.html b/src/registrar/templates/portfolio_domain_request_additional_details.html index 5bc529243..d7d53dd1a 100644 --- a/src/registrar/templates/portfolio_domain_request_additional_details.html +++ b/src/registrar/templates/portfolio_domain_request_additional_details.html @@ -6,16 +6,60 @@ {% endblock %} {% block form_fields %} + {% if requires_feb_questions %} + {{forms.2.management_form}} + {{forms.3.management_form}} + {{forms.4.management_form}} + {{forms.5.management_form}} +
+

Are you working with someone in the Executive Office of the President (EOP) on this request?

+ +

+ Select one. * +

+ {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% input_with_errors forms.0.working_with_eop %} + {% endwith %} -
+ + +

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

+

+ Select one. * +

+ {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% input_with_errors forms.2.has_anything_else_text %} + {% endwith %} + + +
+ {% else %} +

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

-
+
-
-

This question is optional.

- {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} - {% input_with_errors forms.0.anything_else %} - {% endwith %} -
+
+

This question is optional.

+ {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} + {% input_with_errors forms.0.anything_else %} + {% endwith %} +
+ {% endif %} {% endblock %} diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index 9320dd3d3..7bc150326 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -1984,6 +1984,8 @@ class TestDomainRequestAdmin(MockEppLib): "feb_naming_requirements", "feb_naming_requirements_details", "feb_purpose_choice", + "working_with_eop", + "eop_contact", "purpose", "has_timeframe", "time_frame_details", diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 7b117f67d..7db89a9e8 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -2550,7 +2550,7 @@ class DomainRequestTests(TestWithUser, WebTest): # @less_console_noise_decorator @override_flag("organization_feature", active=True) - def test_domain_request_dotgov_domain_FEB_questions(self): + def test_domain_request_FEB_questions(self): """ Test that for a member of a federal executive branch portfolio with org feature on, the dotgov domain page contains additional questions for OMB. @@ -2612,13 +2612,14 @@ class DomainRequestTests(TestWithUser, WebTest): # separate out these tests for readability self.feb_dotgov_domain_tests(dotgov_page) - # Now proceed with the actual test domain_form = dotgov_page.forms[0] domain = "test.gov" domain_form["dotgov_domain-requested_domain"] = domain domain_form["dotgov_domain-feb_naming_requirements"] = "True" domain_form["dotgov_domain-feb_naming_requirements_details"] = "test" - with patch('registrar.forms.domain_request_wizard.DotGovDomainForm.clean_requested_domain', return_value=domain): # noqa + with patch( + "registrar.forms.domain_request_wizard.DotGovDomainForm.clean_requested_domain", return_value=domain + ): # noqa self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) domain_result = domain_form.submit() @@ -2628,6 +2629,20 @@ class DomainRequestTests(TestWithUser, WebTest): self.feb_purpose_page_tests(purpose_page) + purpose_form = purpose_page.forms[0] + purpose_form["purpose-feb_purpose_choice"] = "redirect" + purpose_form["purpose-purpose"] = "test" + purpose_form["purpose-has_timeframe"] = "True" + purpose_form["purpose-time_frame_details"] = "test" + purpose_form["purpose-is_interagency_initiative"] = "True" + purpose_form["purpose-interagency_initiative_details"] = "test" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + purpose_result = purpose_form.submit() + + # ---- ADDITIONAL DETAILS PAGE ---- + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + additional_details_page = purpose_result.follow() + self.feb_additional_details_page_tests(additional_details_page) def feb_purpose_page_tests(self, purpose_page): self.assertContains(purpose_page, "What is the purpose of your requested domain?") @@ -2669,6 +2684,23 @@ class DomainRequestTests(TestWithUser, WebTest): # Check that the details form was included self.assertContains(dotgov_page, "feb_naming_requirements_details") + def feb_additional_details_page_tests(self, additional_details_page): + test_text = "Are you working with someone in the Executive Office of the President (EOP) on this request?" + self.assertContains(additional_details_page, test_text) + + # Make sure the EOP form is present + self.assertContains(additional_details_page, "working_with_eop") + + # Make sure the EOP contact form is present + self.assertContains(additional_details_page, "eop-contact-container") + self.assertContains(additional_details_page, "additional_details-first_name") + self.assertContains(additional_details_page, "additional_details-last_name") + self.assertContains(additional_details_page, "additional_details-email") + + # Make sure the additional details form is present + self.assertContains(additional_details_page, "additional_details-has_anything_else_text") + self.assertContains(additional_details_page, "additional_details-anything_else") + @less_console_noise_decorator def test_domain_request_formsets(self): """Users are able to add more than one of some fields.""" diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 55cd9a19e..1aeb52a20 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -15,7 +15,7 @@ from registrar.decorators import ( grant_access, ) from registrar.forms import domain_request_wizard as forms -from registrar.forms.domainrequestwizard import purpose +from registrar.forms.domainrequestwizard import (purpose, additional_details) from registrar.forms.utility.wizard_form_helper import request_step_list from registrar.models import DomainRequest from registrar.models.contact import Contact @@ -609,7 +609,45 @@ class RequestingEntity(DomainRequestWizard): class PortfolioAdditionalDetails(DomainRequestWizard): template_name = "portfolio_domain_request_additional_details.html" - forms = [forms.PortfolioAnythingElseForm] + forms = [ + additional_details.WorkingWithEOPYesNoForm, + additional_details.EOPContactForm, + additional_details.FEBAnythingElseYesNoForm, + forms.PortfolioAnythingElseForm, + ] + + def get_context_data(self): + context = super().get_context_data() + context["requires_feb_questions"] = self.requires_feb_questions() + return context + + def is_valid(self, forms: list) -> bool: + """ + Validates the forms for portfolio additional details. + + Expected order of forms_list: + 0: WorkingWithEOPYesNoForm + 1: EOPContactForm + 2: FEBAnythingElseYesNoForm + 3: PortfolioAnythingElseForm + """ + eop_forms_valid = True + if not forms[0].is_valid(): + # If the user isn't working with EOP, don't validate the EOP contact form + forms[1].mark_form_for_deletion() + eop_forms_valid = False + if forms[0].cleaned_data.get("working_with_eop"): + eop_forms_valid = forms[1].is_valid() + else: + forms[1].mark_form_for_deletion() + anything_else_forms_valid = True + if not forms[2].is_valid(): + forms[3].mark_form_for_deletion() + anything_else_forms_valid = False + if forms[2].cleaned_data.get("has_anything_else_text"): + forms[3].fields["anything_else"].required = True + anything_else_forms_valid = forms[3].is_valid() + return (eop_forms_valid and anything_else_forms_valid) # Non-portfolio pages