diff --git a/src/Pipfile b/src/Pipfile index 07b1db715..819481fa7 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -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 = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 6cec6c03a..914a217d0 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -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": [ diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js index f077448aa..933fe2757 100644 --- a/src/registrar/assets/src/js/getgov/main.js +++ b/src/registrar/assets/src/js/getgov/main.js @@ -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(); diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 779100f73..a7d487b40 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -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 you’ll use your .gov domain. \ + "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?" } ), @@ -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={ diff --git a/src/registrar/forms/feb.py b/src/registrar/forms/feb.py index 44fd417c2..2dabbff0d 100644 --- a/src/registrar/forms/feb.py +++ b/src/registrar/forms/feb.py @@ -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" diff --git a/src/registrar/migrations/0144_domainrequest_eop_stakeholder_email_and_more.py b/src/registrar/migrations/0144_domainrequest_eop_stakeholder_email_and_more.py new file mode 100644 index 000000000..b23b6c107 --- /dev/null +++ b/src/registrar/migrations/0144_domainrequest_eop_stakeholder_email_and_more.py @@ -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), + ), + ] diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 2a4541542..d68a29ab1 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -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. diff --git a/src/registrar/templates/domain_request_requirements.html b/src/registrar/templates/domain_request_requirements.html index 115c3ee69..4d49b235e 100644 --- a/src/registrar/templates/domain_request_requirements.html +++ b/src/registrar/templates/domain_request_requirements.html @@ -4,7 +4,7 @@ {% block form_instructions %}
Please read this page. Check the box at the bottom to show that you agree to the requirements for operating a .gov domain.
-The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t 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.
+The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t 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.
.Gov domains are registered for a one-year period. To renew your domain, you'll be asked to verify your organization’s eligibility and your contact information.
Though a domain may expire, it will not automatically be put on hold or deleted. We’ll make extensive efforts to contact your organization before holding or deleting a domain.
+ {% 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 %} ++ Select one. * +
+ {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% input_with_errors forms.0.working_with_eop %} + {% endwith %} -+ Provide the name and email of the person you're working with.* +
+ {% 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 %} ++ Select one. * +
+ {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% input_with_errors forms.2.has_anything_else_text %} + {% endwith %} + ++ Provide details below * +
+ {% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %} + {% input_with_errors forms.3.anything_else %} + {% endwith %} +This question is optional.
- {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} - {% input_with_errors forms.0.anything_else %} - {% endwith %} -This question is optional.
+ {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} + {% input_with_errors forms.0.anything_else %} + {% endwith %} +