mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 10:46:06 +02:00
Merge branch 'main' of https://github.com/cisagov/manage.get.gov into rh/quick-fix-action-needed-emails
This commit is contained in:
commit
ce4f62f1d0
15 changed files with 714 additions and 38 deletions
41
src/registrar/assets/src/js/getgov/domain-purpose-form.js
Normal file
41
src/registrar/assets/src/js/getgov/domain-purpose-form.js
Normal 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')
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { hookupYesNoListener } from './radios.js';
|
||||
import { hookupYesNoListener, hookupCallbacksToRadioToggler } from './radios.js';
|
||||
import { initDomainValidators } from './domain-validators.js';
|
||||
import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js';
|
||||
import { initFormNameservers } from './form-nameservers'
|
||||
|
@ -16,6 +16,7 @@ import { initDomainManagersPage } from './domain-managers.js';
|
|||
import { initDomainDSData } from './domain-dsdata.js';
|
||||
import { initDomainDNSSEC } from './domain-dnssec.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';
|
||||
|
||||
initDomainValidators();
|
||||
|
@ -27,6 +28,14 @@ initFormNameservers();
|
|||
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees');
|
||||
hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null);
|
||||
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);
|
||||
hookupYesNoListener("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();
|
||||
|
||||
userProfileListener();
|
||||
|
|
|
@ -17,7 +17,7 @@ export function hookupYesNoListener(radioButtonName, elementIdToShowIfYes, eleme
|
|||
'False': elementIdToShowIfNo
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hookup listeners for radio togglers in form fields.
|
||||
*
|
||||
|
@ -75,3 +75,57 @@ export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -608,7 +608,10 @@ class DotGovDomainForm(RegistrarForm):
|
|||
)
|
||||
|
||||
|
||||
class PurposeForm(RegistrarForm):
|
||||
class PurposeDetailsForm(BaseDeletableRegistrarForm):
|
||||
|
||||
field_name = "purpose"
|
||||
|
||||
purpose = forms.CharField(
|
||||
label="Purpose",
|
||||
widget=forms.Textarea(
|
||||
|
|
123
src/registrar/forms/feb.py
Normal file
123
src/registrar/forms/feb.py
Normal 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."},
|
||||
)
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -54,6 +54,11 @@ class DomainRequest(TimeStampedModel):
|
|||
"""Returns the associated label for a given status name"""
|
||||
return cls(status_name).label if status_name else None
|
||||
|
||||
class FEBPurposeChoices(models.TextChoices):
|
||||
WEBSITE = "website"
|
||||
REDIRECT = "redirect"
|
||||
OTHER = "other"
|
||||
|
||||
class StateTerritoryChoices(models.TextChoices):
|
||||
ALABAMA = "AL", "Alabama (AL)"
|
||||
ALASKA = "AK", "Alaska (AK)"
|
||||
|
@ -501,6 +506,51 @@ class DomainRequest(TimeStampedModel):
|
|||
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(
|
||||
"registrar.Website",
|
||||
blank=True,
|
||||
|
@ -508,11 +558,6 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Other domain names the creator provided for consideration",
|
||||
)
|
||||
|
||||
purpose = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
other_contacts = models.ManyToManyField(
|
||||
"registrar.Contact",
|
||||
blank=True,
|
||||
|
@ -1389,6 +1434,12 @@ class DomainRequest(TimeStampedModel):
|
|||
has_details = False
|
||||
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]:
|
||||
"""Is this domain request for a federal agency?
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
{% if request.path|endswith:"renewal"%}
|
||||
<h1>Renew {{domain.name}} </h1>
|
||||
{%else%}
|
||||
<h1 class="break-word">Domain Overview</h1>
|
||||
<h1 class="break-word">Domain overview</h1>
|
||||
{% endif%}
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
{% if domain.dnssecdata is not None %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
|
||||
{% 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 %}
|
||||
|
||||
{% if portfolio %}
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% 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">
|
||||
<li>Be available </li>
|
||||
<li>Relate to your organization’s 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>
|
||||
</ul>
|
||||
</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.
|
||||
{% 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 state's two-letter abbreviation.{% endif %}</p>
|
||||
|
||||
{% 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 organization's initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>Note that <strong>only federal agencies can request generic terms</strong> like
|
||||
|
@ -41,9 +41,10 @@
|
|||
<legend>
|
||||
<h2>What .gov domain do you want?</h2>
|
||||
</legend>
|
||||
|
||||
<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. 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>
|
||||
|
||||
<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.
|
||||
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" %}
|
||||
{# attr_validate / validate="domain" invokes code in getgov.min.js #}
|
||||
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
||||
|
@ -63,10 +64,9 @@
|
|||
<legend>
|
||||
<h2 id="alternative-domains-title">Alternative domains (optional)</h2>
|
||||
</legend>
|
||||
|
||||
<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?</p>
|
||||
|
||||
<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?
|
||||
</p>
|
||||
{% with attr_aria_labelledby="alt_domain_instructions" %}
|
||||
{# 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" %}
|
||||
|
@ -83,10 +83,10 @@
|
|||
<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">
|
||||
<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>
|
||||
</svg><span class="margin-left-05">Add another alternative</span>
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#add_circle"></use>
|
||||
</svg>
|
||||
<span class="margin-left-05">Add another alternative</span>
|
||||
</button>
|
||||
|
||||
<div class="margin-bottom-3">
|
||||
<div class="usa-sr-only" id="alternative-domains__check-availability">Check domain availability</div>
|
||||
<button
|
||||
|
@ -98,10 +98,41 @@
|
|||
aria-describedby="alternative-domains__check-availability"
|
||||
>Check availability</button>
|
||||
</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 you’re not sure this is the domain you want, that’s 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 %}
|
||||
|
|
|
@ -3,17 +3,84 @@
|
|||
|
||||
{% 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>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 you’ll use your .gov domain. Will it be used for a website, email, or something else?</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>
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% 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 you’ll 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" %}
|
||||
{% input_with_errors forms.0.purpose %}
|
||||
{% input_with_errors forms.1.purpose %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -1981,7 +1981,14 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"senior_official",
|
||||
"approved_domain",
|
||||
"requested_domain",
|
||||
"feb_naming_requirements",
|
||||
"feb_naming_requirements_details",
|
||||
"feb_purpose_choice",
|
||||
"purpose",
|
||||
"has_timeframe",
|
||||
"time_frame_details",
|
||||
"is_interagency_initiative",
|
||||
"interagency_initiative_details",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"has_anything_else_text",
|
||||
|
|
|
@ -14,10 +14,11 @@ from registrar.forms.domain_request_wizard import (
|
|||
OtherContactsForm,
|
||||
RequirementsForm,
|
||||
TribalGovernmentForm,
|
||||
PurposeForm,
|
||||
AnythingElseForm,
|
||||
AboutYourOrganizationForm,
|
||||
)
|
||||
from registrar.forms import PurposeDetailsForm
|
||||
|
||||
from registrar.forms.domain import ContactForm
|
||||
from registrar.forms.portfolio import (
|
||||
PortfolioInvitedMemberForm,
|
||||
|
@ -257,7 +258,7 @@ class TestFormValidation(MockEppLib):
|
|||
@less_console_noise_decorator
|
||||
def test_purpose_form_character_count_invalid(self):
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = PurposeForm(
|
||||
form = PurposeDetailsForm(
|
||||
data={
|
||||
"purpose": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.utils import timezone
|
|||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from .common import MockSESClient, completed_domain_request # type: ignore
|
||||
from django_webtest import WebTest # type: ignore
|
||||
import boto3_mocking # type: ignore
|
||||
|
@ -53,6 +54,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
UserPortfolioPermission.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
FederalAgency.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_form_intro_acknowledgement(self):
|
||||
|
@ -2546,6 +2548,128 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.assertContains(dotgov_page, "CityofEudoraKS.gov")
|
||||
self.assertNotContains(dotgov_page, "medicare.gov")
|
||||
|
||||
# @less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_domain_request_dotgov_domain_FEB_questions(self):
|
||||
"""
|
||||
Test that for a member of a federal executive branch portfolio with org feature on, the dotgov domain page
|
||||
contains additional questions for OMB.
|
||||
"""
|
||||
agency, _ = FederalAgency.objects.get_or_create(
|
||||
agency="US Treasury Dept",
|
||||
federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
|
||||
portfolio, _ = Portfolio.objects.get_or_create(
|
||||
creator=self.user,
|
||||
organization_name="Test Portfolio",
|
||||
organization_type=Portfolio.OrganizationChoices.FEDERAL,
|
||||
federal_agency=agency,
|
||||
)
|
||||
|
||||
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
intro_page = self.app.get(reverse("domain-request:start"))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
intro_form = intro_page.forms[0]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
intro_result = intro_form.submit()
|
||||
|
||||
# follow first redirect
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
portfolio_requesting_entity = intro_result.follow()
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
# ---- REQUESTING ENTITY PAGE ----
|
||||
requesting_entity_form = portfolio_requesting_entity.forms[0]
|
||||
requesting_entity_form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = False
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
requesting_entity_result = requesting_entity_form.submit()
|
||||
|
||||
# ---- CURRENT SITES PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
current_sites_page = requesting_entity_result.follow()
|
||||
current_sites_form = current_sites_page.forms[0]
|
||||
current_sites_form["current_sites-0-website"] = "www.treasury.com"
|
||||
|
||||
# test saving the page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
current_sites_result = current_sites_form.submit()
|
||||
|
||||
# ---- DOTGOV DOMAIN PAGE ----
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
dotgov_page = current_sites_result.follow()
|
||||
|
||||
# separate out these tests for readability
|
||||
self.feb_dotgov_domain_tests(dotgov_page)
|
||||
|
||||
# Now proceed with the actual test
|
||||
domain_form = dotgov_page.forms[0]
|
||||
domain = "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
|
||||
def test_domain_request_formsets(self):
|
||||
"""Users are able to add more than one of some fields."""
|
||||
|
|
|
@ -15,10 +15,12 @@ from registrar.decorators import (
|
|||
grant_access,
|
||||
)
|
||||
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.models import DomainRequest
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
from registrar.utility.waffle import flag_is_active_for_user
|
||||
from registrar.views.utility import StepsHelper
|
||||
from registrar.utility.enums import Step, PortfolioDomainRequestStep
|
||||
|
||||
|
@ -180,6 +182,9 @@ class DomainRequestWizard(TemplateView):
|
|||
"""Determines which step enum we should use for the wizard"""
|
||||
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
|
||||
def prefix(self):
|
||||
"""Namespace the wizard to avoid clashes in session variable names."""
|
||||
|
@ -652,18 +657,130 @@ class CurrentSites(DomainRequestWizard):
|
|||
|
||||
class DotgovDomain(DomainRequestWizard):
|
||||
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):
|
||||
context = super().get_context_data()
|
||||
context["generic_org_type"] = self.domain_request.generic_org_type
|
||||
context["federal_type"] = self.domain_request.federal_type
|
||||
context["requires_feb_questions"] = self.requires_feb_questions()
|
||||
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):
|
||||
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):
|
||||
|
@ -711,9 +828,7 @@ class OtherContacts(DomainRequestWizard):
|
|||
|
||||
|
||||
class AdditionalDetails(DomainRequestWizard):
|
||||
|
||||
template_name = "domain_request_additional_details.html"
|
||||
|
||||
forms = [
|
||||
forms.CisaRepresentativeYesNoForm,
|
||||
forms.CisaRepresentativeForm,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue