Merge branch 'main' of https://github.com/cisagov/manage.get.gov into rh/quick-fix-action-needed-emails

This commit is contained in:
Rebecca Hsieh 2025-03-14 11:39:49 -07:00
commit ce4f62f1d0
No known key found for this signature in database
15 changed files with 714 additions and 38 deletions

View file

@ -0,0 +1,41 @@
import { showElement } from './helpers.js';
export const domain_purpose_choice_callbacks = {
'new': {
callback: function(value, element) {
//show the purpose details container
showElement(element);
// change just the text inside the em tag
const labelElement = element.querySelector('.usa-label em');
labelElement.innerHTML = 'Explain why a new domain is required and why a ' +
'subdomain of an existing domain doesn\'t meet your needs.' +
'<br><br>' + // Adding double line break for spacing
'Include any data that supports a clear public benefit or ' +
'evidence user need for this new domain. ' +
'<span class="usa-label--required">*</span>';
},
element: document.getElementById('purpose-details-container')
},
'redirect': {
callback: function(value, element) {
// show the purpose details container
showElement(element);
// change just the text inside the em tag
const labelElement = element.querySelector('.usa-label em');
labelElement.innerHTML = 'Explain why a redirect is necessary. ' +
'<span class="usa-label--required">*</span>';
},
element: document.getElementById('purpose-details-container')
},
'other': {
callback: function(value, element) {
// Show the purpose details container
showElement(element);
// change just the text inside the em tag
const labelElement = element.querySelector('.usa-label em');
labelElement.innerHTML = 'Describe how this domain will be used. ' +
'<span class="usa-label--required">*</span>';
},
element: document.getElementById('purpose-details-container')
}
}

View file

@ -1,4 +1,4 @@
import { hookupYesNoListener } from './radios.js'; import { hookupYesNoListener, hookupCallbacksToRadioToggler } from './radios.js';
import { initDomainValidators } from './domain-validators.js'; import { initDomainValidators } from './domain-validators.js';
import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js'; import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js';
import { initFormNameservers } from './form-nameservers' import { initFormNameservers } from './form-nameservers'
@ -16,6 +16,7 @@ import { initDomainManagersPage } from './domain-managers.js';
import { initDomainDSData } from './domain-dsdata.js'; import { initDomainDSData } from './domain-dsdata.js';
import { initDomainDNSSEC } from './domain-dnssec.js'; import { initDomainDNSSEC } from './domain-dnssec.js';
import { initFormErrorHandling } from './form-errors.js'; import { initFormErrorHandling } from './form-errors.js';
import { domain_purpose_choice_callbacks } from './domain-purpose-form.js';
import { initButtonLinks } from '../getgov-admin/button-utils.js'; import { initButtonLinks } from '../getgov-admin/button-utils.js';
initDomainValidators(); initDomainValidators();
@ -27,6 +28,14 @@ initFormNameservers();
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("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container");
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks);
hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null);
hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
initializeUrbanizationToggle(); initializeUrbanizationToggle();
userProfileListener(); userProfileListener();

View file

@ -17,7 +17,7 @@ export function hookupYesNoListener(radioButtonName, elementIdToShowIfYes, eleme
'False': elementIdToShowIfNo 'False': elementIdToShowIfNo
}); });
} }
/** /**
* Hookup listeners for radio togglers in form fields. * Hookup listeners for radio togglers in form fields.
* *
@ -75,3 +75,57 @@ export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
handleRadioButtonChange(); handleRadioButtonChange();
} }
} }
/**
* Hookup listeners for radio togglers in form fields.
*
* Parameters:
* - radioButtonName: The "name=" value for the radio buttons being used as togglers
* - valueToCallbackMap: An object where keys are the values of the radio buttons,
* and values are dictionaries containing a 'callback' key and an optional 'element' key.
* If provided, the element will be passed in as the second argument to the callback function.
*
* Usage Example:
* Assuming you have radio buttons with values 'option1', 'option2', and 'option3',
* and corresponding callback functions 'function1', 'function2', 'function3' that will
* apply to elements 'element1', 'element2', 'element3' respectively.
*
* hookupCallbacksToRadioToggler('exampleRadioGroup', {
* 'option1': {callback: function1, element: element1},
* 'option2': {callback: function2, element: element2},
* 'option3': {callback: function3} // No element provided
* });
*
* Picking the 'option1' radio button will call function1('option1', element1).
* Picking the 'option3' radio button will call function3('option3') without a second parameter.
**/
export function hookupCallbacksToRadioToggler(radioButtonName, valueToCallbackMap) {
// Get the radio buttons
let radioButtons = document.querySelectorAll(`input[name="${radioButtonName}"]`);
function handleRadioButtonChange() {
// Find the checked radio button
let radioButtonChecked = document.querySelector(`input[name="${radioButtonName}"]:checked`);
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
// Execute the callback function for the selected value
if (selectedValue && valueToCallbackMap[selectedValue]) {
const entry = valueToCallbackMap[selectedValue];
if ('element' in entry) {
entry.callback(selectedValue, entry.element);
} else {
entry.callback(selectedValue);
}
}
}
if (radioButtons && radioButtons.length) {
// Add event listener to each radio button
radioButtons.forEach(function (radioButton) {
radioButton.addEventListener('change', handleRadioButtonChange);
});
// Initialize by checking the current state
handleRadioButtonChange();
}
}

View file

@ -608,7 +608,10 @@ class DotGovDomainForm(RegistrarForm):
) )
class PurposeForm(RegistrarForm): class PurposeDetailsForm(BaseDeletableRegistrarForm):
field_name = "purpose"
purpose = forms.CharField( purpose = forms.CharField(
label="Purpose", label="Purpose",
widget=forms.Textarea( widget=forms.Textarea(

123
src/registrar/forms/feb.py Normal file
View file

@ -0,0 +1,123 @@
from django import forms
from django.core.validators import MaxLengthValidator
from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm
class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm):
"""
Form for verifying if the domain request meets the Federal Executive Branch domain naming requirements.
If the "no" option is selected, details must be provided via the separate details form.
"""
field_name = "feb_naming_requirements"
@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.feb_naming_requirements
class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
# Text area for additional details; rendered conditionally when "no" is selected.
feb_naming_requirements_details = forms.CharField(
widget=forms.Textarea(attrs={"maxlength": "2000"}),
max_length=2000,
required=True,
error_messages={"required": ("This field is required.")},
validators=[
MaxLengthValidator(
2000,
message="Response must be less than 2000 characters.",
)
],
label="",
help_text="Maximum 2000 characters allowed.",
)
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"),
)
feb_purpose_choice = forms.ChoiceField(
required=True,
choices=form_choices,
widget=forms.RadioSelect,
error_messages={
"required": "This question is required.",
},
label="Select one",
)
class FEBTimeFrameYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
"""
Form for determining whether the domain request comes with a target timeframe for launch.
If the "no" option is selected, details must be provided via the separate details form.
"""
field_name = "has_timeframe"
@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.has_timeframe
class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
time_frame_details = forms.CharField(
label="time_frame_details",
widget=forms.Textarea(
attrs={
"aria-label": "Provide details on your target timeframe. \
Is there a special significance to this date (legal requirement, announcement, event, etc)?"
}
),
validators=[
MaxLengthValidator(
2000,
message="Response must be less than 2000 characters.",
)
],
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.
If the "no" option is selected, details must be provided via the separate details form.
"""
field_name = "is_interagency_initiative"
@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.is_interagency_initiative
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."}),
validators=[
MaxLengthValidator(
2000,
message="Response must be less than 2000 characters.",
)
],
error_messages={"required": "Name the agencies that will be involved in this initiative."},
)

View file

@ -0,0 +1,50 @@
# Generated by Django 4.2.17 on 2025-03-10 19:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0141_alter_portfolioinvitation_additional_permissions_and_more"),
]
operations = [
migrations.AddField(
model_name="domainrequest",
name="feb_naming_requirements",
field=models.BooleanField(blank=True, null=True),
),
migrations.AddField(
model_name="domainrequest",
name="feb_naming_requirements_details",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name="domainrequest",
name="feb_purpose_choice",
field=models.CharField(
blank=True, choices=[("website", "Website"), ("redirect", "Redirect"), ("other", "Other")], null=True
),
),
migrations.AddField(
model_name="domainrequest",
name="has_timeframe",
field=models.BooleanField(blank=True, null=True),
),
migrations.AddField(
model_name="domainrequest",
name="interagency_initiative_details",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name="domainrequest",
name="is_interagency_initiative",
field=models.BooleanField(blank=True, null=True),
),
migrations.AddField(
model_name="domainrequest",
name="time_frame_details",
field=models.TextField(blank=True, null=True),
),
]

View file

@ -54,6 +54,11 @@ class DomainRequest(TimeStampedModel):
"""Returns the associated label for a given status name""" """Returns the associated label for a given status name"""
return cls(status_name).label if status_name else None return cls(status_name).label if status_name else None
class FEBPurposeChoices(models.TextChoices):
WEBSITE = "website"
REDIRECT = "redirect"
OTHER = "other"
class StateTerritoryChoices(models.TextChoices): class StateTerritoryChoices(models.TextChoices):
ALABAMA = "AL", "Alabama (AL)" ALABAMA = "AL", "Alabama (AL)"
ALASKA = "AK", "Alaska (AK)" ALASKA = "AK", "Alaska (AK)"
@ -501,6 +506,51 @@ class DomainRequest(TimeStampedModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
# Fields specific to Federal Executive Branch agencies, used by OMB for reviewing requests
feb_naming_requirements = models.BooleanField(
null=True,
blank=True,
)
feb_naming_requirements_details = models.TextField(
null=True,
blank=True,
)
feb_purpose_choice = models.CharField(
null=True,
blank=True,
choices=FEBPurposeChoices.choices,
)
# 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.
purpose = models.TextField(
null=True,
blank=True,
)
has_timeframe = models.BooleanField(
null=True,
blank=True,
)
time_frame_details = models.TextField(
null=True,
blank=True,
)
is_interagency_initiative = models.BooleanField(
null=True,
blank=True,
)
interagency_initiative_details = models.TextField(
null=True,
blank=True,
)
alternative_domains = models.ManyToManyField( alternative_domains = models.ManyToManyField(
"registrar.Website", "registrar.Website",
blank=True, blank=True,
@ -508,11 +558,6 @@ class DomainRequest(TimeStampedModel):
help_text="Other domain names the creator provided for consideration", help_text="Other domain names the creator provided for consideration",
) )
purpose = models.TextField(
null=True,
blank=True,
)
other_contacts = models.ManyToManyField( other_contacts = models.ManyToManyField(
"registrar.Contact", "registrar.Contact",
blank=True, blank=True,
@ -1389,6 +1434,12 @@ class DomainRequest(TimeStampedModel):
has_details = False has_details = False
return has_details return has_details
def is_feb(self) -> bool:
"""Is this domain request for a Federal Executive Branch agency?"""
if self.portfolio:
return self.portfolio.federal_type == BranchChoices.EXECUTIVE
return False
def is_federal(self) -> Union[bool, None]: def is_federal(self) -> Union[bool, None]:
"""Is this domain request for a federal agency? """Is this domain request for a federal agency?

View file

@ -58,7 +58,7 @@
{% if request.path|endswith:"renewal"%} {% if request.path|endswith:"renewal"%}
<h1>Renew {{domain.name}} </h1> <h1>Renew {{domain.name}} </h1>
{%else%} {%else%}
<h1 class="break-word">Domain Overview</h1> <h1 class="break-word">Domain overview</h1>
{% endif%} {% endif%}
{% endblock %} {# domain_content #} {% endblock %} {# domain_content #}

View file

@ -99,7 +99,7 @@
{% if domain.dnssecdata is not None %} {% if domain.dnssecdata is not None %}
{% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %} {% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
{% else %} {% else %}
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %} {% include "includes/summary_item.html" with title='DNSSEC' value='Not enabled' edit_link=url editable=is_editable %}
{% endif %} {% endif %}
{% if portfolio %} {% if portfolio %}

View file

@ -2,19 +2,19 @@
{% load static field_helpers url_helpers %} {% load static field_helpers url_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Before requesting a .gov domain, please make sure it meets <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing' %}">our naming requirements</a>. Your domain name must: <p>Before requesting a .gov domain, please make sure it meets <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% if requires_feb_questions %}https://get.gov/domains/executive-branch-guidance/{% else %}{% public_site_url 'domains/choosing' %}{% endif %}">our naming requirements</a>. Your domain name must:
<ul class="usa-list"> <ul class="usa-list">
<li>Be available </li> <li>Be available </li>
<li>Relate to your organizations name, location, and/or services </li> <li>Relate to your organization's name, location, and/or services </li>
<li>Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience) </li> <li>Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience) </li>
</ul> </ul>
</p> </p>
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations. <p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations.
{% if not is_federal %}In most instances, this requires including your states two-letter abbreviation.{% endif %}</p> {% if not is_federal %}In most instances, this requires including your state's two-letter abbreviation.{% endif %}</p>
{% if not portfolio %} {% if not portfolio %}
<p>Requests for your organizations initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p> <p>Requests for your organization's initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
{% endif %} {% endif %}
<p>Note that <strong>only federal agencies can request generic terms</strong> like <p>Note that <strong>only federal agencies can request generic terms</strong> like
@ -41,9 +41,10 @@
<legend> <legend>
<h2>What .gov domain do you want?</h2> <h2>What .gov domain do you want?</h2>
</legend> </legend>
<p id="domain_instructions" class="margin-top-05">
<p id="domain_instructions" class="margin-top-05">After you enter your domain, well make sure its available and that it meets some of our naming requirements. If your domain passes these initial checks, well verify that it meets all our requirements after you complete the rest of this form.</p> After you enter your domain, we'll make sure it's available and that it meets some of our naming requirements.
If your domain passes these initial checks, we'll verify that it meets all our requirements after you complete the rest of this form.
</p>
{% with attr_aria_labelledby="domain_instructions domain_instructions2" attr_aria_describedby="id_dotgov_domain-requested_domain--toast" %} {% with attr_aria_labelledby="domain_instructions domain_instructions2" attr_aria_describedby="id_dotgov_domain-requested_domain--toast" %}
{# attr_validate / validate="domain" invokes code in getgov.min.js #} {# attr_validate / validate="domain" invokes code in getgov.min.js #}
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %} {% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
@ -63,10 +64,9 @@
<legend> <legend>
<h2 id="alternative-domains-title">Alternative domains (optional)</h2> <h2 id="alternative-domains-title">Alternative domains (optional)</h2>
</legend> </legend>
<p id="alt_domain_instructions" class="margin-top-05">
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains youd like if we cant give Are there other domains you'd like if we can't give you your first choice?
you your first choice?</p> </p>
{% with attr_aria_labelledby="alt_domain_instructions" %} {% with attr_aria_labelledby="alt_domain_instructions" %}
{# Will probably want to remove blank-ok and do related cleanup when we implement delete #} {# Will probably want to remove blank-ok and do related cleanup when we implement delete #}
{% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %} {% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %}
@ -83,10 +83,10 @@
<div class="usa-sr-only" id="alternative-domains__add-another-alternative">Add another alternative domain</div> <div class="usa-sr-only" id="alternative-domains__add-another-alternative">Add another alternative domain</div>
<button aria-labelledby="alternative-domains-title" aria-describedby="alternative-domains__add-another-alternative" type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form"> <button aria-labelledby="alternative-domains-title" aria-describedby="alternative-domains__add-another-alternative" type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use> <use xlink:href="{% static 'img/sprite.svg' %}#add_circle"></use>
</svg><span class="margin-left-05">Add another alternative</span> </svg>
<span class="margin-left-05">Add another alternative</span>
</button> </button>
<div class="margin-bottom-3"> <div class="margin-bottom-3">
<div class="usa-sr-only" id="alternative-domains__check-availability">Check domain availability</div> <div class="usa-sr-only" id="alternative-domains__check-availability">Check domain availability</div>
<button <button
@ -98,10 +98,41 @@
aria-describedby="alternative-domains__check-availability" aria-describedby="alternative-domains__check-availability"
>Check availability</button> >Check availability</button>
</div> </div>
<p class="margin-top-05">
If you're not sure this is the domain you want, that's ok. You can change the domain later.
</p>
</fieldset>
<p class="margin-top-05">If youre not sure this is the domain you want, thats ok. You can change the domain later. </p> {{ forms.2.management_form }}
{{ forms.3.management_form }}
</fieldset> {% if requires_feb_questions %}
<fieldset class="usa-fieldset margin-top-0 dotgov-domain-form">
<legend>
<h2>Does this submission meet each domain naming requirement?</h2>
</legend>
<p id="dotgov-domain-naming-requirements" class="margin-top-05">
OMB will review each request against the domain
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="https://get.gov/domains/executive-branch-guidance/">
naming requirements for executive branch agencies
</a>.
Agency submissions are expected to meet each requirement.
</p>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.2.feb_naming_requirements %}
{% endwith %}
{# Conditional Details Field only shown when the executive naming requirements radio is "False" #}
<div id="domain-naming-requirements-details-container" class="conditional-panel" style="display: none;">
<p class="usa-label">
Provide details below <span class="usa-label--required">*</span>
</p>
{% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %}
{% input_with_errors forms.3.feb_naming_requirements_details %}
{% endwith %}
<p class="usa-hint">Maximum 2000 characters allowed.</p>
</div>
</fieldset>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -3,17 +3,84 @@
{% block form_instructions %} {% block form_instructions %}
<p>.Gov domains are intended for public use. Domains will not be given to organizations that only want to reserve a domain name (defensive registration) or that only intend to use the domain internally (as for an intranet).</p> <p>.Gov domains are intended for public use. Domains will not be given to organizations that only want to reserve a domain name (defensive registration) or that only intend to use the domain internally (as for an intranet).</p>
<p>Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p> <p>Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
<h2>What is the purpose of your requested domain?</h2>
<p>Describe how youll use your .gov domain. Will it be used for a website, email, or something else?</p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}
{# commented out so it does not appear on this page #} {# empty this block so it doesn't show on this page #}
{% endblock %} {% endblock %}
{% block form_fields %} {% block form_fields %}
{% if requires_feb_questions %}
<fieldset class="usa-fieldset margin-top-0 dotgov-domain-form">
{{forms.0.management_form}}
{{forms.1.management_form}}
{{forms.2.management_form}}
{{forms.3.management_form}}
{{forms.4.management_form}}
{{forms.5.management_form}}
<h2>What is the purpose of your requested 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.0.feb_purpose_choice %}
{% endwith %}
<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" attr_maxlength="2000" %}
{% input_with_errors forms.1.purpose %}
{% endwith %}
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
</div>
<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="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" 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 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="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" attr_maxlength="2000" %}
{% input_with_errors forms.5.interagency_initiative_details %}
{% endwith %}
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
</div>
</fieldset>
{% else %}
<h2>What is the purpose of your requested domain?</h2>
<p>Describe how youll use your .gov domain. Will it be used for a website, email, or something else?</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.purpose %} {% input_with_errors forms.1.purpose %}
{% endwith %} {% endwith %}
{% endif %}
{% endblock %} {% endblock %}

View file

@ -1981,7 +1981,14 @@ class TestDomainRequestAdmin(MockEppLib):
"senior_official", "senior_official",
"approved_domain", "approved_domain",
"requested_domain", "requested_domain",
"feb_naming_requirements",
"feb_naming_requirements_details",
"feb_purpose_choice",
"purpose", "purpose",
"has_timeframe",
"time_frame_details",
"is_interagency_initiative",
"interagency_initiative_details",
"no_other_contacts_rationale", "no_other_contacts_rationale",
"anything_else", "anything_else",
"has_anything_else_text", "has_anything_else_text",

View file

@ -14,10 +14,11 @@ from registrar.forms.domain_request_wizard import (
OtherContactsForm, OtherContactsForm,
RequirementsForm, RequirementsForm,
TribalGovernmentForm, TribalGovernmentForm,
PurposeForm,
AnythingElseForm, AnythingElseForm,
AboutYourOrganizationForm, AboutYourOrganizationForm,
) )
from registrar.forms import PurposeDetailsForm
from registrar.forms.domain import ContactForm from registrar.forms.domain import ContactForm
from registrar.forms.portfolio import ( from registrar.forms.portfolio import (
PortfolioInvitedMemberForm, PortfolioInvitedMemberForm,
@ -257,7 +258,7 @@ class TestFormValidation(MockEppLib):
@less_console_noise_decorator @less_console_noise_decorator
def test_purpose_form_character_count_invalid(self): def test_purpose_form_character_count_invalid(self):
"""Response must be less than 2000 characters.""" """Response must be less than 2000 characters."""
form = PurposeForm( form = PurposeDetailsForm(
data={ data={
"purpose": "Bacon ipsum dolor amet fatback strip steak pastrami" "purpose": "Bacon ipsum dolor amet fatback strip steak pastrami"
"shankle, drumstick doner chicken landjaeger turkey andouille." "shankle, drumstick doner chicken landjaeger turkey andouille."

View file

@ -5,6 +5,7 @@ from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from api.tests.common import less_console_noise_decorator 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 .common import MockSESClient, completed_domain_request # type: ignore
from django_webtest import WebTest # type: ignore from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore import boto3_mocking # type: ignore
@ -53,6 +54,7 @@ class DomainRequestTests(TestWithUser, WebTest):
UserPortfolioPermission.objects.all().delete() UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete() Portfolio.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
FederalAgency.objects.all().delete()
@less_console_noise_decorator @less_console_noise_decorator
def test_domain_request_form_intro_acknowledgement(self): def test_domain_request_form_intro_acknowledgement(self):
@ -2546,6 +2548,128 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(dotgov_page, "CityofEudoraKS.gov") self.assertContains(dotgov_page, "CityofEudoraKS.gov")
self.assertNotContains(dotgov_page, "medicare.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 = "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
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 @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,10 +15,12 @@ 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 import feb
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
from registrar.models.user import User from registrar.models.user import User
from registrar.utility.waffle import flag_is_active_for_user
from registrar.views.utility import StepsHelper from registrar.views.utility import StepsHelper
from registrar.utility.enums import Step, PortfolioDomainRequestStep from registrar.utility.enums import Step, PortfolioDomainRequestStep
@ -180,6 +182,9 @@ class DomainRequestWizard(TemplateView):
"""Determines which step enum we should use for the wizard""" """Determines which step enum we should use for the wizard"""
return PortfolioDomainRequestStep if self.is_portfolio else Step return PortfolioDomainRequestStep if self.is_portfolio else Step
def requires_feb_questions(self) -> bool:
return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature")
@property @property
def prefix(self): def prefix(self):
"""Namespace the wizard to avoid clashes in session variable names.""" """Namespace the wizard to avoid clashes in session variable names."""
@ -652,18 +657,130 @@ class CurrentSites(DomainRequestWizard):
class DotgovDomain(DomainRequestWizard): class DotgovDomain(DomainRequestWizard):
template_name = "domain_request_dotgov_domain.html" template_name = "domain_request_dotgov_domain.html"
forms = [forms.DotGovDomainForm, forms.AlternativeDomainFormSet] forms = [
forms.DotGovDomainForm,
forms.AlternativeDomainFormSet,
feb.ExecutiveNamingRequirementsYesNoForm,
feb.ExecutiveNamingRequirementsDetailsForm,
]
def get_context_data(self): def get_context_data(self):
context = super().get_context_data() context = super().get_context_data()
context["generic_org_type"] = self.domain_request.generic_org_type context["generic_org_type"] = self.domain_request.generic_org_type
context["federal_type"] = self.domain_request.federal_type context["federal_type"] = self.domain_request.federal_type
context["requires_feb_questions"] = self.requires_feb_questions()
return context return context
def is_valid(self, forms_list: list) -> bool:
"""
Expected order of forms_list:
0: DotGovDomainForm
1: AlternativeDomainFormSet
2: ExecutiveNamingRequirementsYesNoForm
3: ExecutiveNamingRequirementsDetailsForm
"""
logger.debug("Validating dotgov domain form")
# If FEB questions aren't required, validate only non-FEB forms
if not self.requires_feb_questions():
forms_list[2].mark_form_for_deletion()
forms_list[3].mark_form_for_deletion()
return forms_list[0].is_valid() and forms_list[1].is_valid()
if not forms_list[2].is_valid():
logger.debug("Dotgov domain form is invalid")
# mark details form for deletion so that its errors don't show up
forms_list[3].mark_form_for_deletion()
return False
if forms_list[2].cleaned_data.get("feb_naming_requirements", None):
logger.debug("Marking details form for deletion")
# If the user selects "yes" or has made no selection, no details are needed.
forms_list[3].mark_form_for_deletion()
valid = all(form.is_valid() for i, form in enumerate(forms_list) if i != 3)
else:
# "No" was selected details are required.
valid = all(form.is_valid() for form in forms_list)
return valid
class Purpose(DomainRequestWizard): class Purpose(DomainRequestWizard):
template_name = "domain_request_purpose.html" template_name = "domain_request_purpose.html"
forms = [forms.PurposeForm]
forms = [
feb.FEBPurposeOptionsForm,
forms.PurposeDetailsForm,
feb.FEBTimeFrameYesNoForm,
feb.FEBTimeFrameDetailsForm,
feb.FEBInteragencyInitiativeYesNoForm,
feb.FEBInteragencyInitiativeDetailsForm,
]
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: list) -> bool:
"""
Expected order of forms_list:
0: FEBPurposeOptionsForm
1: PurposeDetailsForm
2: FEBTimeFrameYesNoForm
3: FEBTimeFrameDetailsForm
4: FEBInteragencyInitiativeYesNoForm
5: FEBInteragencyInitiativeDetailsForm
"""
feb_purpose_options_form = forms_list[0]
purpose_details_form = forms_list[1]
feb_timeframe_yes_no_form = forms_list[2]
feb_timeframe_details_form = forms_list[3]
feb_initiative_yes_no_form = forms_list[4]
feb_initiative_details_form = forms_list[5]
if not self.requires_feb_questions():
# if FEB questions don't apply, mark those forms for deletion
feb_purpose_options_form.mark_form_for_deletion()
feb_timeframe_yes_no_form.mark_form_for_deletion()
feb_timeframe_details_form.mark_form_for_deletion()
feb_initiative_yes_no_form.mark_form_for_deletion()
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 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()
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()
if not feb_initiative_valid or not feb_initiative_yes_no_form.cleaned_data.get("is_interagency_initiative"):
# Ensure details form doesn't throw errors if it's not showing
feb_initiative_details_form.mark_form_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): class OtherContacts(DomainRequestWizard):
@ -711,9 +828,7 @@ class OtherContacts(DomainRequestWizard):
class AdditionalDetails(DomainRequestWizard): class AdditionalDetails(DomainRequestWizard):
template_name = "domain_request_additional_details.html" template_name = "domain_request_additional_details.html"
forms = [ forms = [
forms.CisaRepresentativeYesNoForm, forms.CisaRepresentativeYesNoForm,
forms.CisaRepresentativeForm, forms.CisaRepresentativeForm,