merge latest

This commit is contained in:
zandercymatics 2025-03-25 08:20:18 -06:00
commit feed27153e
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
13 changed files with 376 additions and 37 deletions

View file

@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
django = "4.2.17"
django = "4.2.20"
cfenv = "*"
django-cors-headers = "*"
pycryptodomex = "*"
@ -34,6 +34,7 @@ tblib = "*"
django-admin-multiple-choice-list-filter = "*"
django-import-export = "*"
django-waffle = "*"
cryptography = "*"
[dev-packages]
django-debug-toolbar = "*"

21
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "07f7bc9bda4099f96b18f8f063b487b121b82ae01de06a7f2e9013d56098a421"
"sha256": "c854531923af84e93b0b26e64a0bf3b9d9c12870c4795b1afb667569ea740e2b"
},
"pipfile-spec": 6,
"requires": {},
@ -281,6 +281,7 @@
"sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7",
"sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"
],
"index": "pypi",
"markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'",
"version": "==44.0.2"
},
@ -316,12 +317,12 @@
},
"django": {
"hashes": [
"sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0",
"sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"
"sha256:213381b6e4405f5c8703fffc29cd719efdf189dec60c67c04f76272b3dc845b9",
"sha256:92bac5b4432a64532abb73b2ac27203f485e40225d2640a7fbef2b62b876e789"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.2.17"
"version": "==4.2.20"
},
"django-admin-multiple-choice-list-filter": {
"hashes": [
@ -1389,11 +1390,11 @@
},
"botocore-stubs": {
"hashes": [
"sha256:937c9b787e4f784019f321fa1d88a505965c25f425e810bde45e23b7ca564282",
"sha256:bb9a9e7cd2f48ecb429a7d0df0387f63399db8fb363bdfa38eba285854d622a2"
"sha256:b9c3a1e8fb57fb70b49aa5380cabefab32ec028d8a1d8f5ac83dd836c5b429a8",
"sha256:c6cb18979a86db311a365448b67e4a492a530c3f4fb313432d41deaee6268b95"
],
"markers": "python_version >= '3.8'",
"version": "==1.37.18"
"version": "==1.37.17"
},
"click": {
"hashes": [
@ -1405,12 +1406,12 @@
},
"django": {
"hashes": [
"sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0",
"sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"
"sha256:213381b6e4405f5c8703fffc29cd719efdf189dec60c67c04f76272b3dc845b9",
"sha256:92bac5b4432a64532abb73b2ac27203f485e40225d2640a7fbef2b62b876e789"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.2.17"
"version": "==4.2.20"
},
"django-debug-toolbar": {
"hashes": [

View file

@ -28,6 +28,8 @@ initFormNameservers();
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees');
hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null);
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);
hookupYesNoListener("portfolio_additional_details-working_with_eop", "eop-contact-container", null);
hookupYesNoListener("portfolio_additional_details-has_anything_else_text", 'anything-else-details-container', null);
hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container");
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks);
@ -35,7 +37,6 @@ hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choic
hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null);
hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
initializeUrbanizationToggle();
userProfileListener();

View file

@ -615,7 +615,8 @@ class PurposeDetailsForm(BaseDeletableRegistrarForm):
label="Purpose",
widget=forms.Textarea(
attrs={
"aria-label": "What is the purpose of your requested domain? Describe how youll use your .gov domain. \
"aria-label": "What is the purpose of your requested domain? \
Describe how youll use your .gov domain. \
Will it be used for a website, email, or something else?"
}
),
@ -921,6 +922,7 @@ class AnythingElseYesNoForm(BaseYesNoForm):
class RequirementsForm(RegistrarForm):
is_policy_acknowledged = forms.BooleanField(
label="I read and agree to the requirements for operating a .gov domain.",
error_messages={

View file

@ -121,3 +121,83 @@ class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm):
],
error_messages={"required": "Name the agencies that will be involved in this initiative."},
)
class WorkingWithEOPYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
"""
Form for determining if the Federal Executive Branch (FEB) agency is working with the
Executive Office of the President (EOP) on the domain request.
"""
field_name = "working_with_eop"
@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.working_with_eop
class EOPContactForm(BaseDeletableRegistrarForm):
"""
Form for contact information of the representative of the
Executive Office of the President (EOP) that the Federal
Executive Branch (FEB) agency is working with.
"""
first_name = forms.CharField(
label="First name / given name",
error_messages={"required": "Enter the first name / given name of this contact."},
required=True,
)
last_name = forms.CharField(
label="Last name / family name",
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):
# This function overrides the behavior of the BaseDeletableRegistrarForm.
# in order to preserve deletable functionality, we need to call the
# superclass's to_database method if the form is marked for deletion.
if self.form_data_marked_for_deletion:
super().to_database(obj)
return
if not self.is_valid():
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()
class FEBAnythingElseYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm):
"""Yes/no toggle for the anything else question on additional details"""
form_is_checked = property(lambda self: self.domain_request.has_anything_else_text) # type: ignore
field_name = "has_anything_else_text"

View file

@ -0,0 +1,33 @@
# Generated by Django 4.2.17 on 2025-03-17 20:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0143_create_groups_v18"),
]
operations = [
migrations.AddField(
model_name="domainrequest",
name="eop_stakeholder_email",
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name="EOP Stakeholder Email"),
),
migrations.AddField(
model_name="domainrequest",
name="eop_stakeholder_first_name",
field=models.CharField(blank=True, null=True, verbose_name="EOP Stakeholder 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"),
),
migrations.AddField(
model_name="domainrequest",
name="working_with_eop",
field=models.BooleanField(blank=True, null=True),
),
]

View file

@ -523,6 +523,29 @@ class DomainRequest(TimeStampedModel):
choices=FEBPurposeChoices.choices,
)
working_with_eop = models.BooleanField(
null=True,
blank=True,
)
eop_stakeholder_first_name = models.CharField(
null=True,
blank=True,
verbose_name="EOP Stakeholder 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",
)
# 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.

View file

@ -4,7 +4,7 @@
{% block form_instructions %}
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating a .gov domain.</p>
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we dont review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we dont review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
<h2>What you cant do with a .gov domain</h2>
@ -52,20 +52,41 @@
<p>.Gov domains are registered for a one-year period. To renew your domain, you'll be asked to verify your organizations eligibility and your contact information. </p>
<p>Though a domain may expire, it will not automatically be put on hold or deleted. Well make extensive efforts to contact your organization before holding or deleting a domain.</p>
{% endblock %}
{% 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 %}
<h2>Required and prohibited activities</h2>
<h3>Prohibitions on non-governmental use</h3>
{% block form_required_fields_help_text %}
{# commented out so it does not appear on this page #}
{% endblock %}
{% block form_fields %}
<fieldset class="usa-fieldset">
<legend>
<h2>Acknowledgement of .gov domain requirements</h2>
</legend>
<p>Agencies may not use a .gov domain name:
<ul class="usa-list">
<li>On behalf of a non-federal executive branch entity</li>
<li>For a non-governmental purpose</li>
</ul>
</p>
<h3>Compliance with the 21st Century IDEA Act is required</h3>
<p>As required by the DOTGOV Act, agencies must ensure
that any website or digital service that uses a .gov
domain name is in compliance with the
<a href="https://digital.gov/resources/delivering-digital-first-public-experience-act/" target="_blank" rel="noopener noreferrer">21st Century Integrated Digital Experience Act</a>.
and
<a href="https://bidenwhitehouse.gov/wp-content/uploads/2023/09/M-23-22-Delivering-a-Digital-First-Public-Experience.pdf" target="_blank" rel="noopener noreferrer">Guidance for Agencies</a>.
</p>
<h2>Acknowledgement of .gov domain requirements</h2>
{% input_with_errors forms.0.is_policy_acknowledged %}
{% else %}
<fieldset class="usa-fieldset">
<legend>
<h2>Acknowledgement of .gov domain requirements</h2>
</legend>
</fieldset>
{% input_with_errors forms.0.is_policy_acknowledged %}
</fieldset>
{% endif %}
{% endblock %}

View file

@ -6,16 +6,60 @@
{% endblock %}
{% block form_fields %}
{% if requires_feb_questions %}
{{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 %}
<fieldset class="usa-fieldset">
<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>
</fieldset>
<div id="anything-else">
<p><em>This question is optional.</em></p>
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.anything_else %}
{% endwith %}
</div>
<div 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>
{% endif %}
{% endblock %}

View file

@ -2060,6 +2060,10 @@ class TestDomainRequestAdmin(MockEppLib):
"feb_naming_requirements",
"feb_naming_requirements_details",
"feb_purpose_choice",
"working_with_eop",
"eop_stakeholder_first_name",
"eop_stakeholder_last_name",
"eop_stakeholder_email",
"purpose",
"has_timeframe",
"time_frame_details",

View file

@ -2550,7 +2550,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# @less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_domain_request_dotgov_domain_FEB_questions(self):
def test_domain_request_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.
@ -2612,7 +2612,6 @@ class DomainRequestTests(TestWithUser, WebTest):
# separate out these tests for readability
self.feb_dotgov_domain_tests(dotgov_page)
# Now proceed with the actual test
domain_form = dotgov_page.forms[0]
domain = "test.gov"
domain_form["dotgov_domain-requested_domain"] = domain
@ -2630,6 +2629,36 @@ class DomainRequestTests(TestWithUser, WebTest):
self.feb_purpose_page_tests(purpose_page)
purpose_form = purpose_page.forms[0]
purpose_form["purpose-feb_purpose_choice"] = "redirect"
purpose_form["purpose-purpose"] = "test"
purpose_form["purpose-has_timeframe"] = "True"
purpose_form["purpose-time_frame_details"] = "test"
purpose_form["purpose-is_interagency_initiative"] = "True"
purpose_form["purpose-interagency_initiative_details"] = "test"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_result = purpose_form.submit()
# ---- ADDITIONAL DETAILS PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_page = purpose_result.follow()
self.feb_additional_details_page_tests(additional_details_page)
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-has_anything_else_text"] = "True"
additional_details_form["portfolio_additional_details-anything_else"] = "test"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_result = additional_details_form.submit()
# ---- REQUIREMENTS PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
requirements_page = additional_details_result.follow()
self.feb_requirements_page_tests(requirements_page)
def feb_purpose_page_tests(self, purpose_page):
self.assertContains(purpose_page, "What is the purpose of your requested domain?")
@ -2670,6 +2699,40 @@ class DomainRequestTests(TestWithUser, WebTest):
# Check that the details form was included
self.assertContains(dotgov_page, "feb_naming_requirements_details")
def feb_additional_details_page_tests(self, additional_details_page):
test_text = "Are you working with someone in the Executive Office of the President (EOP) on this request?"
self.assertContains(additional_details_page, test_text)
# Make sure the EOP form is present
self.assertContains(additional_details_page, "working_with_eop")
# Make sure the EOP contact form is present
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")
self.assertContains(additional_details_page, "additional_details-anything_else")
def feb_requirements_page_tests(self, requirements_page):
# Check for the 21st Century IDEA Act links
self.assertContains(
requirements_page, "https://digital.gov/resources/delivering-digital-first-public-experience-act/"
)
self.assertContains(
requirements_page,
"https://bidenwhitehouse.gov/wp-content/uploads/2023/09/M-23-22-Delivering-a-Digital-First-Public-Experience.pdf", # noqa
)
# Check for the policy acknowledgement form
self.assertContains(requirements_page, "is_policy_acknowledged")
self.assertContains(
requirements_page,
"I read and understand the guidance outlined in the DOTGOV Act for operating a .gov domain.",
)
@less_console_noise_decorator
def test_domain_request_formsets(self):
"""Users are able to add more than one of some fields."""

View file

@ -603,7 +603,49 @@ class RequestingEntity(DomainRequestWizard):
class PortfolioAdditionalDetails(DomainRequestWizard):
template_name = "portfolio_domain_request_additional_details.html"
forms = [forms.PortfolioAnythingElseForm]
forms = [
feb.WorkingWithEOPYesNoForm,
feb.EOPContactForm,
feb.FEBAnythingElseYesNoForm,
forms.PortfolioAnythingElseForm,
]
def get_context_data(self):
context = super().get_context_data()
context["requires_feb_questions"] = self.requires_feb_questions()
return context
def is_valid(self, forms: list) -> bool:
"""
Validates the forms for portfolio additional details.
Expected order of forms_list:
0: WorkingWithEOPYesNoForm
1: EOPContactForm
2: FEBAnythingElseYesNoForm
3: PortfolioAnythingElseForm
"""
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
forms[1].mark_form_for_deletion()
eop_forms_valid = False
if forms[0].cleaned_data.get("working_with_eop"):
eop_forms_valid = forms[1].is_valid()
else:
forms[1].mark_form_for_deletion()
anything_else_forms_valid = True
if not forms[2].is_valid():
forms[3].mark_form_for_deletion()
anything_else_forms_valid = False
if forms[2].cleaned_data.get("has_anything_else_text"):
forms[3].fields["anything_else"].required = True
forms[3].fields["anything_else"].error_messages[
"required"
] = "Please provide additional details you'd like us to know. \
If you have nothing to add, select 'No'."
anything_else_forms_valid = forms[3].is_valid()
return eop_forms_valid and anything_else_forms_valid
# Non-portfolio pages
@ -887,6 +929,29 @@ class Requirements(DomainRequestWizard):
template_name = "domain_request_requirements.html"
forms = [forms.RequirementsForm]
def get_context_data(self):
context = super().get_context_data()
context["requires_feb_questions"] = self.requires_feb_questions()
return context
# Override the get_forms method to set the policy acknowledgement label conditionally based on feb status
def get_forms(self, step=None, use_post=False, use_db=False, files=None):
forms_list = super().get_forms(step, use_post, use_db, files)
# Pass the is_federal context to the form
for form in forms_list:
if isinstance(form, forms.RequirementsForm):
if self.requires_feb_questions():
form.fields["is_policy_acknowledged"].label = (
"I read and understand the guidance outlined in the DOTGOV Act for operating a .gov domain." # noqa: E501
)
else:
form.fields["is_policy_acknowledged"].label = (
"I read and agree to the requirements for operating a .gov domain." # noqa: E501
)
return forms_list
class Review(DomainRequestWizard):
template_name = "domain_request_review.html"
@ -899,6 +964,7 @@ class Review(DomainRequestWizard):
context = super().get_context_data()
context["Step"] = self.get_step_enum().__members__
context["domain_request"] = self.domain_request
context["requires_feb_questions"] = self.requires_feb_questions()
return context
def goto_next_step(self):

View file

@ -13,7 +13,7 @@ defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1,
diff-match-patch==20241021; python_version >= '3.7'
dj-database-url==2.3.0
dj-email-url==1.0.6
django==4.2.17; python_version >= '3.8'
django==4.2.20; python_version >= '3.8'
django-admin-multiple-choice-list-filter==0.1.1
django-allow-cidr==0.7.1
django-auditlog==3.0.0; python_version >= '3.8'