Merge pull request #3700 from cisagov/cb/3212-subissues

#3212: FEB final part - [CB]
This commit is contained in:
Erin Song 2025-04-03 10:39:55 -07:00 committed by GitHub
commit 60ed2fd0a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 549 additions and 197 deletions

View file

@ -2741,6 +2741,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
portfolio_urbanization.short_description = "Urbanization" # type: ignore portfolio_urbanization.short_description = "Urbanization" # type: ignore
# ------ FEB fields ------
# This is just a placeholder. This field will be populated in the detail_table_fieldset view. # This is just a placeholder. This field will be populated in the detail_table_fieldset view.
# This is not a field that exists on the model. # This is not a field that exists on the model.
def status_history(self, obj): def status_history(self, obj):
@ -2821,7 +2823,16 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
] ]
}, },
), ),
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}), (
".gov domain",
{
"fields": [
"requested_domain",
"alternative_domains",
"feb_naming_requirements_details",
]
},
),
( (
"Contacts", "Contacts",
{ {
@ -2833,10 +2844,24 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
"cisa_representative_first_name", "cisa_representative_first_name",
"cisa_representative_last_name", "cisa_representative_last_name",
"cisa_representative_email", "cisa_representative_email",
"eop_stakeholder_first_name",
"eop_stakeholder_last_name",
]
},
),
(
"Background info",
{
"fields": [
"feb_purpose_choice",
"purpose",
"time_frame_details",
"interagency_initiative_details",
"anything_else",
"current_websites",
] ]
}, },
), ),
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
( (
"Type of organization", "Type of organization",
{ {
@ -3033,23 +3058,41 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
fieldsets = super().get_fieldsets(request, obj) fieldsets = super().get_fieldsets(request, obj)
excluded_fields = set()
feb_fields = [
"feb_naming_requirements_details",
"feb_purpose_choice",
"time_frame_details",
"interagency_initiative_details",
"eop_stakeholder_first_name",
"eop_stakeholder_last_name",
]
org_fields = [
"portfolio",
"sub_organization",
"requested_suborganization",
"suborganization_city",
"suborganization_state_territory",
]
org_flag = flag_is_active_for_user(request.user, "organization_requests")
# Hide FEB fields for non-FEB requests
if not (obj and obj.portfolio and obj.is_feb()):
excluded_fields.update(feb_fields)
# Hide certain portfolio and suborg fields behind the organization requests flag # Hide certain portfolio and suborg fields behind the organization requests flag
# if it is not enabled # if it is not enabled
if not flag_is_active_for_user(request.user, "organization_requests"): if not org_flag:
excluded_fields = [ excluded_fields.update(org_fields)
"portfolio", excluded_fields.update(feb_fields)
"sub_organization",
"requested_suborganization", modified_fieldsets = []
"suborganization_city", for name, data in fieldsets:
"suborganization_state_territory", fields = data.get("fields", [])
] fields = tuple(field for field in fields if field not in excluded_fields)
modified_fieldsets = [] modified_fieldsets.append((name, {**data, "fields": fields}))
for name, data in fieldsets: return modified_fieldsets
fields = data.get("fields", [])
fields = tuple(field for field in fields if field not in excluded_fields)
modified_fieldsets.append((name, {**data, "fields": fields}))
return modified_fieldsets
return fieldsets
# Trigger action when a fieldset is changed # Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):

View file

@ -1,41 +1,105 @@
import { showElement } from './helpers.js'; import { showElement } from './helpers.js';
// Flag to track if we're in the initial page load
let isInitialLoad = true;
export const domain_purpose_choice_callbacks = { export const domain_purpose_choice_callbacks = {
'new': { 'new': {
callback: function(value, element) { callback: function(value, element, event) {
// Only clear errors if this is a user-initiated event (not initial page load)
if (!isInitialLoad) {
clearErrors(element);
}
//show the purpose details container //show the purpose details container
showElement(element); showElement(element);
// change just the text inside the em tag // change just the text inside the em tag
const labelElement = element.querySelector('.usa-label em'); const labelElement = element.querySelector('p em');
labelElement.innerHTML = 'Explain why a new domain is required and why a ' + labelElement.innerHTML = 'Explain why a new domain name is needed instead of using a ' +
'subdomain of an existing domain doesn\'t meet your needs.' + 'subdomain of an existing website.' +
'<br><br>' + // Adding double line break for spacing '<br><br>' + // Adding double line break for spacing
'Include any data that supports a clear public benefit or ' + 'Include any information or data that shows how the new domain would ' +
'evidence user need for this new domain. ' + 'benefit the public or meet user needs. ' +
'<span class="usa-label--required">*</span>'; '<span class="usa-label--required">*</span>';
// Mark that we're no longer in initial load
isInitialLoad = false;
}, },
element: document.getElementById('purpose-details-container') element: document.getElementById('purpose-details-container')
}, },
'redirect': { 'redirect': {
callback: function(value, element) { callback: function(value, element, event) {
// Only clear errors if this is a user-initiated event (not initial page load)
if (!isInitialLoad) {
clearErrors(element);
}
// show the purpose details container // show the purpose details container
showElement(element); showElement(element);
// change just the text inside the em tag // change just the text inside the em tag
const labelElement = element.querySelector('.usa-label em'); const labelElement = element.querySelector('p em');
labelElement.innerHTML = 'Explain why a redirect is necessary. ' + labelElement.innerHTML = 'Explain why a redirect is necessary. ' +
'<span class="usa-label--required">*</span>'; '<span class="usa-label--required">*</span>';
// Mark that we're no longer in initial load
isInitialLoad = false;
}, },
element: document.getElementById('purpose-details-container') element: document.getElementById('purpose-details-container')
}, },
'other': { 'other': {
callback: function(value, element) { callback: function(value, element, event) {
// Only clear errors if this is a user-initiated event (not initial page load)
if (!isInitialLoad) {
clearErrors(element);
}
// Show the purpose details container // Show the purpose details container
showElement(element); showElement(element);
// change just the text inside the em tag // change just the text inside the em tag
const labelElement = element.querySelector('.usa-label em'); const labelElement = element.querySelector('p em');
labelElement.innerHTML = 'Describe how this domain will be used. ' + labelElement.innerHTML = 'Describe how this domain will be used. ' +
'<span class="usa-label--required">*</span>'; '<span class="usa-label--required">*</span>';
// Mark that we're no longer in initial load
isInitialLoad = false;
}, },
element: document.getElementById('purpose-details-container') element: document.getElementById('purpose-details-container')
} }
}
// Helper function to clear error messages in a textarea
function clearErrors(element) {
// Find the error message div
const errorMessage = element.querySelector('#id_purpose-purpose__error-message');
if (errorMessage) {
errorMessage.remove();
}
// Find the form group and remove error class
const formGroup = element.querySelector('.usa-form-group');
if (formGroup) {
formGroup.classList.remove('usa-form-group--error');
}
// Find the textarea and remove error class
const textarea = element.querySelector('#id_purpose-purpose');
if (textarea) {
textarea.classList.remove('usa-input--error');
// Also update aria attributes
textarea.setAttribute('aria-invalid', 'false');
// Remove error message from aria-describedby
const describedBy = textarea.getAttribute('aria-describedby');
if (describedBy) {
const newDescribedBy = describedBy.replace('id_purpose-purpose__error-message', '').trim();
textarea.setAttribute('aria-describedby', newDescribedBy);
}
}
// Find the label and remove error class
const label = element.querySelector('label');
if (label) {
label.classList.remove('usa-label--error');
}
} }

View file

@ -22,6 +22,13 @@ class UserFixture:
""" """
ADMINS = [ ADMINS = [
{
"username": "4aa78480-6272-42f9-ac29-a034ebdd9231",
"first_name": "Kaitlin",
"last_name": "Abbitt",
"email": "kaitlin.abbitt@cisa.dhs.gov",
"title": "Captain pirate",
},
{ {
"username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf", "username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf",
"first_name": "Aditi", "first_name": "Aditi",

View file

@ -1,6 +1,7 @@
from django import forms from django import forms
from django.core.validators import MaxLengthValidator from django.core.validators import MaxLengthValidator
from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm
from registrar.models.domain_request import DomainRequest
class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm): class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm):
@ -11,6 +12,8 @@ class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm, BaseDeletableRegistrar
field_name = "feb_naming_requirements" field_name = "feb_naming_requirements"
required_error_message = "Select “Yes” if your submission meets each domain naming requirement. Select “No” if it doesnt meet each requirement." # noqa: E501
@property @property
def form_is_checked(self): def form_is_checked(self):
""" """
@ -25,7 +28,9 @@ class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
widget=forms.Textarea(attrs={"maxlength": "2000"}), widget=forms.Textarea(attrs={"maxlength": "2000"}),
max_length=2000, max_length=2000,
required=True, required=True,
error_messages={"required": ("This field is required.")}, error_messages={
"required": ("Provide details on why your submission does not meet each domain naming requirement.")
}, # noqa: E501
validators=[ validators=[
MaxLengthValidator( MaxLengthValidator(
2000, 2000,
@ -41,18 +46,14 @@ class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
field_name = "feb_purpose_choice" field_name = "feb_purpose_choice"
form_choices = ( form_choices = DomainRequest.FEBPurposeChoices.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( feb_purpose_choice = forms.ChoiceField(
required=True, required=True,
choices=form_choices, choices=form_choices,
widget=forms.RadioSelect, widget=forms.RadioSelect,
error_messages={ error_messages={
"required": "This question is required.", "required": "Select the purpose of your requested domain.",
}, },
label="Select one", label="Select one",
) )
@ -65,6 +66,10 @@ class FEBTimeFrameYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
""" """
field_name = "has_timeframe" field_name = "has_timeframe"
required_error_message = (
"Select “Yes” if you have a target time frame for"
" launching this domain. Select “No” if you dont have a target time frame."
)
@property @property
def form_is_checked(self): def form_is_checked(self):
@ -79,7 +84,7 @@ class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
label="time_frame_details", label="time_frame_details",
widget=forms.Textarea( widget=forms.Textarea(
attrs={ attrs={
"aria-label": "Provide details on your target timeframe. \ "aria-label": "Provide details on your target time frame. \
Is there a special significance to this date (legal requirement, announcement, event, etc)?" Is there a special significance to this date (legal requirement, announcement, event, etc)?"
} }
), ),
@ -89,7 +94,7 @@ class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
message="Response must be less than 2000 characters.", message="Response must be less than 2000 characters.",
) )
], ],
error_messages={"required": "Provide details on your target timeframe."}, error_messages={"required": "Provide details on your target time frame."},
) )
@ -100,6 +105,10 @@ class FEBInteragencyInitiativeYesNoForm(BaseDeletableRegistrarForm, BaseYesNoFor
""" """
field_name = "is_interagency_initiative" field_name = "is_interagency_initiative"
required_error_message = (
"Select “Yes” if the domain will be used for an "
"interagency initiative. Select “No” if it wont be used for an interagency initiative."
)
@property @property
def form_is_checked(self): def form_is_checked(self):
@ -156,29 +165,12 @@ class EOPContactForm(BaseDeletableRegistrarForm):
error_messages={"required": "Enter the last name / family name of this contact."}, error_messages={"required": "Enter the last name / family name of this contact."},
required=True, 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 @classmethod
def from_database(cls, obj): def from_database(cls, obj):
return { return {
"first_name": obj.eop_stakeholder_first_name, "first_name": obj.eop_stakeholder_first_name,
"last_name": obj.eop_stakeholder_last_name, "last_name": obj.eop_stakeholder_last_name,
"email": obj.eop_stakeholder_email,
} }
def to_database(self, obj): def to_database(self, obj):
@ -192,7 +184,6 @@ class EOPContactForm(BaseDeletableRegistrarForm):
return return
obj.eop_stakeholder_first_name = self.cleaned_data["first_name"] obj.eop_stakeholder_first_name = self.cleaned_data["first_name"]
obj.eop_stakeholder_last_name = self.cleaned_data["last_name"] obj.eop_stakeholder_last_name = self.cleaned_data["last_name"]
obj.eop_stakeholder_email = self.cleaned_data["email"]
obj.save() obj.save()

View file

@ -234,9 +234,12 @@ class BaseYesNoForm(RegistrarForm):
# For instance, this could be "has_other_contacts" # For instance, this could be "has_other_contacts"
field_name: str field_name: str
# This field can be overriden to show a custom error
# message.
required_error_message = "This question is required." required_error_message = "This question is required."
# Default form choice mapping. Default is suitable for most cases. # Default form choice mapping. Default is suitable for most cases.
# Override for more complex scenarios.
form_choices = ((True, "Yes"), (False, "No")) form_choices = ((True, "Yes"), (False, "No"))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View file

@ -13,17 +13,17 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name="domainrequest", model_name="domainrequest",
name="eop_stakeholder_email", name="eop_stakeholder_email",
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name="EOP Stakeholder Email"), field=models.EmailField(blank=True, max_length=254, null=True, verbose_name="EOP contact email"),
), ),
migrations.AddField( migrations.AddField(
model_name="domainrequest", model_name="domainrequest",
name="eop_stakeholder_first_name", name="eop_stakeholder_first_name",
field=models.CharField(blank=True, null=True, verbose_name="EOP Stakeholder First Name"), field=models.CharField(blank=True, null=True, verbose_name="EOP contact first name"),
), ),
migrations.AddField( migrations.AddField(
model_name="domainrequest", model_name="domainrequest",
name="eop_stakeholder_last_name", name="eop_stakeholder_last_name",
field=models.CharField(blank=True, null=True, verbose_name="EOP Stakeholder Last Name"), field=models.CharField(blank=True, null=True, verbose_name="EOP contact last name"),
), ),
migrations.AddField( migrations.AddField(
model_name="domainrequest", model_name="domainrequest",

View file

@ -0,0 +1,56 @@
# Generated by Django 4.2.20 on 2025-03-26 19:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0145_create_groups_v19"),
]
operations = [
migrations.RemoveField(
model_name="domainrequest",
name="eop_stakeholder_email",
),
migrations.AlterField(
model_name="domainrequest",
name="feb_naming_requirements",
field=models.BooleanField(blank=True, null=True, verbose_name="Meets naming requirements"),
),
migrations.AlterField(
model_name="domainrequest",
name="feb_naming_requirements_details",
field=models.TextField(
blank=True,
help_text="Required if requested domain that doesn't meet naming requirements",
null=True,
verbose_name="Domain name rationale",
),
),
migrations.AlterField(
model_name="domainrequest",
name="feb_purpose_choice",
field=models.CharField(
blank=True,
choices=[
("new", "Used for a new website"),
("redirect", "Used as a redirect for an existing website"),
("other", "Not for a website"),
],
null=True,
verbose_name="Purpose type",
),
),
migrations.AlterField(
model_name="domainrequest",
name="interagency_initiative_details",
field=models.TextField(blank=True, null=True, verbose_name="Interagency initiative"),
),
migrations.AlterField(
model_name="domainrequest",
name="time_frame_details",
field=models.TextField(blank=True, null=True, verbose_name="Target time frame"),
),
]

View file

@ -55,9 +55,14 @@ class DomainRequest(TimeStampedModel):
return cls(status_name).label if status_name else None return cls(status_name).label if status_name else None
class FEBPurposeChoices(models.TextChoices): class FEBPurposeChoices(models.TextChoices):
WEBSITE = "website" WEBSITE = "new", "Used for a new website"
REDIRECT = "redirect" REDIRECT = "redirect", "Used as a redirect for an existing website"
OTHER = "other" OTHER = "other", "Not for a website"
@classmethod
def get_purpose_label(cls, purpose_name: str | None):
"""Returns the associated label for a given purpose name"""
return cls(purpose_name).label if purpose_name else None
class StateTerritoryChoices(models.TextChoices): class StateTerritoryChoices(models.TextChoices):
ALABAMA = "AL", "Alabama (AL)" ALABAMA = "AL", "Alabama (AL)"
@ -510,17 +515,21 @@ class DomainRequest(TimeStampedModel):
feb_naming_requirements = models.BooleanField( feb_naming_requirements = models.BooleanField(
null=True, null=True,
blank=True, blank=True,
verbose_name="Meets naming requirements",
) )
feb_naming_requirements_details = models.TextField( feb_naming_requirements_details = models.TextField(
null=True, null=True,
blank=True, blank=True,
help_text="Required if requested domain that doesn't meet naming requirements",
verbose_name="Domain name rationale",
) )
feb_purpose_choice = models.CharField( feb_purpose_choice = models.CharField(
null=True, null=True,
blank=True, blank=True,
choices=FEBPurposeChoices.choices, choices=FEBPurposeChoices.choices,
verbose_name="Purpose type",
) )
working_with_eop = models.BooleanField( working_with_eop = models.BooleanField(
@ -531,24 +540,17 @@ class DomainRequest(TimeStampedModel):
eop_stakeholder_first_name = models.CharField( eop_stakeholder_first_name = models.CharField(
null=True, null=True,
blank=True, blank=True,
verbose_name="EOP Stakeholder First Name", verbose_name="EOP contact first name",
) )
eop_stakeholder_last_name = models.CharField( eop_stakeholder_last_name = models.CharField(
null=True, null=True,
blank=True, blank=True,
verbose_name="EOP Stakeholder Last Name", verbose_name="EOP contact last name",
)
eop_stakeholder_email = models.EmailField(
null=True,
blank=True,
verbose_name="EOP Stakeholder Email",
) )
# 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.
purpose = models.TextField( purpose = models.TextField(
null=True, null=True,
blank=True, blank=True,
@ -562,6 +564,7 @@ class DomainRequest(TimeStampedModel):
time_frame_details = models.TextField( time_frame_details = models.TextField(
null=True, null=True,
blank=True, blank=True,
verbose_name="Target time frame",
) )
is_interagency_initiative = models.BooleanField( is_interagency_initiative = models.BooleanField(
@ -572,6 +575,7 @@ class DomainRequest(TimeStampedModel):
interagency_initiative_details = models.TextField( interagency_initiative_details = models.TextField(
null=True, null=True,
blank=True, blank=True,
verbose_name="Interagency initiative",
) )
alternative_domains = models.ManyToManyField( alternative_domains = models.ManyToManyField(
@ -1015,11 +1019,15 @@ class DomainRequest(TimeStampedModel):
if not context: if not context:
has_organization_feature_flag = flag_is_active_for_user(recipient, "organization_feature") has_organization_feature_flag = flag_is_active_for_user(recipient, "organization_feature")
is_org_user = has_organization_feature_flag and recipient.has_view_portfolio_permission(self.portfolio) is_org_user = has_organization_feature_flag and recipient.has_view_portfolio_permission(self.portfolio)
requires_feb_questions = self.is_feb() and is_org_user
purpose_label = DomainRequest.FEBPurposeChoices.get_purpose_label(self.feb_purpose_choice)
context = { context = {
"domain_request": self, "domain_request": self,
# This is the user that we refer to in the email # This is the user that we refer to in the email
"recipient": recipient, "recipient": recipient,
"is_org_user": is_org_user, "is_org_user": is_org_user,
"requires_feb_questions": requires_feb_questions,
"purpose_label": purpose_label,
} }
if custom_email_content: if custom_email_content:

View file

@ -2,19 +2,25 @@
{% 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="{% 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: <p>
Before requesting a .gov domain, please make sure it meets
{% if requires_feb_questions %}
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="https://get.gov/domains/executive-branch-guidance/">our naming requirements for executive branch agencies</a>. Your domain name must:
{% else %}
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing' %}">our naming requirements</a>. Your domain name must:
{% endif %}
<ul class="usa-list"> <ul class="usa-list">
<li>Be available </li> <li>Be available </li>
<li>Relate to your organization's name, location, and/or services </li> <li>Relate to your organizations 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 state's two-letter abbreviation.{% endif %}</p> {% if not is_federal %}In most instances, this requires including your states two-letter abbreviation.{% endif %}</p>
{% if not portfolio %} {% if not portfolio %}
<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> <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>
{% 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
@ -29,7 +35,7 @@
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}
{# empty this block so it doesn't show on this page #} {# empty this block so it doesnt show on this page #}
{% endblock %} {% endblock %}
@ -42,8 +48,8 @@
<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, we'll make sure it's available and that it meets some of our naming requirements. 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, we'll verify that it meets all our requirements after you complete the rest of this form. If your domain passes these initial checks, well verify that it meets all our requirements after you complete the rest of this form.
</p> </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 #}
@ -65,7 +71,7 @@
<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 you'd like if we can't give you your first choice? Are there other domains youd like if we cant give 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 #}
@ -99,7 +105,7 @@
>Check availability</button> >Check availability</button>
</div> </div>
<p class="margin-top-05"> <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. If youre not sure this is the domain you want, thats ok. You can change the domain later.
</p> </p>
</fieldset> </fieldset>
@ -114,9 +120,10 @@
<p id="dotgov-domain-naming-requirements" class="margin-top-05"> <p id="dotgov-domain-naming-requirements" class="margin-top-05">
OMB will review each request against the domain 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/"> <a class="usa-link" rel="noopener noreferrer" target="_blank" href="https://get.gov/domains/executive-branch-guidance/">
naming requirements for executive branch agencies naming requirements for executive branch agencies.</a> Agency submissions are expected to meet each requirement.
</a>. </p>
Agency submissions are expected to meet each requirement. <p class="usa-label margin-top-0 margin-bottom-0">
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p> </p>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.2.feb_naming_requirements %} {% input_with_errors forms.2.feb_naming_requirements %}
@ -124,13 +131,12 @@
{# Conditional Details Field only shown when the executive naming requirements radio is "False" #} {# 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;"> <div id="domain-naming-requirements-details-container" class="conditional-panel" style="display: none;">
<p class="usa-label"> <p class="margin-bottom-3 margin-top-3">
Provide details below <span class="usa-label--required">*</span> <em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p> </p>
{% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %} {% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %}
{% input_with_errors forms.3.feb_naming_requirements_details %} {% input_with_errors forms.3.feb_naming_requirements_details %}
{% endwith %} {% endwith %}
<p class="usa-hint">Maximum 2000 characters allowed.</p>
</div> </div>
</fieldset> </fieldset>
{% endif %} {% endif %}

View file

@ -12,14 +12,12 @@
{% block form_fields %} {% block form_fields %}
{% if requires_feb_questions %} {% if requires_feb_questions %}
<fieldset class="usa-fieldset margin-top-0 dotgov-domain-form"> <fieldset class="usa-fieldset margin-top-0">
{{forms.0.management_form}} {{forms.0.management_form}}
{{forms.1.management_form}} {{forms.1.management_form}}
{{forms.2.management_form}} <legend>
{{forms.3.management_form}} <h2>What is the purpose of your requested domain?</h2>
{{forms.4.management_form}} </legend>
{{forms.5.management_form}}
<h2>What is the purpose of your requested domain?</h2>
<p class="margin-bottom-0 margin-top-1"> <p class="margin-bottom-0 margin-top-1">
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em> <em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p> </p>
@ -28,51 +26,58 @@
{% endwith %} {% endwith %}
<div id="purpose-details-container" class="conditional-panel display-none"> <div id="purpose-details-container" class="conditional-panel display-none">
<p class="usa-label"> <p class="margin-bottom-3 margin-top-3">
<em>Provide details below <span class="usa-label--required">*</span></em> <em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p> </p>
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %} {% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.1.purpose %} {% input_with_errors forms.1.purpose %}
{% endwith %} {% endwith %}
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
</div> </div>
</fieldset>
<h2>Do you have a target time frame for launching this domain?</h2> <fieldset class="usa-fieldset margin-top-2">
<p class="margin-bottom-0 margin-top-1"> {{forms.2.management_form}}
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em> {{forms.3.management_form}}
</p> <legend>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} <h2>Do you have a target time frame for launching this domain?</h2>
{% input_with_errors forms.2.has_timeframe %} </legend>
{% endwith %}
<div id="purpose-timeframe-details-container" class="conditional-panel">
<p class="margin-bottom-0 margin-top-1"> <p class="margin-bottom-0 margin-top-1">
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em> <em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p> </p>
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.3.time_frame_details %} {% input_with_errors forms.2.has_timeframe %}
{% endwith %} {% 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> <div id="purpose-timeframe-details-container" class="conditional-panel">
<p class="margin-bottom-0 margin-top-1"> <p class="margin-bottom-3 margin-top-3">
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em> <em>Provide details on your target time frame. Is there special significance of this day (legal requirement, announcement, event, etc.)? <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p> </p>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.4.is_interagency_initiative %} {% input_with_errors forms.3.time_frame_details %}
{% endwith %} {% endwith %}
</div>
<div id="purpose-interagency-initaitive-details-container" class="conditional-panel"> </fieldset>
<fieldset class="usa-fieldset margin-top-2" aria-labelledby="interagency-initiative-question">
{{forms.4.management_form}}
{{forms.5.management_form}}
<legend>
<h2 id="interagency-initiative-question">Will the domain name be used for an interagency initiative?</h2>
</legend>
<p class="margin-bottom-0 margin-top-1"> <p class="margin-bottom-0 margin-top-1">
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em> <em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p> </p>
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.5.interagency_initiative_details %} {% input_with_errors forms.4.is_interagency_initiative %}
{% endwith %} {% endwith %}
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
</div> <div id="purpose-interagency-initaitive-details-container" class="conditional-panel">
</fieldset> <p class="margin-bottom-3 margin-top-3">
<em>Name the agencies that will be involved in this initiative. <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 %}
</div>
</fieldset>
{% else %} {% else %}
<h2>What is the purpose of your requested domain?</h2> <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> <p>Describe how youll use your .gov domain. Will it be used for a website, email, or something else?</p>

View file

@ -8,12 +8,47 @@ Current websites: {% for site in domain_request.current_websites.all %}
{% endfor %}{% endif %} {% endfor %}{% endif %}
.gov domain: .gov domain:
{{ domain_request.requested_domain.name }} {{ domain_request.requested_domain.name }}
{% if requires_feb_questions %}
Meets naming requirements
{% if domain_request.feb_naming_requirements %}
{{ domain_request.feb_naming_requirements }}
{% else %}
No
{{ domain_request.feb_naming_requirements_details }}
{% endif %}
{% endif %}
{% if domain_request.alternative_domains.all %} {% if domain_request.alternative_domains.all %}
Alternative domains: Alternative domains:
{% for site in domain_request.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %} {% for site in domain_request.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
{% endfor %}{% endif %} {% endfor %}{% endif %}
Purpose of your domain: Purpose of your domain:
{% if requires_feb_questions %}
{{ purpose_label }}
{{ domain_request.purpose }} {{ domain_request.purpose }}
Target time frame:
{% if domain_request.has_target_time_frame %}
{{ domain_request.time_frame_details }}
{% else %}
No
{% endif %}
Interagency initiative:
{% if domain_request.is_interagency_initiative %}
{{ domain_request.interagency_initiative_details }}
{% else %}
No
{% endif %}
{% else %}
{{ domain_request.purpose }}
{% endif %}
{% if requires_feb_questions %}
EOP contact:
{% if domain_request.working_with_eop %}
{{ domain_request.eop_contact.first_name }} {{ domain_request.eop_contact.last_name }}
{{ domain_request.eop_contact.email }}
{% else %}
No
{% endif %}
{% endif %}
{% if domain_request.anything_else %} {% if domain_request.anything_else %}
Additional details: Additional details:
{{ domain_request.anything_else }} {{ domain_request.anything_else }}

View file

@ -10,19 +10,19 @@
{% if step == Step.REQUESTING_ENTITY %} {% if step == Step.REQUESTING_ENTITY %}
{% with title=form_titles|get_item:step %} {% with title=form_titles|get_item:step %}
{% if domain_request.sub_organization %} {% if domain_request.sub_organization %}
{% include "includes/summary_item.html" with value=domain_request.sub_organization edit_link=domain_request_url %} {% include "includes/summary_item.html" with title=title value=domain_request.sub_organization editable=is_editable edit_link=domain_request_url %}
{% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %} {% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %}
{% elif domain_request.requesting_entity_is_suborganization %} {% elif domain_request.requesting_entity_is_suborganization %}
{% include "includes/summary_item.html" with value=domain_request.requested_suborganization edit_link=domain_request_url %} {% include "includes/summary_item.html" with title=title value=domain_request.requested_suborganization editable=is_editable edit_link=domain_request_url %}
<p class="margin-y-0">{{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}</p> <p class="margin-y-0">{{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}</p>
{% elif domain_request.requesting_entity_is_portfolio %} {% elif domain_request.requesting_entity_is_portfolio %}
{% include "includes/summary_item.html" with value=domain_request.portfolio.organization_name edit_link=domain_request_url %} {% include "includes/summary_item.html" with title=title value=domain_request.portfolio.organization_name editable=is_editable edit_link=domain_request_url %}
{% if domain_request.portfolio.city and domain_request.portfolio.state_territory %} {% if domain_request.portfolio.city and domain_request.portfolio.state_territory %}
<p class="margin-y-0">{{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}}</p> <p class="margin-y-0">{{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}}</p>
{% endif %} {% endif %}
{% else %} {% else %}
{% with value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %} {% with value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
{% include "includes/summary_item.html" with edit_link=domain_request_url %} {% include "includes/summary_item.html" with title=title editable=is_editable edit_link=domain_request_url %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
@ -53,18 +53,78 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if requires_feb_questions %}
<h4 class="margin-bottom-0">Meets naming requirements</h4>
{% if domain_request.feb_naming_requirements is None %}
<p class="margin-y-0"><span class='text-bold text-secondary-dark'>Incomplete</span></p>
{% elif domain_request.feb_naming_requirements %}
<p class="margin-y-0">Yes</p>
{% else %}
<p class="margin-y-0">No</p>
<p class="margin-y-0">{{domain_request.feb_naming_requirements_details}}</p>
{% endif %}
{% endif %}
{% endif %} {% endif %}
{% if step == Step.PURPOSE %} {% if step == Step.PURPOSE %}
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %} {% if requires_feb_questions %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} {% with title=form_titles|get_item:step %}
{% endwith %} {% include "includes/summary_item.html" with title=title value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
{% endwith %}
<h4 class="margin-bottom-0">Purpose</h4>
{% if domain_request.feb_purpose_choice %}
<p class="margin-y-0">{{purpose_label}}</p>
<p class="margin-y-0">{{domain_request.purpose}}</p>
{% else %}
<p class="margin-y-0"><span class='text-bold text-secondary-dark'>Incomplete</span></p>
{% endif %}
<h4 class="margin-bottom-0">Target time frame</h4>
{% if domain_request.has_timeframe is None %}
<p class="margin-y-0"><span class='text-bold text-secondary-dark'>Incomplete</span></p>
{% elif domain_request.has_timeframe %}
<p class="margin-y-0">{{domain_request.time_frame_details}}</p>
{% else %}
<p class="margin-y-0">No</p>
{% endif %}
<h4 class="margin-bottom-0">Interagency initiative</h4>
{% if domain_request.is_interagency_initiative is None %}
<p class="margin-y-0"><span class='text-bold text-secondary-dark'>Incomplete</span></p>
{% elif domain_request.is_interagency_initiative %}
<p class="margin-y-0">{{domain_request.interagency_initiative_details}}</p>
{% else %}
<p class="margin-y-0">No</p>
{% endif %}
{% else %}
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
{% endwith %}
{% endif %}
{% endif %} {% endif %}
{% if step == Step.ADDITIONAL_DETAILS %} {% if step == Step.ADDITIONAL_DETAILS %}
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"None" %} {% if requires_feb_questions %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} {% with title=form_titles|get_item:step %}
{% endwith %} {% include "includes/summary_item.html" with title=title value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
{% endwith %}
<h4 class="margin-bottom-0">EOP contact</h4>
{% if domain_request.working_with_eop is None %}
<p class="margin-y-0"><span class='text-bold text-secondary-dark'>Incomplete</span></p>
{% elif domain_request.working_with_eop %}
<p class="margin-y-0">{{domain_request.eop_stakeholder_first_name}} {{domain_request.eop_stakeholder_last_name}}</p>
{% else %}
<p class="margin-y-0">No</p>
{% endif %}
<h4 class="margin-bottom-0">Anything else</h4>
{% if domain_request.anything_else %}
<p class="margin-y-0">{{domain_request.anything_else}}</p>
{% else %}
<p class="margin-y-0">None</p>
{% endif %}
{% else %}
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"None" %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
{% endwith %}
{% endif %}
{% endif %} {% endif %}
{% if step == Step.REQUIREMENTS %} {% if step == Step.REQUIREMENTS %}

View file

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

View file

@ -23,8 +23,10 @@ def input_with_errors(context, field=None): # noqa: C901
add_required_class: like `add_class` but only if field is required add_required_class: like `add_class` but only if field is required
add_label_class: append to input element's label's `class` attribute add_label_class: append to input element's label's `class` attribute
add_legend_class: append to input element's legend's `class` attribute add_legend_class: append to input element's legend's `class` attribute
add_legend_heading: sets the text for the legend associated with this element
add_group_class: append to input element's surrounding tag's `class` attribute add_group_class: append to input element's surrounding tag's `class` attribute
add_aria_label: append to input element's `aria_label` attribute add_aria_label: append to input element's `aria_label` attribute
add_aria_describedby: appends to input element's `aria-describedby` attribute
attr_* - adds or replaces any single html attribute for the input attr_* - adds or replaces any single html attribute for the input
add_error_attr_* - like `attr_*` but only if field.errors is not empty add_error_attr_* - like `attr_*` but only if field.errors is not empty
toggleable_input: shows a simple edit button, and adds display-none to the input field. toggleable_input: shows a simple edit button, and adds display-none to the input field.
@ -106,6 +108,8 @@ def input_with_errors(context, field=None): # noqa: C901
elif key == "add_aria_label": elif key == "add_aria_label":
aria_labels.append(value) aria_labels.append(value)
elif key == "add_aria_describedby":
described_by.append(value)
elif key == "sublabel_text": elif key == "sublabel_text":
sublabel_text.append(value) sublabel_text.append(value)

View file

@ -2064,7 +2064,6 @@ class TestDomainRequestAdmin(MockEppLib):
"working_with_eop", "working_with_eop",
"eop_stakeholder_first_name", "eop_stakeholder_first_name",
"eop_stakeholder_last_name", "eop_stakeholder_last_name",
"eop_stakeholder_email",
"purpose", "purpose",
"has_timeframe", "has_timeframe",
"time_frame_details", "time_frame_details",

View file

@ -2615,8 +2615,8 @@ class DomainRequestTests(TestWithUser, WebTest):
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"] = "False"
domain_form["dotgov_domain-feb_naming_requirements_details"] = "test" domain_form["dotgov_domain-feb_naming_requirements_details"] = "Because this is a test"
with patch( with patch(
"registrar.forms.domain_request_wizard.DotGovDomainForm.clean_requested_domain", return_value=domain "registrar.forms.domain_request_wizard.DotGovDomainForm.clean_requested_domain", return_value=domain
): # noqa ): # noqa
@ -2631,11 +2631,11 @@ class DomainRequestTests(TestWithUser, WebTest):
purpose_form = purpose_page.forms[0] purpose_form = purpose_page.forms[0]
purpose_form["purpose-feb_purpose_choice"] = "redirect" purpose_form["purpose-feb_purpose_choice"] = "redirect"
purpose_form["purpose-purpose"] = "test" purpose_form["purpose-purpose"] = "testPurpose123"
purpose_form["purpose-has_timeframe"] = "True" purpose_form["purpose-has_timeframe"] = "True"
purpose_form["purpose-time_frame_details"] = "test" purpose_form["purpose-time_frame_details"] = "1/2/2025 - 1/2/2026"
purpose_form["purpose-is_interagency_initiative"] = "True" purpose_form["purpose-is_interagency_initiative"] = "True"
purpose_form["purpose-interagency_initiative_details"] = "test" purpose_form["purpose-interagency_initiative_details"] = "FakeInteragencyInitiative"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_result = purpose_form.submit() purpose_result = purpose_form.submit()
@ -2646,9 +2646,8 @@ class DomainRequestTests(TestWithUser, WebTest):
additional_details_form = additional_details_page.forms[0] additional_details_form = additional_details_page.forms[0]
additional_details_form["portfolio_additional_details-working_with_eop"] = "True" additional_details_form["portfolio_additional_details-working_with_eop"] = "True"
additional_details_form["portfolio_additional_details-first_name"] = "Testy" additional_details_form["portfolio_additional_details-first_name"] = "TesterFirstName"
additional_details_form["portfolio_additional_details-last_name"] = "Tester" additional_details_form["portfolio_additional_details-last_name"] = "TesterLastName"
additional_details_form["portfolio_additional_details-email"] = "testy@town.com"
additional_details_form["portfolio_additional_details-has_anything_else_text"] = "True" additional_details_form["portfolio_additional_details-has_anything_else_text"] = "True"
additional_details_form["portfolio_additional_details-anything_else"] = "test" additional_details_form["portfolio_additional_details-anything_else"] = "test"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -2659,6 +2658,16 @@ class DomainRequestTests(TestWithUser, WebTest):
requirements_page = additional_details_result.follow() requirements_page = additional_details_result.follow()
self.feb_requirements_page_tests(requirements_page) self.feb_requirements_page_tests(requirements_page)
requirements_form = requirements_page.forms[0]
requirements_form["requirements-is_policy_acknowledged"] = "True"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
requirements_result = requirements_form.submit()
# ---- REVIEW PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
review_page = requirements_result.follow()
self.feb_review_page_tests(review_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?")
@ -2710,7 +2719,6 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(additional_details_page, "eop-contact-container") self.assertContains(additional_details_page, "eop-contact-container")
self.assertContains(additional_details_page, "additional_details-first_name") 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-last_name")
self.assertContains(additional_details_page, "additional_details-email")
# Make sure the additional details form is present # 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-has_anything_else_text")
@ -2733,6 +2741,25 @@ class DomainRequestTests(TestWithUser, WebTest):
"I read and understand the guidance outlined in the DOTGOV Act for operating a .gov domain.", "I read and understand the guidance outlined in the DOTGOV Act for operating a .gov domain.",
) )
def feb_review_page_tests(self, review_page):
# Meets naming requirements
self.assertContains(review_page, "Meets naming requirements")
self.assertContains(review_page, "No")
self.assertContains(review_page, "Because this is a test")
# Purpose
self.assertContains(review_page, "Purpose")
self.assertContains(review_page, "Used as a redirect for an existing website")
self.assertContains(review_page, "testPurpose123")
# Target time frame
self.assertContains(review_page, "Target time frame")
self.assertContains(review_page, "1/2/2025 - 1/2/2026")
# Interagency initiative
self.assertContains(review_page, "Interagency initiative")
self.assertContains(review_page, "FakeInteragencyInitiative")
# EOP contact
self.assertContains(review_page, "EOP contact")
self.assertContains(review_page, "TesterFirstName TesterLastName")
@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

@ -1841,6 +1841,18 @@ class DomainRequestExport(BaseExport):
details = [cisa_rep, model.get("anything_else")] details = [cisa_rep, model.get("anything_else")]
additional_details = " | ".join([field for field in details if field]) additional_details = " | ".join([field for field in details if field])
# FEB fields
purpose_type = model.get("feb_purpose_choice")
purpose_type_display = (
DomainRequest.FEBPurposeChoices.get_purpose_label(purpose_type) if purpose_type else "N/A"
)
eop_stakeholder_first_name = model.get("eop_stakeholder_first_name")
eop_stakeholder_last_name = model.get("eop_stakeholder_last_name")
if not eop_stakeholder_first_name or not eop_stakeholder_last_name:
eop_stakeholder_name = None
else:
eop_stakeholder_name = f"{eop_stakeholder_first_name} {eop_stakeholder_last_name}"
# create a dictionary of fields which can be included in output. # create a dictionary of fields which can be included in output.
# "extra_fields" are precomputed fields (generated in the DB or parsed). # "extra_fields" are precomputed fields (generated in the DB or parsed).
FIELDS = { FIELDS = {
@ -1882,6 +1894,12 @@ class DomainRequestExport(BaseExport):
"Last submitted date": model.get("last_submitted_date"), "Last submitted date": model.get("last_submitted_date"),
"First submitted date": model.get("first_submitted_date"), "First submitted date": model.get("first_submitted_date"),
"Last status update": model.get("last_status_update"), "Last status update": model.get("last_status_update"),
# FEB only fields
"Purpose": purpose_type_display,
"Domain name rationale": model.get("feb_naming_requirements_details", None),
"Target time frame": model.get("time_frame_details", None),
"Interagency initiative": model.get("interagency_initiative_details", None),
"EOP stakeholder name": eop_stakeholder_name,
} }
row = [FIELDS.get(column, "") for column in columns] row = [FIELDS.get(column, "") for column in columns]
@ -1927,6 +1945,12 @@ class DomainRequestDataType(DomainRequestExport):
"Last submitted date", "Last submitted date",
"First submitted date", "First submitted date",
"Last status update", "Last status update",
"Purpose",
"Domain name rationale",
"Target time frame",
"Interagency initiative",
"EOP stakeholder name",
"EOP stakeholder email",
] ]
@classmethod @classmethod
@ -2071,6 +2095,12 @@ class DomainRequestDataFull(DomainRequestExport):
"CISA regional representative", "CISA regional representative",
"Current websites", "Current websites",
"Investigator", "Investigator",
"Purpose",
"Domain name rationale",
"Target time frame",
"Interagency initiative",
"EOP stakeholder name",
"EOP stakeholder email",
] ]
@classmethod @classmethod

View file

@ -625,6 +625,11 @@ class PortfolioAdditionalDetails(DomainRequestWizard):
2: FEBAnythingElseYesNoForm 2: FEBAnythingElseYesNoForm
3: PortfolioAnythingElseForm 3: PortfolioAnythingElseForm
""" """
if not self.requires_feb_questions():
for i in range(3):
forms[i].mark_form_for_deletion()
# If FEB questions aren't required, validate only the anything else form
return forms[3].is_valid()
eop_forms_valid = True eop_forms_valid = True
if not forms[0].is_valid(): if not forms[0].is_valid():
# If the user isn't working with EOP, don't validate the EOP contact form # If the user isn't working with EOP, don't validate the EOP contact form
@ -792,11 +797,11 @@ class Purpose(DomainRequestWizard):
option = feb_purpose_options_form.cleaned_data.get("feb_purpose_choice") option = feb_purpose_options_form.cleaned_data.get("feb_purpose_choice")
if option == "new": if option == "new":
purpose_details_form.fields["purpose"].error_messages = { purpose_details_form.fields["purpose"].error_messages = {
"required": "Explain why a new domain is required." "required": "Provide details on why a new domain is required."
} }
elif option == "redirect": elif option == "redirect":
purpose_details_form.fields["purpose"].error_messages = { purpose_details_form.fields["purpose"].error_messages = {
"required": "Explain why a redirect is needed." "required": "Provide details on why a redirect is necessary."
} }
elif option == "other": elif option == "other":
purpose_details_form.fields["purpose"].error_messages = { purpose_details_form.fields["purpose"].error_messages = {
@ -965,6 +970,9 @@ class Review(DomainRequestWizard):
context["Step"] = self.get_step_enum().__members__ context["Step"] = self.get_step_enum().__members__
context["domain_request"] = self.domain_request context["domain_request"] = self.domain_request
context["requires_feb_questions"] = self.requires_feb_questions() context["requires_feb_questions"] = self.requires_feb_questions()
context["purpose_label"] = DomainRequest.FEBPurposeChoices.get_purpose_label(
self.domain_request.feb_purpose_choice
)
return context return context
def goto_next_step(self): def goto_next_step(self):
@ -1178,6 +1186,10 @@ class PortfolioDomainRequestStatusViewOnly(DetailView):
context["Step"] = PortfolioDomainRequestStep.__members__ context["Step"] = PortfolioDomainRequestStep.__members__
context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep) context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep)
context["form_titles"] = wizard.titles context["form_titles"] = wizard.titles
context["requires_feb_questions"] = self.object.is_feb() and flag_is_active_for_user(
self.request.user, "organization_feature"
)
context["purpose_label"] = DomainRequest.FEBPurposeChoices.get_purpose_label(self.object.feb_purpose_choice)
return context return context