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
# ------ FEB fields ------
# 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.
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",
{
@ -2833,10 +2844,24 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
"cisa_representative_first_name",
"cisa_representative_last_name",
"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",
{
@ -3033,23 +3058,41 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
def get_fieldsets(self, request, obj=None):
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
# if it is not enabled
if not flag_is_active_for_user(request.user, "organization_requests"):
excluded_fields = [
"portfolio",
"sub_organization",
"requested_suborganization",
"suborganization_city",
"suborganization_state_territory",
]
modified_fieldsets = []
for name, data in 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
if not org_flag:
excluded_fields.update(org_fields)
excluded_fields.update(feb_fields)
modified_fieldsets = []
for name, data in 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
# Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change):

View file

@ -1,41 +1,105 @@
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 = {
'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
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.' +
const labelElement = element.querySelector('p em');
labelElement.innerHTML = 'Explain why a new domain name is needed instead of using a ' +
'subdomain of an existing website.' +
'<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. ' +
'Include any information or data that shows how the new domain would ' +
'benefit the public or meet user needs. ' +
'<span class="usa-label--required">*</span>';
// Mark that we're no longer in initial load
isInitialLoad = false;
},
element: document.getElementById('purpose-details-container')
},
'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
showElement(element);
// 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. ' +
'<span class="usa-label--required">*</span>';
// Mark that we're no longer in initial load
isInitialLoad = false;
},
element: document.getElementById('purpose-details-container')
},
'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
showElement(element);
// 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. ' +
'<span class="usa-label--required">*</span>';
// Mark that we're no longer in initial load
isInitialLoad = false;
},
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 = [
{
"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",
"first_name": "Aditi",

View file

@ -1,6 +1,7 @@
from django import forms
from django.core.validators import MaxLengthValidator
from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm
from registrar.models.domain_request import DomainRequest
class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm):
@ -11,6 +12,8 @@ class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm, BaseDeletableRegistrar
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
def form_is_checked(self):
"""
@ -25,7 +28,9 @@ class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
widget=forms.Textarea(attrs={"maxlength": "2000"}),
max_length=2000,
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=[
MaxLengthValidator(
2000,
@ -41,18 +46,14 @@ class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
field_name = "feb_purpose_choice"
form_choices = (
("new", "Used for a new website"),
("redirect", "Used as a redirect for an existing website"),
("other", "Not for a website"),
)
form_choices = DomainRequest.FEBPurposeChoices.choices
feb_purpose_choice = forms.ChoiceField(
required=True,
choices=form_choices,
widget=forms.RadioSelect,
error_messages={
"required": "This question is required.",
"required": "Select the purpose of your requested domain.",
},
label="Select one",
)
@ -65,6 +66,10 @@ class FEBTimeFrameYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
"""
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
def form_is_checked(self):
@ -79,7 +84,7 @@ class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
label="time_frame_details",
widget=forms.Textarea(
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)?"
}
),
@ -89,7 +94,7 @@ class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
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"
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
def form_is_checked(self):
@ -156,29 +165,12 @@ class EOPContactForm(BaseDeletableRegistrarForm):
error_messages={"required": "Enter the last name / family name of this contact."},
required=True,
)
email = forms.EmailField(
label="Email",
max_length=None,
error_messages={
"required": ("Enter an email address in the required format, like name@example.com."),
"invalid": ("Enter an email address in the required format, like name@example.com."),
},
validators=[
MaxLengthValidator(
320,
message="Response must be less than 320 characters.",
)
],
required=True,
help_text="Enter an email address in the required format, like name@example.com.",
)
@classmethod
def from_database(cls, obj):
return {
"first_name": obj.eop_stakeholder_first_name,
"last_name": obj.eop_stakeholder_last_name,
"email": obj.eop_stakeholder_email,
}
def to_database(self, obj):
@ -192,7 +184,6 @@ class EOPContactForm(BaseDeletableRegistrarForm):
return
obj.eop_stakeholder_first_name = self.cleaned_data["first_name"]
obj.eop_stakeholder_last_name = self.cleaned_data["last_name"]
obj.eop_stakeholder_email = self.cleaned_data["email"]
obj.save()

View file

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

View file

@ -13,17 +13,17 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="domainrequest",
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(
model_name="domainrequest",
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(
model_name="domainrequest",
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(
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
class FEBPurposeChoices(models.TextChoices):
WEBSITE = "website"
REDIRECT = "redirect"
OTHER = "other"
WEBSITE = "new", "Used for a new website"
REDIRECT = "redirect", "Used as a redirect for an existing website"
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):
ALABAMA = "AL", "Alabama (AL)"
@ -510,17 +515,21 @@ class DomainRequest(TimeStampedModel):
feb_naming_requirements = models.BooleanField(
null=True,
blank=True,
verbose_name="Meets naming requirements",
)
feb_naming_requirements_details = models.TextField(
null=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(
null=True,
blank=True,
choices=FEBPurposeChoices.choices,
verbose_name="Purpose type",
)
working_with_eop = models.BooleanField(
@ -531,24 +540,17 @@ class DomainRequest(TimeStampedModel):
eop_stakeholder_first_name = models.CharField(
null=True,
blank=True,
verbose_name="EOP Stakeholder First Name",
verbose_name="EOP contact first name",
)
eop_stakeholder_last_name = models.CharField(
null=True,
blank=True,
verbose_name="EOP Stakeholder Last Name",
)
eop_stakeholder_email = models.EmailField(
null=True,
blank=True,
verbose_name="EOP Stakeholder Email",
verbose_name="EOP contact last name",
)
# 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,
@ -562,6 +564,7 @@ class DomainRequest(TimeStampedModel):
time_frame_details = models.TextField(
null=True,
blank=True,
verbose_name="Target time frame",
)
is_interagency_initiative = models.BooleanField(
@ -572,6 +575,7 @@ class DomainRequest(TimeStampedModel):
interagency_initiative_details = models.TextField(
null=True,
blank=True,
verbose_name="Interagency initiative",
)
alternative_domains = models.ManyToManyField(
@ -1015,11 +1019,15 @@ class DomainRequest(TimeStampedModel):
if not context:
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)
requires_feb_questions = self.is_feb() and is_org_user
purpose_label = DomainRequest.FEBPurposeChoices.get_purpose_label(self.feb_purpose_choice)
context = {
"domain_request": self,
# This is the user that we refer to in the email
"recipient": recipient,
"is_org_user": is_org_user,
"requires_feb_questions": requires_feb_questions,
"purpose_label": purpose_label,
}
if custom_email_content:

View file

@ -2,19 +2,25 @@
{% 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="{% 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">
<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>
</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 states 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 organizations 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
@ -29,7 +35,7 @@
{% 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 %}
@ -42,8 +48,8 @@
<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.
After you enter your domain, well make sure its available and that it meets some of our naming requirements.
If your domain passes these initial checks, well verify that it meets all our requirements after you complete the rest of this form.
</p>
{% 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 #}
@ -65,7 +71,7 @@
<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?
Are there other domains youd like if we cant 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 #}
@ -99,7 +105,7 @@
>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.
If youre not sure this is the domain you want, thats ok. You can change the domain later.
</p>
</fieldset>
@ -114,9 +120,10 @@
<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.
naming requirements for executive branch agencies.</a> Agency submissions are expected to meet each requirement.
</p>
<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>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% 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" #}
<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 class="margin-bottom-3 margin-top-3">
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p>
{% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %}
{% input_with_errors forms.3.feb_naming_requirements_details %}
{% endwith %}
<p class="usa-hint">Maximum 2000 characters allowed.</p>
</div>
</fieldset>
{% endif %}

View file

@ -12,14 +12,12 @@
{% block form_fields %}
{% 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.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>
<legend>
<h2>What is the purpose of your requested domain?</h2>
</legend>
<p class="margin-bottom-0 margin-top-1">
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
</p>
@ -28,51 +26,58 @@
{% 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 class="margin-bottom-3 margin-top-3">
<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.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">
</fieldset>
<fieldset class="usa-fieldset margin-top-2">
{{forms.2.management_form}}
{{forms.3.management_form}}
<legend>
<h2>Do you have a target time frame for launching this domain?</h2>
</legend>
<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>
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.3.time_frame_details %}
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.2.has_timeframe %}
{% 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">
<div id="purpose-timeframe-details-container" class="conditional-panel">
<p class="margin-bottom-3 margin-top-3">
<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>
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.3.time_frame_details %}
{% endwith %}
</div>
</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">
<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>
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
{% input_with_errors forms.5.interagency_initiative_details %}
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.4.is_interagency_initiative %}
{% endwith %}
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
</div>
</fieldset>
<div id="purpose-interagency-initaitive-details-container" class="conditional-panel">
<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 %}
<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>

View file

@ -8,12 +8,47 @@ Current websites: {% for site in domain_request.current_websites.all %}
{% endfor %}{% endif %}
.gov domain:
{{ 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 %}
Alternative domains:
{% for site in domain_request.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
{% endfor %}{% endif %}
Purpose of your domain:
{% if requires_feb_questions %}
{{ purpose_label }}
{{ 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 %}
Additional details:
{{ domain_request.anything_else }}

View file

@ -10,19 +10,19 @@
{% if step == Step.REQUESTING_ENTITY %}
{% with title=form_titles|get_item:step %}
{% 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 %}
{% 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>
{% 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 %}
<p class="margin-y-0">{{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}}</p>
{% endif %}
{% else %}
{% 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 %}
{% endif %}
{% endwith %}
@ -53,18 +53,78 @@
{% endfor %}
</ul>
{% 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 %}
{% 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 %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
{% endwith %}
{% if requires_feb_questions %}
{% with title=form_titles|get_item:step %}
{% 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 %}
{% if step == Step.ADDITIONAL_DETAILS %}
{% 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 %}
{% if requires_feb_questions %}
{% with title=form_titles|get_item:step %}
{% 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 %}
{% if step == Step.REQUIREMENTS %}

View file

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

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_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_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_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
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.
@ -106,6 +108,8 @@ def input_with_errors(context, field=None): # noqa: C901
elif key == "add_aria_label":
aria_labels.append(value)
elif key == "add_aria_describedby":
described_by.append(value)
elif key == "sublabel_text":
sublabel_text.append(value)

View file

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

View file

@ -2615,8 +2615,8 @@ class DomainRequestTests(TestWithUser, WebTest):
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"
domain_form["dotgov_domain-feb_naming_requirements"] = "False"
domain_form["dotgov_domain-feb_naming_requirements_details"] = "Because this is a test"
with patch(
"registrar.forms.domain_request_wizard.DotGovDomainForm.clean_requested_domain", return_value=domain
): # noqa
@ -2631,11 +2631,11 @@ class DomainRequestTests(TestWithUser, WebTest):
purpose_form = purpose_page.forms[0]
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-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-interagency_initiative_details"] = "test"
purpose_form["purpose-interagency_initiative_details"] = "FakeInteragencyInitiative"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_result = purpose_form.submit()
@ -2646,9 +2646,8 @@ class DomainRequestTests(TestWithUser, WebTest):
additional_details_form = additional_details_page.forms[0]
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-last_name"] = "Tester"
additional_details_form["portfolio_additional_details-email"] = "testy@town.com"
additional_details_form["portfolio_additional_details-first_name"] = "TesterFirstName"
additional_details_form["portfolio_additional_details-last_name"] = "TesterLastName"
additional_details_form["portfolio_additional_details-has_anything_else_text"] = "True"
additional_details_form["portfolio_additional_details-anything_else"] = "test"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -2659,6 +2658,16 @@ class DomainRequestTests(TestWithUser, WebTest):
requirements_page = additional_details_result.follow()
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):
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, "additional_details-first_name")
self.assertContains(additional_details_page, "additional_details-last_name")
self.assertContains(additional_details_page, "additional_details-email")
# Make sure the additional details form is present
self.assertContains(additional_details_page, "additional_details-has_anything_else_text")
@ -2733,6 +2741,25 @@ class DomainRequestTests(TestWithUser, WebTest):
"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
def test_domain_request_formsets(self):
"""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")]
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.
# "extra_fields" are precomputed fields (generated in the DB or parsed).
FIELDS = {
@ -1882,6 +1894,12 @@ class DomainRequestExport(BaseExport):
"Last submitted date": model.get("last_submitted_date"),
"First submitted date": model.get("first_submitted_date"),
"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]
@ -1927,6 +1945,12 @@ class DomainRequestDataType(DomainRequestExport):
"Last submitted date",
"First submitted date",
"Last status update",
"Purpose",
"Domain name rationale",
"Target time frame",
"Interagency initiative",
"EOP stakeholder name",
"EOP stakeholder email",
]
@classmethod
@ -2071,6 +2095,12 @@ class DomainRequestDataFull(DomainRequestExport):
"CISA regional representative",
"Current websites",
"Investigator",
"Purpose",
"Domain name rationale",
"Target time frame",
"Interagency initiative",
"EOP stakeholder name",
"EOP stakeholder email",
]
@classmethod

View file

@ -625,6 +625,11 @@ class PortfolioAdditionalDetails(DomainRequestWizard):
2: FEBAnythingElseYesNoForm
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
if not forms[0].is_valid():
# 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")
if option == "new":
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":
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":
purpose_details_form.fields["purpose"].error_messages = {
@ -965,6 +970,9 @@ class Review(DomainRequestWizard):
context["Step"] = self.get_step_enum().__members__
context["domain_request"] = self.domain_request
context["requires_feb_questions"] = self.requires_feb_questions()
context["purpose_label"] = DomainRequest.FEBPurposeChoices.get_purpose_label(
self.domain_request.feb_purpose_choice
)
return context
def goto_next_step(self):
@ -1178,6 +1186,10 @@ class PortfolioDomainRequestStatusViewOnly(DetailView):
context["Step"] = PortfolioDomainRequestStep.__members__
context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep)
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