add FEB purpose questions

This commit is contained in:
matthewswspence 2025-03-03 16:11:50 -06:00
parent c821b20e8e
commit 4004a2f735
No known key found for this signature in database
GPG key ID: FB458202A7852BA4
8 changed files with 191 additions and 61 deletions

View file

@ -3,7 +3,6 @@ import { showElement } from './helpers.js';
export const domain_purpose_choice_callbacks = {
'new': {
callback: function(value, element) {
console.log("Callback for new")
//show the purpose details container
showElement(element);
// change just the text inside the em tag
@ -15,11 +14,10 @@ export const domain_purpose_choice_callbacks = {
'evidence user need for this new domain. ' +
'<span class="usa-label--required">*</span>';
},
element: document.getElementById('domain-purpose-details-container')
element: document.getElementById('purpose-details-container')
},
'redirect': {
callback: function(value, element) {
console.log("Callback for redirect")
// show the purpose details container
showElement(element);
// change just the text inside the em tag
@ -27,11 +25,10 @@ export const domain_purpose_choice_callbacks = {
labelElement.innerHTML = 'Explain why a redirect is necessary. ' +
'<span class="usa-label--required">*</span>';
},
element: document.getElementById('domain-purpose-details-container')
element: document.getElementById('purpose-details-container')
},
'other': {
callback: function(value, element) {
console.log("Callback for other")
// Show the purpose details container
showElement(element);
// change just the text inside the em tag
@ -39,6 +36,6 @@ export const domain_purpose_choice_callbacks = {
labelElement.innerHTML = 'Describe how this domain will be used. ' +
'<span class="usa-label--required">*</span>';
},
element: document.getElementById('domain-purpose-details-container')
element: document.getElementById('purpose-details-container')
}
}

View file

@ -29,8 +29,8 @@ hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-namin
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks);
hookupYesNoListener("purpose-has_timeframe", "domain-timeframe-details-container", null);
hookupYesNoListener("purpose-is_interagency_initiative", "domain-interagency-initaitive-details-container", null);
hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null);
hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
initializeUrbanizationToggle();
@ -56,4 +56,4 @@ initFormErrorHandling();
// Init the portfolio new member page
initPortfolioMemberPageRadio();
initPortfolioNewMemberPageToggle();
initAddNewMemberPageListeners();
initAddNewMemberPageListeners();

View file

@ -1,12 +1,17 @@
from django import forms
from django.core.validators import MaxLengthValidator
from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm, RegistrarForm
from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm
class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
field_name = "feb_purpose_choice"
form_choices = (("new", "Used for a new website"), ("redirect", "Used as a redirect for an existing website"), ("other", "Not for a website"))
form_choices = (
("new", "Used for a new website"),
("redirect", "Used as a redirect for an existing website"),
("other", "Not for a website"),
)
feb_purpose_choice = forms.ChoiceField(
required=True,
@ -15,12 +20,13 @@ class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
error_messages={
"required": "This question is required.",
},
label = "Select one"
label="Select one",
)
class PurposeDetailsForm(BaseDeletableRegistrarForm):
field_name="purpose"
field_name = "purpose"
purpose = forms.CharField(
label="Purpose",
@ -39,6 +45,7 @@ class PurposeDetailsForm(BaseDeletableRegistrarForm):
error_messages={"required": "Describe how youll use the .gov domain youre requesting."},
)
class FEBTimeFrameYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
"""
Form for determining whether the domain request comes with a target timeframe for launch.
@ -73,6 +80,7 @@ class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
error_messages={"required": "Provide details on your target timeframe."},
)
class FEBInteragencyInitiativeYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
"""
Form for determining whether the domain request is part of an interagency initative.
@ -92,11 +100,7 @@ class FEBInteragencyInitiativeYesNoForm(BaseDeletableRegistrarForm, BaseYesNoFor
class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm):
interagency_initiative_details = forms.CharField(
label="interagency_initiative_details",
widget=forms.Textarea(
attrs={
"aria-label": "Name the agencies that will be involved in this initiative."
}
),
widget=forms.Textarea(attrs={"aria-label": "Name the agencies that will be involved in this initiative."}),
validators=[
MaxLengthValidator(
2000,
@ -104,4 +108,4 @@ class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm):
)
],
error_messages={"required": "Name the agencies that will be involved in this initiative."},
)
)

View file

@ -53,7 +53,7 @@ class DomainRequest(TimeStampedModel):
def get_status_label(cls, status_name: str):
"""Returns the associated label for a given status name"""
return cls(status_name).label if status_name else None
class FEBPurposeChoices(models.TextChoices):
WEBSITE = "website"
REDIRECT = "redirect"
@ -558,8 +558,6 @@ class DomainRequest(TimeStampedModel):
help_text="Other domain names the creator provided for consideration",
)
other_contacts = models.ManyToManyField(
"registrar.Contact",
blank=True,

View file

@ -7,6 +7,10 @@
<h2>What is the purpose of your requested domain?</h2>
{% endblock %}
{% block form_required_fields_help_text %}
{# empty this block so it doesn't show on this page #}
{% endblock %}
{% block form_fields %}
{% if requires_feb_questions %}
<fieldset class="usa-fieldset margin-top-0 dotgov-domain-form">
@ -16,54 +20,54 @@
{{forms.3.management_form}}
{{forms.4.management_form}}
{{forms.5.management_form}}
<p class="usa-label">
<em>Select One <span class="usa-label--required">*</span></em>
<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.feb_purpose_choice %}
{% endwith %}
<div id="domain-purpose-details-container" class="conditional-panel display-none">
<div id="purpose-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" maxlength="2000" %}
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.1.purpose %}
{% endwith %}
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
</div>
<h2 class="margin-top-5">Do you have a target time frame for launching this domain?</h2>
<p class="usa-label">
<em>Select One <span class="usa-label--required">*</span></em>
<h2>Do you have a target time frame for launching this domain?</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_timeframe %}
{% endwith %}
<div id="domain-timeframe-details-container" class="conditional-panel">
<p>
<em>Provide details below <span class="usa-label--required">*</span></em>
<div id="purpose-timeframe-details-container" class="conditional-panel">
<p class="margin-bottom-0 margin-top-1">
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p>
{% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %}
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.3.time_frame_details %}
{% endwith %}
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
</div>
<h2>Will the domain name be used for an interagency initiative?</h2>
<p>
<em>Select One <span class="usa-label--required">*</span></em>
<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.4.is_interagency_initiative %}
{% endwith %}
<div id="domain-interagency-initaitive-details-container" class="conditional-panel">
<p class="usa-label">
<em>Provide details below <span class="usa-label--required">*</span></em>
<div id="purpose-interagency-initaitive-details-container" class="conditional-panel">
<p class="margin-bottom-0 margin-top-1">
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p>
{% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %}
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.5.interagency_initiative_details %}
{% endwith %}
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>

View file

@ -14,10 +14,11 @@ from registrar.forms.domain_request_wizard import (
OtherContactsForm,
RequirementsForm,
TribalGovernmentForm,
PurposeDetailsForm,
AnythingElseForm,
AboutYourOrganizationForm,
)
from registrar.forms.domainrequestwizard.purpose import PurposeDetailsForm
from registrar.forms.domain import ContactForm
from registrar.forms.portfolio import (
PortfolioInvitedMemberForm,

View file

@ -5,6 +5,7 @@ from django.utils import timezone
from django.conf import settings
from django.urls import reverse
from api.tests.common import less_console_noise_decorator
from registrar.utility.constants import BranchChoices
from .common import MockSESClient, completed_domain_request # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -2521,6 +2522,124 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(dotgov_page, "CityofEudoraKS.gov")
self.assertNotContains(dotgov_page, "medicare.gov")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_domain_request_dotgov_domain_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.
"""
agency, _ = FederalAgency.objects.get_or_create(
agency="US Treasury Dept",
federal_type=BranchChoices.EXECUTIVE,
)
portfolio, _ = Portfolio.objects.get_or_create(
creator=self.user,
organization_name="Test Portfolio",
organization_type=Portfolio.OrganizationChoices.FEDERAL,
federal_agency=agency,
)
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
intro_page = self.app.get(reverse("domain-request:start"))
# 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]
intro_form = intro_page.forms[0]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
intro_result = intro_form.submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
portfolio_requesting_entity = intro_result.follow()
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# ---- REQUESTING ENTITY PAGE ----
requesting_entity_form = portfolio_requesting_entity.forms[0]
requesting_entity_form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = False
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
requesting_entity_result = requesting_entity_form.submit()
# ---- CURRENT SITES PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_page = requesting_entity_result.follow()
current_sites_form = current_sites_page.forms[0]
current_sites_form["current_sites-0-website"] = "www.treasury.com"
# test saving the page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_result = current_sites_form.submit()
# ---- DOTGOV DOMAIN PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
dotgov_page = current_sites_result.follow()
# 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_form["dotgov_domain-requested_domain"] = "test.gov"
domain_form["dotgov_domain-feb_naming_requirements"] = "True"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
domain_result = domain_form.submit()
# ---- PURPOSE PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_page = domain_result.follow()
self.feb_purpose_page_tests(purpose_page)
def feb_purpose_page_tests(self, purpose_page):
self.assertContains(purpose_page, "What is the purpose of your requested domain?")
# Make sure the purpose selector form is present
self.assertContains(purpose_page, "feb_purpose_choice")
# Make sure the purpose details form is present
self.assertContains(purpose_page, "purpose-details")
# Make sure the timeframe yes/no form is present
self.assertContains(purpose_page, "purpose-has_timeframe")
# Make sure the timeframe details form is present
self.assertContains(purpose_page, "purpose-time_frame_details")
# Make sure the interagency initiative yes/no form is present
self.assertContains(purpose_page, "purpose-is_interagency_initiative")
# Make sure the interagency initiative details form is present
self.assertContains(purpose_page, "purpose-interagency_initiative_details")
def feb_dotgov_domain_tests(self, dotgov_page):
# Make sure the dynamic example content doesn't show
self.assertNotContains(dotgov_page, "medicare.gov")
# Make sure the link at the top directs to OPM FEB guidance
self.assertContains(dotgov_page, "https://get.gov/domains/executive-branch-guidance/")
# Check for header of first FEB form
self.assertContains(dotgov_page, "Does this submission meet each domain naming requirement?")
# Check for label of second FEB form
self.assertContains(dotgov_page, "Provide details below")
# Check that the yes/no form was included
self.assertContains(dotgov_page, "feb_naming_requirements")
# Check that the details form was included
self.assertContains(dotgov_page, "feb_naming_requirements_details")
@less_console_noise_decorator
def test_domain_request_formsets(self):
"""Users are able to add more than one of some fields."""

View file

@ -183,9 +183,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
return PortfolioDomainRequestStep if self.is_portfolio else Step
def requires_feb_questions(self) -> bool:
# TODO: this is for testing, revert later
return True
# return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature")
return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature")
@property
def prefix(self):
@ -713,19 +711,20 @@ class DotgovDomain(DomainRequestWizard):
class Purpose(DomainRequestWizard):
template_name = "domain_request_purpose.html"
forms = [purpose.FEBPurposeOptionsForm,
purpose.PurposeDetailsForm,
purpose.FEBTimeFrameYesNoForm,
purpose.FEBTimeFrameDetailsForm,
purpose.FEBInteragencyInitiativeYesNoForm,
purpose.FEBInteragencyInitiativeDetailsForm
]
forms = [
purpose.FEBPurposeOptionsForm,
purpose.PurposeDetailsForm,
purpose.FEBTimeFrameYesNoForm,
purpose.FEBTimeFrameDetailsForm,
purpose.FEBInteragencyInitiativeYesNoForm,
purpose.FEBInteragencyInitiativeDetailsForm,
]
def get_context_data(self):
context= super().get_context_data()
context = super().get_context_data()
context["requires_feb_questions"] = self.requires_feb_questions()
return context
def is_valid(self, forms_list: list) -> bool:
"""
Expected order of forms_list:
@ -753,17 +752,29 @@ class Purpose(DomainRequestWizard):
feb_initiative_details_form.mark_form_for_deletion()
# we only care about the purpose details form in this case since it's used in both instances
return purpose_details_form.is_valid()
if not feb_purpose_options_form.is_valid():
if feb_purpose_options_form.is_valid():
option = feb_purpose_options_form.cleaned_data.get("feb_purpose_choice")
if option == "new":
purpose_details_form.fields["purpose"].error_messages = {
"required": "Explain why a new domain is required."
}
elif option == "redirect":
purpose_details_form.fields["purpose"].error_messages = {
"required": "Explain why a redirect is needed."
}
elif option == "other":
purpose_details_form.fields["purpose"].error_messages = {
"required": "Provide details on how this domain will be used."
}
# If somehow none of these are true use the default error message
else:
# Ensure details form doesn't throw errors if it's not showing
purpose_details_form.mark_form_for_deletion()
feb_timeframe_valid = feb_timeframe_yes_no_form.is_valid()
feb_initiative_valid = feb_initiative_yes_no_form.is_valid()
logger.debug(f"feb timeframe yesno: {feb_timeframe_yes_no_form.cleaned_data.get('has_timeframe')}")
logger.debug(f"FEB initiative yesno: {feb_initiative_yes_no_form.cleaned_data.get('is_interagency_initiative')}")
if not feb_timeframe_valid or not feb_timeframe_yes_no_form.cleaned_data.get("has_timeframe"):
# Ensure details form doesn't throw errors if it's not showing
feb_timeframe_details_form.mark_form_for_deletion()
@ -772,15 +783,11 @@ class Purpose(DomainRequestWizard):
# Ensure details form doesn't throw errors if it's not showing
feb_initiative_details_form.mark_form_for_deletion()
for i, form in enumerate(forms_list):
logger.debug(f"Form {i} is marked for deletion: {form.form_data_marked_for_deletion}")
valid = all(form.is_valid() for form in forms_list if not form.form_data_marked_for_deletion)
return valid
class OtherContacts(DomainRequestWizard):
template_name = "domain_request_other_contacts.html"
forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]