From dfd237755dc3142c06170607fd03ef9503f6844c Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 18 Feb 2025 12:11:19 -0600
Subject: [PATCH 01/21] pushing to pull on other device
---
src/registrar/assets/src/js/getgov/main.js | 2 +
src/registrar/forms/domain_request_wizard.py | 64 +++++++++++++++++-
...equest_feb_naming_requirements_and_more.py | 23 +++++++
src/registrar/models/domain_request.py | 20 ++++++
.../domain_request_dotgov_domain.html | 66 ++++++++++++++-----
src/registrar/views/domain_request.py | 45 +++++++++++--
6 files changed, 197 insertions(+), 23 deletions(-)
create mode 100644 src/registrar/migrations/0141_domainrequest_feb_naming_requirements_and_more.py
diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js
index a077da929..67db06376 100644
--- a/src/registrar/assets/src/js/getgov/main.js
+++ b/src/registrar/assets/src/js/getgov/main.js
@@ -25,6 +25,8 @@ nameserversFormListener();
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("feb_naming_requirements", "", "domain-naming-requirements-details-container");
+
initializeUrbanizationToggle();
userProfileListener();
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index d7a02b124..91c8ccb9b 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -607,6 +607,68 @@ class DotGovDomainForm(RegistrarForm):
},
)
+class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm):
+ """
+ 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
+
+ def clean(self):
+ # Skip validation if this form is not applicable.
+ if not (self.domain_request.is_federal() and self.domain_request.federal_type == "Executive"):
+ # If not executive, default to "yes"
+ self.cleaned_data["feb_naming_requirements"] = "yes"
+ return self.cleaned_data
+
+ # Only validate the yes/no field here; details are handled by the separate details form.
+ cleaned = super().clean()
+ return cleaned
+
+ def to_database(self, obj: DomainRequest):
+ """
+ Saves the cleaned data from this form to the DomainRequest object.
+ """
+ if not self.is_valid():
+ return
+ obj.feb_naming_requirements = (self.cleaned_data["feb_naming_requirements"] == "yes")
+ obj.save()
+
+ @classmethod
+ def from_database(cls, obj):
+ """
+ Retrieves initial data from the DomainRequest object to prepopulate the form.
+ """
+ initial = {}
+ if hasattr(obj, "feb_naming_requirements"):
+ initial["feb_naming_requirements"] = "yes" if obj.feb_naming_requirements else "no"
+ return initial
+
+class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
+ JOIN = "feb_naming_requirements_details"
+
+ # 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,
+ label="",
+ help_text="Maximum 2000 characters allowed.",
+ )
+
+ def to_database(self, obj: DomainRequest):
+ if not self.is_valid():
+ return
+ obj.feb_naming_requirements_details = self.cleaned_data["feb_naming_requirements_details"]
+ obj.save()
class PurposeForm(RegistrarForm):
purpose = forms.CharField(
@@ -625,7 +687,7 @@ class PurposeForm(RegistrarForm):
],
error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
)
-
+
class OtherContactsYesNoForm(BaseYesNoForm):
"""The yes/no field for the OtherContacts form."""
diff --git a/src/registrar/migrations/0141_domainrequest_feb_naming_requirements_and_more.py b/src/registrar/migrations/0141_domainrequest_feb_naming_requirements_and_more.py
new file mode 100644
index 000000000..32634d8ee
--- /dev/null
+++ b/src/registrar/migrations/0141_domainrequest_feb_naming_requirements_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.17 on 2025-02-13 22:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("registrar", "0140_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),
+ ),
+ ]
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 1cca3742f..ceaa19e77 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -501,6 +501,16 @@ class DomainRequest(TimeStampedModel):
on_delete=models.PROTECT,
)
+ feb_naming_requirements = models.BooleanField(
+ null=True,
+ blank=True,
+ )
+
+ feb_naming_requirements_details = models.TextField(
+ null=True,
+ blank=True,
+ )
+
alternative_domains = models.ManyToManyField(
"registrar.Website",
blank=True,
@@ -1388,6 +1398,16 @@ class DomainRequest(TimeStampedModel):
if self.has_anything_else_text is None or self.has_cisa_representative is None:
has_details = False
return has_details
+
+ def is_feb(self) -> bool:
+ """Is this domain request for a Federal Executive Branch agency?"""
+ # if not self.generic_org_type:
+ # # generic_org_type is either blank or None, assume no
+ # return False
+ # if self.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
+ # return self.federal_type == DomainRequest.FederalChoices.EXECUTIVE
+ # return False
+ return True # TODO: this is for testing, remove before merging
def is_federal(self) -> Union[bool, None]:
"""Is this domain request for a federal agency?
diff --git a/src/registrar/templates/domain_request_dotgov_domain.html b/src/registrar/templates/domain_request_dotgov_domain.html
index 91373609d..d58815e3b 100644
--- a/src/registrar/templates/domain_request_dotgov_domain.html
+++ b/src/registrar/templates/domain_request_dotgov_domain.html
@@ -2,19 +2,19 @@
{% load static field_helpers url_helpers %}
{% block form_instructions %}
-
Before requesting a .gov domain, please make sure it meets our naming requirements. Your domain name must:
+
Before requesting a .gov domain, please make sure it meets our naming requirements. Your domain name must:
Be available
-
Relate to your organization’s name, location, and/or services
+
Relate to your organization's name, location, and/or services
Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience)
Names that uniquely apply to your organization 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 %}
+ {% if not is_federal %}In most instances, this requires including your state's two-letter abbreviation.{% endif %}
{% if not portfolio %}
-
Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.
+
Requests for your organization's initials or an abbreviated name might not be approved, but we encourage you to request the name you want.
{% endif %}
Note that only federal agencies can request generic terms like
@@ -41,9 +41,10 @@
-
-
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, 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.
+
{% 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 @@
-
-
Are there other domains you’d like if we can’t give
- you your first choice?
-
+
+ Are there other domains you'd like if we can't give you your first choice?
+
{% 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" %}
@@ -79,13 +79,12 @@
{% endfor %}
{% endwith %}
{% endwith %}
-
-
-
+
+ If you're not sure this is the domain you want, that's ok. You can change the domain later.
+
+
-
If you’re not sure this is the domain you want, that’s ok. You can change the domain later.
+ {{ forms.2.management_form }}
+ {{ forms.3.management_form }}
-
+ {% if is_feb %}
+
+ {% endif %}
{% endblock %}
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 3248c1368..83662505f 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -466,6 +466,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
"requested_domain__name": requested_domain_name,
}
context["domain_request_id"] = self.domain_request.id
+ context["is_executive"] = self.domain_request.is_federal() and self.domain_request.federal_type == "Executive"
return context
def get_step_list(self) -> list:
@@ -652,14 +653,52 @@ class CurrentSites(DomainRequestWizard):
class DotgovDomain(DomainRequestWizard):
template_name = "domain_request_dotgov_domain.html"
- forms = [forms.DotGovDomainForm, forms.AlternativeDomainFormSet]
+ forms = [
+ forms.DotGovDomainForm,
+ forms.AlternativeDomainFormSet,
+ forms.ExecutiveNamingRequirementsYesNoForm,
+ forms.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["is_feb"] = self.domain_request.is_feb()
return context
+ def is_valid(self, forms_list: list) -> bool:
+ """
+ Expected order of forms_list:
+ 0: DotGovDomainForm
+ 1: AlternativeDomainFormSet
+ 2: ExecutiveNamingRequirementsYesNoForm
+ 3: ExecutiveNamingRequirementsDetailsForm
+ """
+ # If not a federal executive branch agency, mark executive-related forms for deletion.
+ if not (self.domain_request.is_feb()):
+ forms_list[2].mark_form_for_deletion()
+ forms_list[3].mark_form_for_deletion()
+ return all(form.is_valid() for form in forms_list)
+
+ valid = True
+ yesno_form = forms_list[2]
+ details_form = forms_list[3]
+
+ if yesno_form.cleaned_data.get("feb_naming_requirements") == "yes":
+ # If the user selects "yes", no details are needed.
+ details_form.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 = (
+ yesno_form.is_valid() and
+ details_form.is_valid() and
+ all(form.is_valid() for i, form in enumerate(forms_list) if i not in [2, 3])
+ )
+ return valid
+
class Purpose(DomainRequestWizard):
template_name = "domain_request_purpose.html"
@@ -711,9 +750,7 @@ class OtherContacts(DomainRequestWizard):
class AdditionalDetails(DomainRequestWizard):
-
template_name = "domain_request_additional_details.html"
-
forms = [
forms.CisaRepresentativeYesNoForm,
forms.CisaRepresentativeForm,
From 292c08902a01daf43c69148ff7190d5cd41b0d22 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Fri, 21 Feb 2025 15:28:50 -0600
Subject: [PATCH 02/21] add new FEB questions to gov domain request wizard page
---
src/registrar/assets/src/js/getgov/main.js | 2 +-
src/registrar/forms/domain_request_wizard.py | 29 ++++++++++++++++++--
src/registrar/models/domain_request.py | 13 ++++-----
src/registrar/views/domain_request.py | 22 +++++++++------
4 files changed, 47 insertions(+), 19 deletions(-)
diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js
index 67db06376..796e6f815 100644
--- a/src/registrar/assets/src/js/getgov/main.js
+++ b/src/registrar/assets/src/js/getgov/main.js
@@ -25,7 +25,7 @@ nameserversFormListener();
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("feb_naming_requirements", "", "domain-naming-requirements-details-container");
+hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container");
initializeUrbanizationToggle();
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 91c8ccb9b..98ccc9122 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -625,8 +625,11 @@ class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm):
def clean(self):
# Skip validation if this form is not applicable.
if not (self.domain_request.is_federal() and self.domain_request.federal_type == "Executive"):
- # If not executive, default to "yes"
- self.cleaned_data["feb_naming_requirements"] = "yes"
+ # Initialize cleaned_data if it doesn't exist
+ if not hasattr(self, 'cleaned_data'):
+ self.cleaned_data = {}
+ # If not executive, default to None
+ self.cleaned_data["feb_naming_requirements"] = None
return self.cleaned_data
# Only validate the yes/no field here; details are handled by the separate details form.
@@ -639,7 +642,7 @@ class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm):
"""
if not self.is_valid():
return
- obj.feb_naming_requirements = (self.cleaned_data["feb_naming_requirements"] == "yes")
+ obj.feb_naming_requirements = (self.cleaned_data.get("feb_naming_requirements", None) == "yes")
obj.save()
@classmethod
@@ -660,6 +663,7 @@ class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
widget=forms.Textarea(attrs={'maxlength': '2000'}),
max_length=2000,
required=True,
+ error_messages={"required": ("This field is required.")},
label="",
help_text="Maximum 2000 characters allowed.",
)
@@ -670,6 +674,25 @@ class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
obj.feb_naming_requirements_details = self.cleaned_data["feb_naming_requirements_details"]
obj.save()
+ def is_valid(self):
+ """
+ Validate that details are provided when required.
+ If the form is marked for deletion, bypass validation.
+ """
+ if self.form_data_marked_for_deletion:
+ return True
+
+ is_valid = super().is_valid()
+ if not is_valid:
+ return False
+
+ # Check if the details field has content
+ details = self.cleaned_data.get('feb_naming_requirements_details', '').strip()
+ if not details:
+ return False
+
+ return True
+
class PurposeForm(RegistrarForm):
purpose = forms.CharField(
label="Purpose",
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index ceaa19e77..e90edfa72 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -1401,13 +1401,12 @@ class DomainRequest(TimeStampedModel):
def is_feb(self) -> bool:
"""Is this domain request for a Federal Executive Branch agency?"""
- # if not self.generic_org_type:
- # # generic_org_type is either blank or None, assume no
- # return False
- # if self.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
- # return self.federal_type == DomainRequest.FederalChoices.EXECUTIVE
- # return False
- return True # TODO: this is for testing, remove before merging
+ if not self.generic_org_type:
+ # generic_org_type is either blank or None, assume no
+ return False
+ if self.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
+ return self.federal_type == DomainRequest.FederalChoices.EXECUTIVE
+ return False
def is_federal(self) -> Union[bool, None]:
"""Is this domain request for a federal agency?
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 83662505f..fce15af24 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -674,27 +674,33 @@ class DotgovDomain(DomainRequestWizard):
2: ExecutiveNamingRequirementsYesNoForm
3: ExecutiveNamingRequirementsDetailsForm
"""
+ logger.debug("Validating dotgov domain form")
# If not a federal executive branch agency, mark executive-related forms for deletion.
if not (self.domain_request.is_feb()):
forms_list[2].mark_form_for_deletion()
forms_list[3].mark_form_for_deletion()
return all(form.is_valid() for form in forms_list)
- valid = True
- yesno_form = forms_list[2]
- details_form = forms_list[3]
+ if not forms_list[2].is_valid():
+ logger.debug("Dotgov domain form is invalid")
+ if forms_list[2].cleaned_data.get("feb_naming_requirements", None) != "no":
+ forms_list[3].mark_form_for_deletion()
+ return False
+
+ logger.debug(f"feb_naming_requirements: {forms_list[2].cleaned_data.get('feb_naming_requirements', None)}")
- if yesno_form.cleaned_data.get("feb_naming_requirements") == "yes":
- # If the user selects "yes", no details are needed.
- details_form.mark_form_for_deletion()
+ if forms_list[2].cleaned_data.get("feb_naming_requirements", None) != "no":
+ 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 = (
- yesno_form.is_valid() and
- details_form.is_valid() and
+ forms_list[2].is_valid() and
+ forms_list[3].is_valid() and
all(form.is_valid() for i, form in enumerate(forms_list) if i not in [2, 3])
)
return valid
From 190bca2cac79367d91cab6cfc0426a3a19a788b1 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 25 Feb 2025 10:18:22 -0600
Subject: [PATCH 03/21] fix is_feb
---
src/registrar/models/domain_request.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index e90edfa72..8b20f08bc 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -1405,7 +1405,7 @@ class DomainRequest(TimeStampedModel):
# generic_org_type is either blank or None, assume no
return False
if self.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
- return self.federal_type == DomainRequest.FederalChoices.EXECUTIVE
+ return self.federal_type == BranchChoices.EXECUTIVE
return False
def is_federal(self) -> Union[bool, None]:
From 065febd496eca3350053070f91c426a7d08a5ab4 Mon Sep 17 00:00:00 2001
From: Matt-Spence
Date: Tue, 25 Feb 2025 14:34:01 -0500
Subject: [PATCH 04/21] correct form validation and dynamic elements
---
src/registrar/forms/domain_request_wizard.py | 71 +++----------------
src/registrar/models/domain_request.py | 2 +-
.../domain_request_dotgov_domain.html | 4 +-
src/registrar/views/domain_request.py | 38 +++++-----
4 files changed, 32 insertions(+), 83 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 98ccc9122..32c8e2ed4 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -607,7 +607,8 @@ class DotGovDomainForm(RegistrarForm):
},
)
-class ExecutiveNamingRequirementsYesNoForm(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.
@@ -621,77 +622,25 @@ class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm):
Determines the initial checked state of the form based on the domain_request's attributes.
"""
return self.domain_request.feb_naming_requirements
-
- def clean(self):
- # Skip validation if this form is not applicable.
- if not (self.domain_request.is_federal() and self.domain_request.federal_type == "Executive"):
- # Initialize cleaned_data if it doesn't exist
- if not hasattr(self, 'cleaned_data'):
- self.cleaned_data = {}
- # If not executive, default to None
- self.cleaned_data["feb_naming_requirements"] = None
- return self.cleaned_data
- # Only validate the yes/no field here; details are handled by the separate details form.
- cleaned = super().clean()
- return cleaned
- def to_database(self, obj: DomainRequest):
- """
- Saves the cleaned data from this form to the DomainRequest object.
- """
- if not self.is_valid():
- return
- obj.feb_naming_requirements = (self.cleaned_data.get("feb_naming_requirements", None) == "yes")
- obj.save()
-
- @classmethod
- def from_database(cls, obj):
- """
- Retrieves initial data from the DomainRequest object to prepopulate the form.
- """
- initial = {}
- if hasattr(obj, "feb_naming_requirements"):
- initial["feb_naming_requirements"] = "yes" if obj.feb_naming_requirements else "no"
- return initial
-
class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
- JOIN = "feb_naming_requirements_details"
-
# Text area for additional details; rendered conditionally when "no" is selected.
feb_naming_requirements_details = forms.CharField(
- widget=forms.Textarea(attrs={'maxlength': '2000'}),
+ 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.",
)
- def to_database(self, obj: DomainRequest):
- if not self.is_valid():
- return
- obj.feb_naming_requirements_details = self.cleaned_data["feb_naming_requirements_details"]
- obj.save()
-
- def is_valid(self):
- """
- Validate that details are provided when required.
- If the form is marked for deletion, bypass validation.
- """
- if self.form_data_marked_for_deletion:
- return True
-
- is_valid = super().is_valid()
- if not is_valid:
- return False
-
- # Check if the details field has content
- details = self.cleaned_data.get('feb_naming_requirements_details', '').strip()
- if not details:
- return False
-
- return True
class PurposeForm(RegistrarForm):
purpose = forms.CharField(
@@ -710,7 +659,7 @@ class PurposeForm(RegistrarForm):
],
error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
)
-
+
class OtherContactsYesNoForm(BaseYesNoForm):
"""The yes/no field for the OtherContacts form."""
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 8b20f08bc..860bd9b2c 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -1398,7 +1398,7 @@ class DomainRequest(TimeStampedModel):
if self.has_anything_else_text is None or self.has_cisa_representative is None:
has_details = False
return has_details
-
+
def is_feb(self) -> bool:
"""Is this domain request for a Federal Executive Branch agency?"""
if not self.generic_org_type:
diff --git a/src/registrar/templates/domain_request_dotgov_domain.html b/src/registrar/templates/domain_request_dotgov_domain.html
index d58815e3b..b5eff278d 100644
--- a/src/registrar/templates/domain_request_dotgov_domain.html
+++ b/src/registrar/templates/domain_request_dotgov_domain.html
@@ -2,7 +2,7 @@
{% load static field_helpers url_helpers %}
{% block form_instructions %}
-
Before requesting a .gov domain, please make sure it meets our naming requirements. Your domain name must:
+
Before requesting a .gov domain, please make sure it meets our naming requirements. Your domain name must:
Be available
Relate to your organization's name, location, and/or services
Describe how you’ll use your .gov domain. Will it be used for a website, email, or something else?
- {% block form_fields %}
- {% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
- {% input_with_errors forms.1.purpose %}
- {% endwith %}
- {% endblock %}
-{% endif %}
+ {% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
+ {% input_with_errors forms.1.purpose %}
+ {% endwith %}
+ {% endif %}
+{% endblock %}
diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py
index 35a7d76ac..4a43e70d8 100644
--- a/src/registrar/tests/test_forms.py
+++ b/src/registrar/tests/test_forms.py
@@ -14,7 +14,7 @@ from registrar.forms.domain_request_wizard import (
OtherContactsForm,
RequirementsForm,
TribalGovernmentForm,
- PurposeForm,
+ PurposeDetailsForm,
AnythingElseForm,
AboutYourOrganizationForm,
)
@@ -257,7 +257,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."
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 372861c07..2d755c636 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -184,7 +184,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
def requires_feb_questions(self) -> bool:
# TODO: this is for testing, revert later
- return False
+ return True
# return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature")
@property
@@ -754,7 +754,7 @@ class Purpose(DomainRequestWizard):
# we only care about the purpose form in this case since it's used in both instances
return purpose_details_form.is_valid()
- if not feb_purpose_options_form.id_valid():
+ if not feb_purpose_options_form.is_valid():
# Ensure details form doesn't throw errors if it's not showing
purpose_details_form.mark_form_for_deletion()
@@ -764,7 +764,7 @@ class Purpose(DomainRequestWizard):
if not feb_timeframe_yes_no_form.is_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_timeframe_details_form.mark_form_for_delation()
+ feb_timeframe_details_form.mark_form_for_deletion()
valid = all(form.is_valid() for form in forms_list if not form.form_data_marked_for_deletion)
From c821b20e8eaed6b53d0e902483d233560e581e76 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Fri, 28 Feb 2025 12:54:51 -0600
Subject: [PATCH 08/21] push remote for demo
---
.../src/js/getgov/domain-purpose-form.js | 44 +++++++++++++++
src/registrar/assets/src/js/getgov/main.js | 10 +++-
src/registrar/assets/src/js/getgov/radios.js | 56 ++++++++++++++++++-
.../forms/domainrequestwizard/purpose.py | 7 ---
.../templates/domain_request_purpose.html | 40 ++++++-------
src/registrar/views/domain_request.py | 21 +++++--
6 files changed, 138 insertions(+), 40 deletions(-)
create mode 100644 src/registrar/assets/src/js/getgov/domain-purpose-form.js
diff --git a/src/registrar/assets/src/js/getgov/domain-purpose-form.js b/src/registrar/assets/src/js/getgov/domain-purpose-form.js
new file mode 100644
index 000000000..fa13305b6
--- /dev/null
+++ b/src/registrar/assets/src/js/getgov/domain-purpose-form.js
@@ -0,0 +1,44 @@
+import { showElement } from './helpers.js';
+
+export const domain_purpose_choice_callbacks = {
+ 'new': {
+ callback: function(value, element) {
+ console.log("Callback for new")
+ //show the purpose details container
+ showElement(element);
+ // change just the text inside the em tag
+ 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.' +
+ '
' + // Adding double line break for spacing
+ 'Include any data that supports a clear public benefit or ' +
+ 'evidence user need for this new domain. ' +
+ '*';
+ },
+ element: document.getElementById('domain-purpose-details-container')
+ },
+ 'redirect': {
+ callback: function(value, element) {
+ console.log("Callback for redirect")
+ // show the purpose details container
+ showElement(element);
+ // change just the text inside the em tag
+ const labelElement = element.querySelector('.usa-label em');
+ labelElement.innerHTML = 'Explain why a redirect is necessary. ' +
+ '*';
+ },
+ element: document.getElementById('domain-purpose-details-container')
+ },
+ 'other': {
+ callback: function(value, element) {
+ console.log("Callback for other")
+ // Show the purpose details container
+ showElement(element);
+ // change just the text inside the em tag
+ const labelElement = element.querySelector('.usa-label em');
+ labelElement.innerHTML = 'Describe how this domain will be used. ' +
+ '*';
+ },
+ element: document.getElementById('domain-purpose-details-container')
+ }
+}
\ No newline at end of file
diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js
index 796e6f815..184fd05cb 100644
--- a/src/registrar/assets/src/js/getgov/main.js
+++ b/src/registrar/assets/src/js/getgov/main.js
@@ -1,4 +1,4 @@
-import { hookupYesNoListener, hookupRadioTogglerListener } from './radios.js';
+import { hookupYesNoListener, hookupCallbacksToRadioToggler } from './radios.js';
import { initDomainValidators } from './domain-validators.js';
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
import { initializeUrbanizationToggle } from './urbanization.js';
@@ -15,7 +15,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';
initDomainValidators();
initFormsetsForms();
@@ -27,6 +27,12 @@ hookupYesNoListener("additional_details-has_anything_else_text",'anything-else',
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", "domain-timeframe-details-container", null);
+hookupYesNoListener("purpose-is_interagency_initiative", "domain-interagency-initaitive-details-container", null);
+
+
initializeUrbanizationToggle();
userProfileListener();
diff --git a/src/registrar/assets/src/js/getgov/radios.js b/src/registrar/assets/src/js/getgov/radios.js
index 055bdf621..d5feb05d5 100644
--- a/src/registrar/assets/src/js/getgov/radios.js
+++ b/src/registrar/assets/src/js/getgov/radios.js
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/registrar/forms/domainrequestwizard/purpose.py b/src/registrar/forms/domainrequestwizard/purpose.py
index 23e5c28e9..52946a5e6 100644
--- a/src/registrar/forms/domainrequestwizard/purpose.py
+++ b/src/registrar/forms/domainrequestwizard/purpose.py
@@ -20,13 +20,6 @@ class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
class PurposeDetailsForm(BaseDeletableRegistrarForm):
- labels = {
- "new": "Explain why a new domain is required and why a subdomain of an existing domain doesn't meet your needs. \
- Include any data that supports a clear public benefit or evident user need for this new domain.",
- "redirect": "Explain why a redirect is necessary",
- "other": "Describe how this domain will be used",
- }
-
field_name="purpose"
purpose = forms.CharField(
diff --git a/src/registrar/templates/domain_request_purpose.html b/src/registrar/templates/domain_request_purpose.html
index bef221c4b..a7d5ddbfd 100644
--- a/src/registrar/templates/domain_request_purpose.html
+++ b/src/registrar/templates/domain_request_purpose.html
@@ -7,10 +7,6 @@
What is the purpose of your requested domain?
{% endblock %}
-{% block form_required_fields_help_text %}
-{# commented out so it does not appear on this page #}
-{% endblock %}
-
{% block form_fields %}
{% if requires_feb_questions %}
{% else %}
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 2d755c636..9ecd66191 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -745,26 +745,35 @@ class Purpose(DomainRequestWizard):
feb_initiative_details_form = forms_list[5]
if not self.requires_feb_questions():
- # if FEB questions don't apply, mark all other forms for deletion
+ # 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 form in this case since it's used in both instances
+ # we only care about the purpose details form in this case since it's used in both instances
return purpose_details_form.is_valid()
if not feb_purpose_options_form.is_valid():
# Ensure details form doesn't throw errors if it's not showing
purpose_details_form.mark_form_for_deletion()
+
+ feb_timeframe_valid = feb_timeframe_yes_no_form.is_valid()
+ feb_initiative_valid = feb_initiative_yes_no_form.is_valid()
+
+ logger.debug(f"feb timeframe yesno: {feb_timeframe_yes_no_form.cleaned_data.get('has_timeframe')}")
+ logger.debug(f"FEB initiative yesno: {feb_initiative_yes_no_form.cleaned_data.get('is_interagency_initiative')}")
- if not feb_initiative_yes_no_form.is_valid() or not feb_timeframe_yes_no_form.cleaned_data.get("has_timeframe"):
+ 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()
- if not feb_timeframe_yes_no_form.is_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_timeframe_details_form.mark_form_for_deletion()
+ for i, form in enumerate(forms_list):
+ logger.debug(f"Form {i} is marked for deletion: {form.form_data_marked_for_deletion}")
valid = all(form.is_valid() for form in forms_list if not form.form_data_marked_for_deletion)
From 4004a2f73566b5d2e2c1b1d3207b904dfebbab6c Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Mon, 3 Mar 2025 16:11:50 -0600
Subject: [PATCH 09/21] add FEB purpose questions
---
.../src/js/getgov/domain-purpose-form.js | 9 +-
src/registrar/assets/src/js/getgov/main.js | 6 +-
.../forms/domainrequestwizard/purpose.py | 24 ++--
src/registrar/models/domain_request.py | 4 +-
.../templates/domain_request_purpose.html | 38 +++---
src/registrar/tests/test_forms.py | 3 +-
src/registrar/tests/test_views_request.py | 119 ++++++++++++++++++
src/registrar/views/domain_request.py | 49 ++++----
8 files changed, 191 insertions(+), 61 deletions(-)
diff --git a/src/registrar/assets/src/js/getgov/domain-purpose-form.js b/src/registrar/assets/src/js/getgov/domain-purpose-form.js
index fa13305b6..7cde5bc35 100644
--- a/src/registrar/assets/src/js/getgov/domain-purpose-form.js
+++ b/src/registrar/assets/src/js/getgov/domain-purpose-form.js
@@ -3,7 +3,6 @@ import { showElement } from './helpers.js';
export const domain_purpose_choice_callbacks = {
'new': {
callback: function(value, element) {
- console.log("Callback for new")
//show the purpose details container
showElement(element);
// change just the text inside the em tag
@@ -15,11 +14,10 @@ export const domain_purpose_choice_callbacks = {
'evidence user need for this new domain. ' +
'*';
},
- element: document.getElementById('domain-purpose-details-container')
+ element: document.getElementById('purpose-details-container')
},
'redirect': {
callback: function(value, element) {
- console.log("Callback for redirect")
// show the purpose details container
showElement(element);
// change just the text inside the em tag
@@ -27,11 +25,10 @@ export const domain_purpose_choice_callbacks = {
labelElement.innerHTML = 'Explain why a redirect is necessary. ' +
'*';
},
- element: document.getElementById('domain-purpose-details-container')
+ element: document.getElementById('purpose-details-container')
},
'other': {
callback: function(value, element) {
- console.log("Callback for other")
// Show the purpose details container
showElement(element);
// change just the text inside the em tag
@@ -39,6 +36,6 @@ export const domain_purpose_choice_callbacks = {
labelElement.innerHTML = 'Describe how this domain will be used. ' +
'*';
},
- element: document.getElementById('domain-purpose-details-container')
+ element: document.getElementById('purpose-details-container')
}
}
\ No newline at end of file
diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js
index 184fd05cb..139c8484a 100644
--- a/src/registrar/assets/src/js/getgov/main.js
+++ b/src/registrar/assets/src/js/getgov/main.js
@@ -29,8 +29,8 @@ hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-namin
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks);
-hookupYesNoListener("purpose-has_timeframe", "domain-timeframe-details-container", null);
-hookupYesNoListener("purpose-is_interagency_initiative", "domain-interagency-initaitive-details-container", null);
+hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null);
+hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
initializeUrbanizationToggle();
@@ -56,4 +56,4 @@ initFormErrorHandling();
// Init the portfolio new member page
initPortfolioMemberPageRadio();
initPortfolioNewMemberPageToggle();
-initAddNewMemberPageListeners();
+initAddNewMemberPageListeners();
\ No newline at end of file
diff --git a/src/registrar/forms/domainrequestwizard/purpose.py b/src/registrar/forms/domainrequestwizard/purpose.py
index 52946a5e6..43dd62290 100644
--- a/src/registrar/forms/domainrequestwizard/purpose.py
+++ b/src/registrar/forms/domainrequestwizard/purpose.py
@@ -1,12 +1,17 @@
from django import forms
from django.core.validators import MaxLengthValidator
-from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm, RegistrarForm
+from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm
+
class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
field_name = "feb_purpose_choice"
- form_choices = (("new", "Used for a new website"), ("redirect", "Used as a redirect for an existing website"), ("other", "Not for a website"))
+ form_choices = (
+ ("new", "Used for a new website"),
+ ("redirect", "Used as a redirect for an existing website"),
+ ("other", "Not for a website"),
+ )
feb_purpose_choice = forms.ChoiceField(
required=True,
@@ -15,12 +20,13 @@ class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
error_messages={
"required": "This question is required.",
},
- label = "Select one"
+ label="Select one",
)
+
class PurposeDetailsForm(BaseDeletableRegistrarForm):
- field_name="purpose"
+ field_name = "purpose"
purpose = forms.CharField(
label="Purpose",
@@ -39,6 +45,7 @@ class PurposeDetailsForm(BaseDeletableRegistrarForm):
error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
)
+
class FEBTimeFrameYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
"""
Form for determining whether the domain request comes with a target timeframe for launch.
@@ -73,6 +80,7 @@ class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
error_messages={"required": "Provide details on your target timeframe."},
)
+
class FEBInteragencyInitiativeYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
"""
Form for determining whether the domain request is part of an interagency initative.
@@ -92,11 +100,7 @@ class FEBInteragencyInitiativeYesNoForm(BaseDeletableRegistrarForm, BaseYesNoFor
class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm):
interagency_initiative_details = forms.CharField(
label="interagency_initiative_details",
- widget=forms.Textarea(
- attrs={
- "aria-label": "Name the agencies that will be involved in this initiative."
- }
- ),
+ widget=forms.Textarea(attrs={"aria-label": "Name the agencies that will be involved in this initiative."}),
validators=[
MaxLengthValidator(
2000,
@@ -104,4 +108,4 @@ class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm):
)
],
error_messages={"required": "Name the agencies that will be involved in this initiative."},
- )
\ No newline at end of file
+ )
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index b45767598..b7aaff65d 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -53,7 +53,7 @@ class DomainRequest(TimeStampedModel):
def get_status_label(cls, status_name: str):
"""Returns the associated label for a given status name"""
return cls(status_name).label if status_name else None
-
+
class FEBPurposeChoices(models.TextChoices):
WEBSITE = "website"
REDIRECT = "redirect"
@@ -558,8 +558,6 @@ class DomainRequest(TimeStampedModel):
help_text="Other domain names the creator provided for consideration",
)
-
-
other_contacts = models.ManyToManyField(
"registrar.Contact",
blank=True,
diff --git a/src/registrar/templates/domain_request_purpose.html b/src/registrar/templates/domain_request_purpose.html
index a7d5ddbfd..fb5145476 100644
--- a/src/registrar/templates/domain_request_purpose.html
+++ b/src/registrar/templates/domain_request_purpose.html
@@ -7,6 +7,10 @@
What is the purpose of your requested domain?
{% endblock %}
+{% block form_required_fields_help_text %}
+{# empty this block so it doesn't show on this page #}
+{% endblock %}
+
{% block form_fields %}
{% if requires_feb_questions %}
diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py
index 4a43e70d8..03f20c862 100644
--- a/src/registrar/tests/test_forms.py
+++ b/src/registrar/tests/test_forms.py
@@ -14,10 +14,11 @@ from registrar.forms.domain_request_wizard import (
OtherContactsForm,
RequirementsForm,
TribalGovernmentForm,
- PurposeDetailsForm,
AnythingElseForm,
AboutYourOrganizationForm,
)
+from registrar.forms.domainrequestwizard.purpose import PurposeDetailsForm
+
from registrar.forms.domain import ContactForm
from registrar.forms.portfolio import (
PortfolioInvitedMemberForm,
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index 316be82cf..af4834070 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -5,6 +5,7 @@ from django.utils import timezone
from django.conf import settings
from django.urls import reverse
from api.tests.common import less_console_noise_decorator
+from registrar.utility.constants import BranchChoices
from .common import MockSESClient, completed_domain_request # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@@ -2521,6 +2522,124 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(dotgov_page, "CityofEudoraKS.gov")
self.assertNotContains(dotgov_page, "medicare.gov")
+ @less_console_noise_decorator
+ @override_flag("organization_feature", active=True)
+ def test_domain_request_dotgov_domain_FEB_questions(self):
+ """
+ Test that for a member of a federal executive branch portfolio with org feature on, the dotgov domain page
+ contains additional questions for OMB.
+ """
+ agency, _ = FederalAgency.objects.get_or_create(
+ agency="US Treasury Dept",
+ federal_type=BranchChoices.EXECUTIVE,
+ )
+
+ portfolio, _ = Portfolio.objects.get_or_create(
+ creator=self.user,
+ organization_name="Test Portfolio",
+ organization_type=Portfolio.OrganizationChoices.FEDERAL,
+ federal_agency=agency,
+ )
+
+ portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
+ user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ )
+ intro_page = self.app.get(reverse("domain-request:start"))
+ # django-webtest does not handle cookie-based sessions well because it keeps
+ # resetting the session key on each new request, thus destroying the concept
+ # of a "session". We are going to do it manually, saving the session ID here
+ # and then setting the cookie on each request.
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+
+ intro_form = intro_page.forms[0]
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ intro_result = intro_form.submit()
+
+ # follow first redirect
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ portfolio_requesting_entity = intro_result.follow()
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+
+ # ---- REQUESTING ENTITY PAGE ----
+ requesting_entity_form = portfolio_requesting_entity.forms[0]
+ requesting_entity_form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = False
+
+ # test next button
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ requesting_entity_result = requesting_entity_form.submit()
+
+ # ---- CURRENT SITES PAGE ----
+ # Follow the redirect to the next form page
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ current_sites_page = requesting_entity_result.follow()
+ current_sites_form = current_sites_page.forms[0]
+ current_sites_form["current_sites-0-website"] = "www.treasury.com"
+
+ # test saving the page
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ current_sites_result = current_sites_form.submit()
+
+ # ---- DOTGOV DOMAIN PAGE ----
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ dotgov_page = current_sites_result.follow()
+
+ # separate out these tests for readability
+ self.feb_dotgov_domain_tests(dotgov_page)
+
+ # Now proceed with the actual test
+ domain_form = dotgov_page.forms[0]
+ domain_form["dotgov_domain-requested_domain"] = "test.gov"
+ domain_form["dotgov_domain-feb_naming_requirements"] = "True"
+
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ domain_result = domain_form.submit()
+
+ # ---- PURPOSE PAGE ----
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ purpose_page = domain_result.follow()
+
+ self.feb_purpose_page_tests(purpose_page)
+
+ def feb_purpose_page_tests(self, purpose_page):
+ self.assertContains(purpose_page, "What is the purpose of your requested domain?")
+
+ # Make sure the purpose selector form is present
+ self.assertContains(purpose_page, "feb_purpose_choice")
+
+ # Make sure the purpose details form is present
+ self.assertContains(purpose_page, "purpose-details")
+
+ # Make sure the timeframe yes/no form is present
+ self.assertContains(purpose_page, "purpose-has_timeframe")
+
+ # Make sure the timeframe details form is present
+ self.assertContains(purpose_page, "purpose-time_frame_details")
+
+ # Make sure the interagency initiative yes/no form is present
+ self.assertContains(purpose_page, "purpose-is_interagency_initiative")
+
+ # Make sure the interagency initiative details form is present
+ self.assertContains(purpose_page, "purpose-interagency_initiative_details")
+
+ def feb_dotgov_domain_tests(self, dotgov_page):
+ # Make sure the dynamic example content doesn't show
+ self.assertNotContains(dotgov_page, "medicare.gov")
+
+ # Make sure the link at the top directs to OPM FEB guidance
+ self.assertContains(dotgov_page, "https://get.gov/domains/executive-branch-guidance/")
+
+ # Check for header of first FEB form
+ self.assertContains(dotgov_page, "Does this submission meet each domain naming requirement?")
+
+ # Check for label of second FEB form
+ self.assertContains(dotgov_page, "Provide details below")
+
+ # Check that the yes/no form was included
+ self.assertContains(dotgov_page, "feb_naming_requirements")
+
+ # Check that the details form was included
+ self.assertContains(dotgov_page, "feb_naming_requirements_details")
+
@less_console_noise_decorator
def test_domain_request_formsets(self):
"""Users are able to add more than one of some fields."""
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 9ecd66191..60ebac594 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -183,9 +183,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
return PortfolioDomainRequestStep if self.is_portfolio else Step
def requires_feb_questions(self) -> bool:
- # TODO: this is for testing, revert later
- return True
- # return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature")
+ return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature")
@property
def prefix(self):
@@ -713,19 +711,20 @@ class DotgovDomain(DomainRequestWizard):
class Purpose(DomainRequestWizard):
template_name = "domain_request_purpose.html"
- forms = [purpose.FEBPurposeOptionsForm,
- purpose.PurposeDetailsForm,
- purpose.FEBTimeFrameYesNoForm,
- purpose.FEBTimeFrameDetailsForm,
- purpose.FEBInteragencyInitiativeYesNoForm,
- purpose.FEBInteragencyInitiativeDetailsForm
- ]
+ forms = [
+ purpose.FEBPurposeOptionsForm,
+ purpose.PurposeDetailsForm,
+ purpose.FEBTimeFrameYesNoForm,
+ purpose.FEBTimeFrameDetailsForm,
+ purpose.FEBInteragencyInitiativeYesNoForm,
+ purpose.FEBInteragencyInitiativeDetailsForm,
+ ]
def get_context_data(self):
- context= super().get_context_data()
+ context = super().get_context_data()
context["requires_feb_questions"] = self.requires_feb_questions()
return context
-
+
def is_valid(self, forms_list: list) -> bool:
"""
Expected order of forms_list:
@@ -753,17 +752,29 @@ class Purpose(DomainRequestWizard):
feb_initiative_details_form.mark_form_for_deletion()
# we only care about the purpose details form in this case since it's used in both instances
return purpose_details_form.is_valid()
-
- if not feb_purpose_options_form.is_valid():
+
+ if feb_purpose_options_form.is_valid():
+ option = feb_purpose_options_form.cleaned_data.get("feb_purpose_choice")
+ if option == "new":
+ purpose_details_form.fields["purpose"].error_messages = {
+ "required": "Explain why a new domain is required."
+ }
+ elif option == "redirect":
+ purpose_details_form.fields["purpose"].error_messages = {
+ "required": "Explain why a redirect is needed."
+ }
+ elif option == "other":
+ purpose_details_form.fields["purpose"].error_messages = {
+ "required": "Provide details on how this domain will be used."
+ }
+ # If somehow none of these are true use the default error message
+ else:
# Ensure details form doesn't throw errors if it's not showing
purpose_details_form.mark_form_for_deletion()
feb_timeframe_valid = feb_timeframe_yes_no_form.is_valid()
feb_initiative_valid = feb_initiative_yes_no_form.is_valid()
- logger.debug(f"feb timeframe yesno: {feb_timeframe_yes_no_form.cleaned_data.get('has_timeframe')}")
- logger.debug(f"FEB initiative yesno: {feb_initiative_yes_no_form.cleaned_data.get('is_interagency_initiative')}")
-
if not feb_timeframe_valid or not feb_timeframe_yes_no_form.cleaned_data.get("has_timeframe"):
# Ensure details form doesn't throw errors if it's not showing
feb_timeframe_details_form.mark_form_for_deletion()
@@ -772,15 +783,11 @@ class Purpose(DomainRequestWizard):
# Ensure details form doesn't throw errors if it's not showing
feb_initiative_details_form.mark_form_for_deletion()
- for i, form in enumerate(forms_list):
- logger.debug(f"Form {i} is marked for deletion: {form.form_data_marked_for_deletion}")
-
valid = all(form.is_valid() for form in forms_list if not form.form_data_marked_for_deletion)
return valid
-
class OtherContacts(DomainRequestWizard):
template_name = "domain_request_other_contacts.html"
forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]
From 470e05a7b6bf033f9602dcbbdd093b49114e456f Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Mon, 3 Mar 2025 16:25:42 -0600
Subject: [PATCH 10/21] test fix
---
src/registrar/tests/test_views_request.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index af4834070..abb9d4e77 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -2414,7 +2414,7 @@ class DomainRequestTests(TestWithUser, WebTest):
so_page = org_contact_result.follow()
self.assertContains(so_page, "Domain requests from cities")
- # @less_console_noise_decorator
+ @less_console_noise_decorator
def test_domain_request_dotgov_domain_dynamic_text(self):
intro_page = self.app.get(reverse("domain-request:start"))
# django-webtest does not handle cookie-based sessions well because it keeps
@@ -2497,7 +2497,6 @@ class DomainRequestTests(TestWithUser, WebTest):
# ---- DOTGOV DOMAIN PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
dotgov_page = current_sites_result.follow()
- print(dotgov_page)
self.assertContains(dotgov_page, "medicare.gov")
From 34dd8511e29e981a6edd6f33fa8b6b7e2a3a811c Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Mon, 3 Mar 2025 17:12:28 -0600
Subject: [PATCH 11/21] test fixes
---
src/registrar/tests/test_admin_request.py | 5 +++++
src/registrar/tests/test_views_request.py | 9 +++++++--
src/registrar/views/domain_request.py | 1 -
3 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py
index 9fb415941..9320dd3d3 100644
--- a/src/registrar/tests/test_admin_request.py
+++ b/src/registrar/tests/test_admin_request.py
@@ -1983,7 +1983,12 @@ class TestDomainRequestAdmin(MockEppLib):
"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",
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index 2f3c87207..d2a791969 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -54,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):
@@ -2547,7 +2548,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(dotgov_page, "CityofEudoraKS.gov")
self.assertNotContains(dotgov_page, "medicare.gov")
- @less_console_noise_decorator
+ # @less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_domain_request_dotgov_domain_FEB_questions(self):
"""
@@ -2613,8 +2614,10 @@ class DomainRequestTests(TestWithUser, WebTest):
# Now proceed with the actual test
domain_form = dotgov_page.forms[0]
- domain_form["dotgov_domain-requested_domain"] = "test.gov"
+ domain_form["dotgov_domain-requested_domain"] = "asdffhgjkl.gov"
domain_form["dotgov_domain-feb_naming_requirements"] = "True"
+ domain_form["dotgov_domain-feb_naming_requirements_details"] = "test"
+ print(domain_form.fields)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
domain_result = domain_form.submit()
@@ -2622,9 +2625,11 @@ class DomainRequestTests(TestWithUser, WebTest):
# ---- PURPOSE PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_page = domain_result.follow()
+ print(purpose_page.forms[0].fields)
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?")
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index f4bb877d6..55cd9a19e 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -233,7 +233,6 @@ class DomainRequestWizard(TemplateView):
self._domain_request.generic_org_type = portfolio.organization_type
self._domain_request.save()
if portfolio and not self._domain_request.federal_type:
- logger.debug(f"Setting fed type to {portfolio.federal_type}")
self._domain_request.federal_type = portfolio.federal_type
self._domain_request.save()
else:
From 9ddf7a6cd04dc5dbb6dd3fb06cc6bedceaef1617 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 4 Mar 2025 13:27:41 -0600
Subject: [PATCH 12/21] test fixes
---
src/registrar/forms/domain_request_wizard.py | 3 +++
src/registrar/tests/test_views_request.py | 11 +++++------
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 4e7182843..7cbb159b4 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -599,6 +599,9 @@ class DotGovDomainForm(RegistrarForm):
return_type=ValidationReturnType.FORM_VALIDATION_ERROR,
)
return validated
+
+ def is_valid(self):
+ return super().is_valid()
requested_domain = forms.CharField(
label="What .gov domain do you want?",
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index d2a791969..7b117f67d 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -2614,18 +2614,17 @@ class DomainRequestTests(TestWithUser, WebTest):
# Now proceed with the actual test
domain_form = dotgov_page.forms[0]
- domain_form["dotgov_domain-requested_domain"] = "asdffhgjkl.gov"
+ 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"
- print(domain_form.fields)
-
- self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
- domain_result = domain_form.submit()
+ 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()
- print(purpose_page.forms[0].fields)
self.feb_purpose_page_tests(purpose_page)
From 2c56ea480f6b0e3e3b98a6285fec671e9550b43d Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 6 Mar 2025 09:15:48 -0800
Subject: [PATCH 13/21] Make requested content changes
---
src/registrar/templates/domain_base.html | 2 +-
src/registrar/templates/domain_detail.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/templates/domain_base.html b/src/registrar/templates/domain_base.html
index 58038d0a4..3451157b5 100644
--- a/src/registrar/templates/domain_base.html
+++ b/src/registrar/templates/domain_base.html
@@ -58,7 +58,7 @@
{% if request.path|endswith:"renewal"%}
Renew {{domain.name}}
{%else%}
-
Domain Overview
+
Domain overview
{% endif%}
{% endblock %} {# domain_content #}
diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html
index eba0eaf85..a0d477249 100644
--- a/src/registrar/templates/domain_detail.html
+++ b/src/registrar/templates/domain_detail.html
@@ -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 %}
From b09f68946f61ce7cb465119e4279f1c22c8099e0 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 11 Mar 2025 10:48:54 -0500
Subject: [PATCH 14/21] review changes
---
...equest_feb_naming_requirements_and_more.py | 29 +++++++++++++-
...mainrequest_feb_purpose_choice_and_more.py | 40 -------------------
src/registrar/models/domain_request.py | 8 +---
src/registrar/views/domain_request.py | 4 --
4 files changed, 30 insertions(+), 51 deletions(-)
delete mode 100644 src/registrar/migrations/0142_domainrequest_feb_purpose_choice_and_more.py
diff --git a/src/registrar/migrations/0141_domainrequest_feb_naming_requirements_and_more.py b/src/registrar/migrations/0141_domainrequest_feb_naming_requirements_and_more.py
index 32634d8ee..ad29e57d0 100644
--- a/src/registrar/migrations/0141_domainrequest_feb_naming_requirements_and_more.py
+++ b/src/registrar/migrations/0141_domainrequest_feb_naming_requirements_and_more.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.17 on 2025-02-13 22:41
+# Generated by Django 4.2.17 on 2025-03-10 19:55
from django.db import migrations, models
@@ -20,4 +20,31 @@ class Migration(migrations.Migration):
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),
+ ),
]
diff --git a/src/registrar/migrations/0142_domainrequest_feb_purpose_choice_and_more.py b/src/registrar/migrations/0142_domainrequest_feb_purpose_choice_and_more.py
deleted file mode 100644
index 1cd5fb587..000000000
--- a/src/registrar/migrations/0142_domainrequest_feb_purpose_choice_and_more.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Generated by Django 4.2.17 on 2025-02-25 23:45
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("registrar", "0141_domainrequest_feb_naming_requirements_and_more"),
- ]
-
- operations = [
- 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),
- ),
- ]
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index b7aaff65d..ac1adfad5 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -1436,13 +1436,9 @@ class DomainRequest(TimeStampedModel):
def is_feb(self) -> bool:
"""Is this domain request for a Federal Executive Branch agency?"""
- if not self.generic_org_type:
- # generic_org_type is either blank or None, assume no
- return False
- if self.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
- return self.federal_type == BranchChoices.EXECUTIVE
+ 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?
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 55cd9a19e..33de99c39 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -232,9 +232,6 @@ class DomainRequestWizard(TemplateView):
if portfolio and not self._domain_request.generic_org_type:
self._domain_request.generic_org_type = portfolio.organization_type
self._domain_request.save()
- if portfolio and not self._domain_request.federal_type:
- self._domain_request.federal_type = portfolio.federal_type
- self._domain_request.save()
else:
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
return self._domain_request
@@ -474,7 +471,6 @@ class DomainRequestWizard(TemplateView):
"requested_domain__name": requested_domain_name,
}
context["domain_request_id"] = self.domain_request.id
- context["is_executive"] = self.domain_request.is_federal() and self.domain_request.federal_type == "Executive"
return context
def get_step_list(self) -> list:
From ba631dd857202836abd7359571f1049af5a6c9f5 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 11 Mar 2025 10:51:19 -0500
Subject: [PATCH 15/21] small review fix
---
src/registrar/forms/domain_request_wizard.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 7cbb159b4..4e7182843 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -599,9 +599,6 @@ class DotGovDomainForm(RegistrarForm):
return_type=ValidationReturnType.FORM_VALIDATION_ERROR,
)
return validated
-
- def is_valid(self):
- return super().is_valid()
requested_domain = forms.CharField(
label="What .gov domain do you want?",
From b41e706dcdd95a822f5451929db5b440149e3ce0 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 11 Mar 2025 15:16:36 -0500
Subject: [PATCH 16/21] review changes
---
src/registrar/forms/domain_request_wizard.py | 35 ++++--------
.../purpose.py => feb.py} | 56 +++++++++++--------
src/registrar/views/domain_request.py | 14 ++---
3 files changed, 52 insertions(+), 53 deletions(-)
rename src/registrar/forms/{domainrequestwizard/purpose.py => feb.py} (75%)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 4e7182843..91fe7ba05 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -607,38 +607,25 @@ class DotGovDomainForm(RegistrarForm):
},
)
+class PurposeDetailsForm(BaseDeletableRegistrarForm):
-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 = "purpose"
- 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.")},
+ purpose = forms.CharField(
+ label="Purpose",
+ widget=forms.Textarea(
+ attrs={
+ "aria-label": "What is the purpose of your requested domain? Describe how you’ll use your .gov domain. \
+ Will it be used for a website, email, or something else?"
+ }
+ ),
validators=[
MaxLengthValidator(
2000,
message="Response must be less than 2000 characters.",
)
],
- label="",
- help_text="Maximum 2000 characters allowed.",
+ error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
)
diff --git a/src/registrar/forms/domainrequestwizard/purpose.py b/src/registrar/forms/feb.py
similarity index 75%
rename from src/registrar/forms/domainrequestwizard/purpose.py
rename to src/registrar/forms/feb.py
index 43dd62290..44fd417c2 100644
--- a/src/registrar/forms/domainrequestwizard/purpose.py
+++ b/src/registrar/forms/feb.py
@@ -3,6 +3,40 @@ 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"
@@ -24,28 +58,6 @@ class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
)
-class PurposeDetailsForm(BaseDeletableRegistrarForm):
-
- field_name = "purpose"
-
- purpose = forms.CharField(
- label="Purpose",
- widget=forms.Textarea(
- attrs={
- "aria-label": "What is the purpose of your requested domain? Describe how you’ll use your .gov domain. \
- Will it be used for a website, email, or something else?"
- }
- ),
- validators=[
- MaxLengthValidator(
- 2000,
- message="Response must be less than 2000 characters.",
- )
- ],
- error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
- )
-
-
class FEBTimeFrameYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
"""
Form for determining whether the domain request comes with a target timeframe for launch.
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 33de99c39..b659b0812 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -15,7 +15,7 @@ from registrar.decorators import (
grant_access,
)
from registrar.forms import domain_request_wizard as forms
-from registrar.forms.domainrequestwizard import purpose
+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
@@ -707,12 +707,12 @@ class Purpose(DomainRequestWizard):
template_name = "domain_request_purpose.html"
forms = [
- purpose.FEBPurposeOptionsForm,
- purpose.PurposeDetailsForm,
- purpose.FEBTimeFrameYesNoForm,
- purpose.FEBTimeFrameDetailsForm,
- purpose.FEBInteragencyInitiativeYesNoForm,
- purpose.FEBInteragencyInitiativeDetailsForm,
+ feb.FEBPurposeOptionsForm,
+ forms.PurposeDetailsForm,
+ feb.FEBTimeFrameYesNoForm,
+ feb.FEBTimeFrameDetailsForm,
+ feb.FEBInteragencyInitiativeYesNoForm,
+ feb.FEBInteragencyInitiativeDetailsForm,
]
def get_context_data(self):
From 95cd22d54927fbc5efb5d50051229643a72795b4 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 11 Mar 2025 15:34:25 -0500
Subject: [PATCH 17/21] linter and merge fixes
---
src/registrar/assets/src/js/getgov/main.js | 1 -
src/registrar/forms/domain_request_wizard.py | 1 +
src/registrar/models/domain_request.py | 1 +
src/registrar/tests/test_views_request.py | 5 +++--
src/registrar/views/domain_request.py | 4 ++--
5 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js
index 6995b195d..f077448aa 100644
--- a/src/registrar/assets/src/js/getgov/main.js
+++ b/src/registrar/assets/src/js/getgov/main.js
@@ -1,5 +1,4 @@
import { hookupYesNoListener, hookupCallbacksToRadioToggler } from './radios.js';
-import { hookupYesNoListener } from './radios.js';
import { initDomainValidators } from './domain-validators.js';
import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js';
import { initFormNameservers } from './form-nameservers'
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 91fe7ba05..a9c9f6f03 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -607,6 +607,7 @@ class DotGovDomainForm(RegistrarForm):
},
)
+
class PurposeDetailsForm(BaseDeletableRegistrarForm):
field_name = "purpose"
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index ac1adfad5..5ae51ebc6 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -1439,6 +1439,7 @@ class DomainRequest(TimeStampedModel):
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?
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index 7b117f67d..caeeafc21 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -2618,7 +2618,9 @@ class DomainRequestTests(TestWithUser, WebTest):
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
+ 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()
@@ -2628,7 +2630,6 @@ class DomainRequestTests(TestWithUser, WebTest):
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?")
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index b659b0812..3c395108b 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -660,8 +660,8 @@ class DotgovDomain(DomainRequestWizard):
forms = [
forms.DotGovDomainForm,
forms.AlternativeDomainFormSet,
- forms.ExecutiveNamingRequirementsYesNoForm,
- forms.ExecutiveNamingRequirementsDetailsForm,
+ feb.ExecutiveNamingRequirementsYesNoForm,
+ feb.ExecutiveNamingRequirementsDetailsForm,
]
def get_context_data(self):
From 951fe6610525c75d0ab990eff283f1a6e498ad83 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 11 Mar 2025 16:04:14 -0500
Subject: [PATCH 18/21] fix odd spacing problem
---
src/registrar/templates/domain_request_purpose.html | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/registrar/templates/domain_request_purpose.html b/src/registrar/templates/domain_request_purpose.html
index fb5145476..9c6754f22 100644
--- a/src/registrar/templates/domain_request_purpose.html
+++ b/src/registrar/templates/domain_request_purpose.html
@@ -4,7 +4,6 @@
{% block form_instructions %}
.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).