diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 32c8e2ed4..4e7182843 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -642,25 +642,6 @@ class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm): ) -class PurposeForm(RegistrarForm): - 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 OtherContactsYesNoForm(BaseYesNoForm): """The yes/no field for the OtherContacts form.""" diff --git a/src/registrar/forms/domainrequestwizard/purpose.py b/src/registrar/forms/domainrequestwizard/purpose.py new file mode 100644 index 000000000..23e5c28e9 --- /dev/null +++ b/src/registrar/forms/domainrequestwizard/purpose.py @@ -0,0 +1,114 @@ +from django import forms +from django.core.validators import MaxLengthValidator +from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm, RegistrarForm + +class FEBPurposeOptionsForm(BaseDeletableRegistrarForm): + + field_name = "feb_purpose_choice" + + form_choices = (("new", "Used for a new website"), ("redirect", "Used as a redirect for an existing website"), ("other", "Not for a website")) + + feb_purpose_choice = forms.ChoiceField( + required=True, + choices=form_choices, + widget=forms.RadioSelect, + error_messages={ + "required": "This question is required.", + }, + label = "Select one" + ) + +class 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( + 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. + If the "no" option is selected, details must be provided via the separate details form. + """ + + field_name = "has_timeframe" + + @property + def form_is_checked(self): + """ + Determines the initial checked state of the form based on the domain_request's attributes. + """ + return self.domain_request.has_timeframe + + +class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm): + time_frame_details = forms.CharField( + label="time_frame_details", + widget=forms.Textarea( + attrs={ + "aria-label": "Provide details on your target timeframe. \ + Is there a special significance to this date (legal requirement, announcement, event, etc)?" + } + ), + validators=[ + MaxLengthValidator( + 2000, + message="Response must be less than 2000 characters.", + ) + ], + error_messages={"required": "Provide details on your target timeframe."}, + ) + +class FEBInteragencyInitiativeYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm): + """ + Form for determining whether the domain request is part of an interagency initative. + If the "no" option is selected, details must be provided via the separate details form. + """ + + field_name = "is_interagency_initiative" + + @property + def form_is_checked(self): + """ + Determines the initial checked state of the form based on the domain_request's attributes. + """ + return self.domain_request.is_interagency_initiative + + +class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm): + interagency_initiative_details = forms.CharField( + label="interagency_initiative_details", + widget=forms.Textarea( + attrs={ + "aria-label": "Name the agencies that will be involved in this initiative." + } + ), + validators=[ + MaxLengthValidator( + 2000, + message="Response must be less than 2000 characters.", + ) + ], + error_messages={"required": "Name the agencies that will be involved in this initiative."}, + ) \ No newline at end of file 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 new file mode 100644 index 000000000..1cd5fb587 --- /dev/null +++ b/src/registrar/migrations/0142_domainrequest_feb_purpose_choice_and_more.py @@ -0,0 +1,40 @@ +# 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 860bd9b2c..b45767598 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -53,6 +53,11 @@ 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" + OTHER = "other" class StateTerritoryChoices(models.TextChoices): ALABAMA = "AL", "Alabama (AL)" @@ -501,6 +506,7 @@ class DomainRequest(TimeStampedModel): on_delete=models.PROTECT, ) + # Fields specific to Federal Executive Branch agencies, used by OMB for reviewing requests feb_naming_requirements = models.BooleanField( null=True, blank=True, @@ -511,6 +517,40 @@ class DomainRequest(TimeStampedModel): blank=True, ) + feb_purpose_choice = models.CharField( + null=True, + blank=True, + choices=FEBPurposeChoices.choices, + ) + + # This field is alternately used for generic domain purpose explanations + # and for explanations of the specific purpose chosen with feb_purpose_choice + # by a Federal Executive Branch agency. + purpose = models.TextField( + null=True, + blank=True, + ) + + has_timeframe = models.BooleanField( + null=True, + blank=True, + ) + + time_frame_details = models.TextField( + null=True, + blank=True, + ) + + is_interagency_initiative = models.BooleanField( + null=True, + blank=True, + ) + + interagency_initiative_details = models.TextField( + null=True, + blank=True, + ) + alternative_domains = models.ManyToManyField( "registrar.Website", blank=True, @@ -518,10 +558,7 @@ class DomainRequest(TimeStampedModel): help_text="Other domain names the creator provided for consideration", ) - purpose = models.TextField( - null=True, - blank=True, - ) + other_contacts = models.ManyToManyField( "registrar.Contact", diff --git a/src/registrar/templates/domain_request_purpose.html b/src/registrar/templates/domain_request_purpose.html index bfd9beb15..d0fb65042 100644 --- a/src/registrar/templates/domain_request_purpose.html +++ b/src/registrar/templates/domain_request_purpose.html @@ -3,17 +3,90 @@ {% 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).

-

Read about activities that are prohibited on .gov domains.

-

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?

+

Read about activities that are prohibited on .gov domains.

+

What is the purpose of your requested domain?

+

{{requires_feb_questions}}

{% endblock %} -{% block form_required_fields_help_text %} -{# commented out so it does not appear on this page #} -{% endblock %} +{% if requires_feb_questions %} + {% block feb_fields %} +
+ {{forms.0.management_form}} + {{forms.1.management_form}} + {{forms.2.management_form}} + {{forms.3.management_form}} + {{forms.4.management_form}} + {{forms.5.management_form}} +

+ Select One * +

+ {% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %} + {% input_with_errors forms.0.feb_purpose_choice %} + {% endwith %} + + + + +

Do you have a target time frame for launching this domain?

+
+

+ Select One * +

+ {% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %} + {% input_with_errors forms.2.has_timeframe %} + {% endwith %} + + + + +

Will the domain name be used for an interagency initiative?

+
+

+ Select One * +

+ {% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %} + {% input_with_errors forms.4.is_interagency_initiative %} + {% endwith %} + + +
+ {% endblock %} +{% else %} +

Describe how you’ll use your .gov domain. Will it be used for a website, email, or something else?

+ + {% block form_required_fields_help_text %} + {# commented out so it does not appear on this page #} + {% endblock %} + + {% block form_fields %} + {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} + {% input_with_errors forms.1.purpose %} + {% endwith %} + {% endblock %} +{% endif %} + -{% block form_fields %} - {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} - {% input_with_errors forms.0.purpose %} - {% endwith %} -{% endblock %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index a244c0191..372861c07 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView from django.contrib import messages from registrar.forms import domain_request_wizard as forms +from registrar.forms.domainrequestwizard import purpose from registrar.forms.utility.wizard_form_helper import request_step_list from registrar.models import DomainRequest from registrar.models.contact import Contact @@ -182,7 +183,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): return PortfolioDomainRequestStep if self.is_portfolio else Step def requires_feb_questions(self) -> bool: - return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature") + # TODO: this is for testing, revert later + return False + # return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature") @property def prefix(self): @@ -709,7 +712,64 @@ class DotgovDomain(DomainRequestWizard): class Purpose(DomainRequestWizard): template_name = "domain_request_purpose.html" - forms = [forms.PurposeForm] + + forms = [purpose.FEBPurposeOptionsForm, + purpose.PurposeDetailsForm, + purpose.FEBTimeFrameYesNoForm, + purpose.FEBTimeFrameDetailsForm, + purpose.FEBInteragencyInitiativeYesNoForm, + purpose.FEBInteragencyInitiativeDetailsForm + ] + + def get_context_data(self): + context= super().get_context_data() + context["requires_feb_questions"] = self.requires_feb_questions() + return context + + def is_valid(self, forms_list: list) -> bool: + """ + Expected order of forms_list: + 0: FEBPurposeOptionsForm + 1: PurposeDetailsForm + 2: FEBTimeFrameYesNoForm + 3: FEBTimeFrameDetailsForm + 4: FEBInteragencyInitiativeYesNoForm + 5: FEBInteragencyInitiativeDetailsForm + """ + + feb_purpose_options_form = forms_list[0] + purpose_details_form = forms_list[1] + feb_timeframe_yes_no_form = forms_list[2] + feb_timeframe_details_form = forms_list[3] + feb_initiative_yes_no_form = forms_list[4] + feb_initiative_details_form = forms_list[5] + + if not self.requires_feb_questions(): + # if FEB questions don't apply, mark all other 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 + return purpose_details_form.is_valid() + + if not feb_purpose_options_form.id_valid(): + # Ensure details form doesn't throw errors if it's not showing + purpose_details_form.mark_form_for_deletion() + + if not feb_initiative_yes_no_form.is_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_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_delation() + + valid = all(form.is_valid() for form in forms_list if not form.form_data_marked_for_deletion) + + return valid + class OtherContacts(DomainRequestWizard):