additional details forms

This commit is contained in:
matthewswspence 2025-03-06 11:56:41 -06:00
parent 9ddf7a6cd0
commit b022b13706
No known key found for this signature in database
GPG key ID: FB458202A7852BA4
9 changed files with 260 additions and 15 deletions

View file

@ -25,6 +25,8 @@ nameserversFormListener();
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees'); 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_anything_else_text",'anything-else', null);
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', 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"); hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container");
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks); 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-has_timeframe", "purpose-timeframe-details-container", null);
hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null); hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
initializeUrbanizationToggle(); initializeUrbanizationToggle();
userProfileListener(); userProfileListener();

View file

@ -599,7 +599,7 @@ class DotGovDomainForm(RegistrarForm):
return_type=ValidationReturnType.FORM_VALIDATION_ERROR, return_type=ValidationReturnType.FORM_VALIDATION_ERROR,
) )
return validated return validated
def is_valid(self): def is_valid(self):
return super().is_valid() return super().is_valid()

View file

@ -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"

View file

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

View file

@ -523,6 +523,19 @@ class DomainRequest(TimeStampedModel):
choices=FEBPurposeChoices.choices, 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 # This field is alternately used for generic domain purpose explanations
# and for explanations of the specific purpose chosen with feb_purpose_choice # and for explanations of the specific purpose chosen with feb_purpose_choice
# by a Federal Executive Branch agency. # by a Federal Executive Branch agency.

View file

@ -6,16 +6,60 @@
{% endblock %} {% endblock %}
{% block form_fields %} {% block form_fields %}
{% if requires_feb_questions %}
{{forms.2.management_form}}
{{forms.3.management_form}}
{{forms.4.management_form}}
{{forms.5.management_form}}
<fieldset class="usa-fieldset">
<h2 class="margin-top-0 margin-bottom-0">Are you working with someone in the Executive Office of the President (EOP) on this request?</h2>
<p class="margin-bottom-0 margin-top-1">
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.0.working_with_eop %}
{% endwith %}
<fieldset class="usa-fieldset"> <div id="eop-contact-container" class="conditional-panel display-none">
<p class="margin-bottom-0 margin-top-1">
Provide the name and email of the person you're working with.<span class="usa-label--required">*</span>
</p>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.1.first_name %}
{% input_with_errors forms.1.last_name %}
{% input_with_errors forms.1.email %}
{% endwith %}
</div>
<h2 class="margin-top-0 margin-bottom-0">Is there anything else you'd like us to know about your domain request?</h2>
<p class="margin-bottom-0 margin-top-1">
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.2.has_anything_else_text %}
{% endwith %}
<div id="anything-else-details-container" class="conditional-panel display-none">
<p class="usa-label">
<em>Provide details below <span class="usa-label--required">*</span></em>
</p>
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.3.anything_else %}
{% endwith %}
</div>
</fieldset>
{% else %}
<fieldset class="usa-fieldset">
<h2 class="margin-top-0 margin-bottom-0">Is there anything else youd like us to know about your domain request?</h2> <h2 class="margin-top-0 margin-bottom-0">Is there anything else youd like us to know about your domain request?</h2>
</legend> </legend>
</fieldset> </fieldset>
<div id="anything-else"> <div id="anything-else">
<p><em>This question is optional.</em></p> <p><em>This question is optional.</em></p>
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %} {% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.anything_else %} {% input_with_errors forms.0.anything_else %}
{% endwith %} {% endwith %}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -1984,6 +1984,8 @@ class TestDomainRequestAdmin(MockEppLib):
"feb_naming_requirements", "feb_naming_requirements",
"feb_naming_requirements_details", "feb_naming_requirements_details",
"feb_purpose_choice", "feb_purpose_choice",
"working_with_eop",
"eop_contact",
"purpose", "purpose",
"has_timeframe", "has_timeframe",
"time_frame_details", "time_frame_details",

View file

@ -2550,7 +2550,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# @less_console_noise_decorator # @less_console_noise_decorator
@override_flag("organization_feature", active=True) @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 Test that for a member of a federal executive branch portfolio with org feature on, the dotgov domain page
contains additional questions for OMB. contains additional questions for OMB.
@ -2612,13 +2612,14 @@ class DomainRequestTests(TestWithUser, WebTest):
# separate out these tests for readability # separate out these tests for readability
self.feb_dotgov_domain_tests(dotgov_page) self.feb_dotgov_domain_tests(dotgov_page)
# Now proceed with the actual test
domain_form = dotgov_page.forms[0] domain_form = dotgov_page.forms[0]
domain = "test.gov" domain = "test.gov"
domain_form["dotgov_domain-requested_domain"] = domain domain_form["dotgov_domain-requested_domain"] = domain
domain_form["dotgov_domain-feb_naming_requirements"] = "True" domain_form["dotgov_domain-feb_naming_requirements"] = "True"
domain_form["dotgov_domain-feb_naming_requirements_details"] = "test" 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) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
domain_result = domain_form.submit() domain_result = domain_form.submit()
@ -2628,6 +2629,20 @@ class DomainRequestTests(TestWithUser, WebTest):
self.feb_purpose_page_tests(purpose_page) 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): def feb_purpose_page_tests(self, purpose_page):
self.assertContains(purpose_page, "What is the purpose of your requested domain?") 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 # Check that the details form was included
self.assertContains(dotgov_page, "feb_naming_requirements_details") 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 @less_console_noise_decorator
def test_domain_request_formsets(self): def test_domain_request_formsets(self):
"""Users are able to add more than one of some fields.""" """Users are able to add more than one of some fields."""

View file

@ -15,7 +15,7 @@ from registrar.decorators import (
grant_access, grant_access,
) )
from registrar.forms import domain_request_wizard as forms 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.forms.utility.wizard_form_helper import request_step_list
from registrar.models import DomainRequest from registrar.models import DomainRequest
from registrar.models.contact import Contact from registrar.models.contact import Contact
@ -609,7 +609,45 @@ class RequestingEntity(DomainRequestWizard):
class PortfolioAdditionalDetails(DomainRequestWizard): class PortfolioAdditionalDetails(DomainRequestWizard):
template_name = "portfolio_domain_request_additional_details.html" 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 # Non-portfolio pages