From e04337f94c2ccd11000b83dfe1b33606218927fa Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:39:55 -0600 Subject: [PATCH 01/47] initial form --- src/registrar/forms/domain_request_wizard.py | 74 ++++++++++++++++++- src/registrar/models/domain_request.py | 10 +++ .../domain_request_requesting_entity.html | 15 +++- src/registrar/views/domain_request.py | 2 +- 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 6b160b14d..80d162762 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -13,7 +13,7 @@ BaseYesNoForm, BaseDeletableRegistrarForm, ) -from registrar.models import Contact, DomainRequest, DraftDomain, Domain, FederalAgency +from registrar.models import Contact, DomainRequest, DraftDomain, Domain, FederalAgency, Suborganization from registrar.templatetags.url_helpers import public_site_url from registrar.utility.enums import ValidationReturnType from registrar.utility.constants import BranchChoices @@ -22,11 +22,81 @@ class RequestingEntityForm(RegistrarForm): + sub_organization = forms.ModelChoiceField( + label="Suborganization name", + # not required because this field won't be filled out unless + # it is a federal agency. Use clean to check programatically + # if it has been filled in when required. + required=False, + queryset=Suborganization.objects.all(), + empty_label="--Select--", + ) organization_name = forms.CharField( - label="Organization name", + label="Requested suborganization", + required=False, error_messages={"required": "Enter the name of your organization."}, ) + city = forms.CharField( + label="City", + required=False, + error_messages={"required": "Enter the city where your organization is located."}, + ) + state_territory = forms.ChoiceField( + label="State, territory, or military post", + required=False, + choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices, + error_messages={ + "required": ("Select the state, territory, or military post where your organization is located.") + }, + ) + is_suborganization = forms.NullBooleanField( + widget=forms.RadioSelect( + choices=[ + (True, "Yes"), + (False, "No"), + ], + ) + ) + def clean_sub_organization(self): + """Require something to be selected when this is a federal agency.""" + sub_organization = self.cleaned_data.get("sub_organization", None) + if self.cleaned_data.get("is_suborganization", None): + # TODO - logic for if other is selected, display other stuff + if not sub_organization: + # no answer was selected + raise forms.ValidationError( + "Select a suborganization.", + code="required", + ) + # Maybe we just represent this with none? + elif sub_organization == "other": + org_name = self.cleaned_data.get("organization_name", None) + city = self.cleaned_data.get("city", None) + state = self.cleaned_data.get("state_territory", None) + if not org_name or not city or not state: + raise forms.ValidationError( + "Enter details for your suborganization.", + code="required", + ) + return sub_organization + +class RequestingEntityYesNoForm(BaseYesNoForm): + """The yes/no field for the RequestingEntity form.""" + + form_choices = ((False, "Dynamic portfolio field"), (True, "A suborganization. (choose from list)")) + field_name = "is_suborganization" + + @property + def form_is_checked(self): + """ + Determines the initial checked state of the form based on the domain_request's attributes. + """ + if self.domain_request.portfolio and (self.domain_request.sub_organization or self.domain_request.organization_name): + return self.domain_request.organization_name != self.domain_request.portfolio.organization_name + else: + # No pre-selection for new domain requests + return None class OrganizationTypeForm(RegistrarForm): generic_org_type = forms.ChoiceField( diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index b9e3315d5..fb61e93e5 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1173,6 +1173,16 @@ def is_federal(self) -> Union[bool, None]: return True return False + def is_suborganization(self) -> bool: + if self.portfolio: + if self.sub_organization: + return True + + if self.organization_name != self.portfolio.organization_name: + return True + + return False + def to_dict(self): """This is to process to_dict for Domain Information, making it friendly to "copy" it diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index ed8dd771c..e59ad501c 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -2,15 +2,22 @@ {% load field_helpers url_helpers %} {% block form_instructions %} -

🛸🛸🛸🛸 Placeholder content 🛸🛸🛸🛸

+

To help with our review, we need to understand whether the domain you're requesting will be used by the Department of Energy or by one of its suborganizations.

+

We define a suborganization as any entity (agency, bureau, office) that falls under the overarching organization.

{% endblock %} {% block form_fields %}
-

What is the name of your space vessel?

+

Who will use the domain you’re requesting?

- - {% input_with_errors forms.0.organization_name %} + {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% input_with_errors forms.0.is_suborganization %} + {% endwith %} + {# forms.0 is a small yes/no form that toggles the visibility of "requesting entity" formset #} + {% input_with_errors forms.1.sub_organization %} + {% input_with_errors forms.1.organization_name %} + {% input_with_errors forms.1.city %} + {% input_with_errors forms.1.state_territory %}
{% endblock %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 1dbe9f082..e098ce935 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -588,7 +588,7 @@ class PortfolioDomainRequestWizard(DomainRequestWizard): # Portfolio pages class RequestingEntity(DomainRequestWizard): template_name = "domain_request_requesting_entity.html" - forms = [forms.RequestingEntityForm] + forms = [forms.RequestingEntityYesNoForm, forms.RequestingEntityForm] class PortfolioAdditionalDetails(DomainRequestWizard): From b3e90969108744733d330aa5147c41bfaad05dbb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:46:26 -0600 Subject: [PATCH 02/47] dynamic options --- src/registrar/forms/domain_request_wizard.py | 10 +++++++++- src/registrar/forms/utility/wizard_form_helper.py | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 80d162762..74f5a32bc 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -83,9 +83,17 @@ def clean_sub_organization(self): class RequestingEntityYesNoForm(BaseYesNoForm): """The yes/no field for the RequestingEntity form.""" - form_choices = ((False, "Dynamic portfolio field"), (True, "A suborganization. (choose from list)")) + # This first option will change dynamically + form_choices = ((False, "Current Organization"), (True, "A suborganization. (choose from list)")) field_name = "is_suborganization" + def __init__(self, *args, **kwargs): + """Extend the initialization of the form from RegistrarForm __init__""" + super().__init__(*args, **kwargs) + if self.domain_request.portfolio: + self.form_choices = ((False, self.domain_request.portfolio), (True, "A suborganization. (choose from list)")) + self.fields[self.field_name] = self.get_typed_choice_field() + @property def form_is_checked(self): """ diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index eedf5839b..0a1019aae 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -261,6 +261,7 @@ def get_typed_choice_field(self): "required": self.required_error_message, }, ) + print(f"here are the form choices: {self.form_choices}, here is the initial: {self.get_initial_value()}") return choice_field From 65f1e628a754a3c60154ecdcde3be5488b4b69fd Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:34:08 -0600 Subject: [PATCH 03/47] add javascript logic (needs some minor refinement) --- src/registrar/assets/js/get-gov.js | 84 +++++++++++++++++++ src/registrar/forms/domain_request_wizard.py | 9 +- .../domain_request_requesting_entity.html | 2 +- 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 8a07b3f27..c34493015 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2379,3 +2379,87 @@ document.addEventListener('DOMContentLoaded', function() { } } })(); + +(function handleRequestingEntityFieldset() { + // Check if the requesting-entity-fieldset exists. + // This determines if we are on the requesting entity page or not. + const fieldset = document.getElementById("requesting-entity-fieldset"); + if (!fieldset) return; + console.log("past here") + // Get the is_suborganization radio buttons + // Sadly, these ugly ids are the auto generated + const formPrefix = "portfolio_requesting_entity" + const isSuborgRadios = document.querySelectorAll(`input[name="${formPrefix}-is_suborganization"]`); + var selectedRequestingEntityValue = document.querySelector(`input[name="${formPrefix}-is_suborganization"]:checked`)?.value; + const subOrgSelect = document.querySelector(`#id_${formPrefix}-sub_organization`); + const orgName = document.querySelector(`#id_${formPrefix}-organization_name`); + const city = document.querySelector(`#id_${formPrefix}-city`); + const stateTerritory = document.querySelector(`#id_${formPrefix}-state_territory`); + + console.log(isSuborgRadios) + console.log(subOrgSelect) + console.log(orgName) + console.log(city) + console.log(stateTerritory) + console.log(selectedRequestingEntityValue) + // Don't do anything if we are missing crucial page elements + if (!isSuborgRadios || !subOrgSelect || !orgName || !city || !stateTerritory) return; + console.log("past here x2") + + // Add fake "other" option to sub_organization select + if (subOrgSelect && !Array.from(subOrgSelect.options).some(option => option.value === "other")) { + const fakeOption = document.createElement("option"); + fakeOption.value = "other"; + fakeOption.text = "Other (enter your organization manually)"; + subOrgSelect.add(fakeOption); + } + + // Hide organization_name, city, state_territory by default + hideElement(orgName.parentElement); + hideElement(city.parentElement); + hideElement(stateTerritory.parentElement); + + // Function to toggle forms based on is_suborganization selection + function toggleSubOrganizationFields () { + selectedRequestingEntityValue = document.querySelector(`input[name="${formPrefix}-is_suborganization"]:checked`)?.value; + if (selectedRequestingEntityValue === "True") { + showElement(subOrgSelect.parentElement); + toggleOrganizationDetails(); + } else { + hideElement(subOrgSelect.parentElement); + hideElement(orgName.parentElement); + hideElement(city.parentElement); + hideElement(stateTerritory.parentElement); + } + }; + + // Function to toggle organization details based on sub_organization selection + function toggleOrganizationDetails () { + // We should hide the org name fields when we select the special other value + if (subOrgSelect.value === "other") { + showElement(orgName.parentElement); + showElement(city.parentElement); + showElement(stateTerritory.parentElement); + } else { + hideElement(orgName.parentElement); + hideElement(city.parentElement); + hideElement(stateTerritory.parentElement); + } + }; + + // Initialize visibility + toggleSubOrganizationFields(); + + // Add event listeners to is_suborganization radio buttons + isSuborgRadios.forEach(radio => { + radio.addEventListener("change", () => { + toggleSubOrganizationFields(); + }); + }); + + subOrgSelect.addEventListener("change", () => { + if (selectedRequestingEntityValue === "True") { + toggleOrganizationDetails(); + } + }); +})(); \ No newline at end of file diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 74f5a32bc..a7a971912 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -28,7 +28,7 @@ class RequestingEntityForm(RegistrarForm): # it is a federal agency. Use clean to check programatically # if it has been filled in when required. required=False, - queryset=Suborganization.objects.all(), + queryset=Suborganization.objects.none(), empty_label="--Select--", ) organization_name = forms.CharField( @@ -57,6 +57,13 @@ class RequestingEntityForm(RegistrarForm): ], ) ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if self.domain_request.portfolio: + self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=self.domain_request.portfolio) + def clean_sub_organization(self): """Require something to be selected when this is a federal agency.""" sub_organization = self.cleaned_data.get("sub_organization", None) diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index e59ad501c..ab7b2337b 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -7,7 +7,7 @@ {% endblock %} {% block form_fields %} -
+

Who will use the domain you’re requesting?

From 860f8f4e3cd59547c5094f54a88a865eeb5b6018 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:00:32 -0600 Subject: [PATCH 04/47] initial logic --- src/registrar/assets/js/get-gov.js | 115 ++++++++---------- src/registrar/forms/domain_request_wizard.py | 74 +++++++---- ...uest_requested_suborganization_and_more.py | 95 +++++++++++++++ src/registrar/models/domain_request.py | 40 +++++- src/registrar/models/suborganization.py | 1 + .../domain_request_requesting_entity.html | 37 +++++- src/registrar/views/domain_request.py | 34 +++++- 7 files changed, 302 insertions(+), 94 deletions(-) create mode 100644 src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index c34493015..c68c31e15 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -22,15 +22,17 @@ var SUCCESS = "success"; * */ const hideElement = (element) => { - element.classList.add('display-none'); + if (element && !element.classList.contains("display-none")) + element.classList.add('display-none'); }; /** - * Show element - * +* Show element +* */ const showElement = (element) => { - element.classList.remove('display-none'); + if (element && element.classList.contains("display-none")) + element.classList.remove('display-none'); }; /** @@ -2385,81 +2387,68 @@ document.addEventListener('DOMContentLoaded', function() { // This determines if we are on the requesting entity page or not. const fieldset = document.getElementById("requesting-entity-fieldset"); if (!fieldset) return; - console.log("past here") + // Get the is_suborganization radio buttons // Sadly, these ugly ids are the auto generated const formPrefix = "portfolio_requesting_entity" const isSuborgRadios = document.querySelectorAll(`input[name="${formPrefix}-is_suborganization"]`); - var selectedRequestingEntityValue = document.querySelector(`input[name="${formPrefix}-is_suborganization"]:checked`)?.value; const subOrgSelect = document.querySelector(`#id_${formPrefix}-sub_organization`); - const orgName = document.querySelector(`#id_${formPrefix}-organization_name`); - const city = document.querySelector(`#id_${formPrefix}-city`); - const stateTerritory = document.querySelector(`#id_${formPrefix}-state_territory`); - - console.log(isSuborgRadios) - console.log(subOrgSelect) - console.log(orgName) - console.log(city) - console.log(stateTerritory) - console.log(selectedRequestingEntityValue) + + // The suborganization section is its own div + const suborganizationFieldset = document.querySelector("#requesting-entity-fieldset__suborganization"); + + // Within the suborganization section, we also have a div that contains orgname, city, and stateterritory + const suborganizationDetailsFieldset = document.querySelector("#requesting-entity-fieldset__suborganization__details"); + + // Use a variable to determine which option has been selected on the yes/no form. + // Don't do anything if we are missing crucial page elements - if (!isSuborgRadios || !subOrgSelect || !orgName || !city || !stateTerritory) return; - console.log("past here x2") + if (!isSuborgRadios || !subOrgSelect || !suborganizationFieldset || !suborganizationDetailsFieldset) return; - // Add fake "other" option to sub_organization select - if (subOrgSelect && !Array.from(subOrgSelect.options).some(option => option.value === "other")) { - const fakeOption = document.createElement("option"); - fakeOption.value = "other"; - fakeOption.text = "Other (enter your organization manually)"; - subOrgSelect.add(fakeOption); - } - - // Hide organization_name, city, state_territory by default - hideElement(orgName.parentElement); - hideElement(city.parentElement); - hideElement(stateTerritory.parentElement); - - // Function to toggle forms based on is_suborganization selection - function toggleSubOrganizationFields () { - selectedRequestingEntityValue = document.querySelector(`input[name="${formPrefix}-is_suborganization"]:checked`)?.value; - if (selectedRequestingEntityValue === "True") { - showElement(subOrgSelect.parentElement); - toggleOrganizationDetails(); - } else { - hideElement(subOrgSelect.parentElement); - hideElement(orgName.parentElement); - hideElement(city.parentElement); - hideElement(stateTerritory.parentElement); - } + // Function to toggle suborganization based on is_suborganization selection + function toggleSuborganization(radio) { + if (radio && radio.checked && radio.value === "True") { + showElement(suborganizationFieldset); + toggleSuborganizationDetails(); + } else { + hideElement(suborganizationFieldset); + hideElement(suborganizationDetailsFieldset); + } }; // Function to toggle organization details based on sub_organization selection - function toggleOrganizationDetails () { - // We should hide the org name fields when we select the special other value - if (subOrgSelect.value === "other") { - showElement(orgName.parentElement); - showElement(city.parentElement); - showElement(stateTerritory.parentElement); - } else { - hideElement(orgName.parentElement); - hideElement(city.parentElement); - hideElement(stateTerritory.parentElement); - } + function toggleSuborganizationDetails () { + // We should hide the org name fields when we select the special other value + if (subOrgSelect.value === "other") { + showElement(suborganizationDetailsFieldset); + } else { + hideElement(suborganizationDetailsFieldset); + } }; - // Initialize visibility - toggleSubOrganizationFields(); + // Add fake "other" option to sub_organization select + if (subOrgSelect && !Array.from(subOrgSelect.options).some(option => option.value === "other")) { + const fakeOption = document.createElement("option"); + fakeOption.value = "other"; + fakeOption.text = "Other (enter your organization manually)"; + subOrgSelect.add(fakeOption); + } - // Add event listeners to is_suborganization radio buttons + // Add event listener to is_suborganization radio buttons isSuborgRadios.forEach(radio => { - radio.addEventListener("change", () => { - toggleSubOrganizationFields(); - }); + // Run this here for initial display. + // Since there are only two radio buttons and since this has (practically speaking) no performance impact, this is fine to do. + toggleSuborganization(radio); + + // Add an event listener to each to show/hide the relevant fields + radio.addEventListener("click", () => { + toggleSuborganization(radio); + }); }); + // Add event listener to the suborg dropdown to show/hide the suborg details section subOrgSelect.addEventListener("change", () => { - if (selectedRequestingEntityValue === "True") { - toggleOrganizationDetails(); - } + toggleSuborganizationDetails(); }); + })(); \ No newline at end of file diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index a7a971912..a348029a9 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -31,17 +31,20 @@ class RequestingEntityForm(RegistrarForm): queryset=Suborganization.objects.none(), empty_label="--Select--", ) - organization_name = forms.CharField( + + # We are using the current sub_organization naming convention here. + # We may want to refactor this to suborganization eventually. + requested_suborganization = forms.CharField( label="Requested suborganization", required=False, error_messages={"required": "Enter the name of your organization."}, ) - city = forms.CharField( + suborganization_city = forms.CharField( label="City", required=False, error_messages={"required": "Enter the city where your organization is located."}, ) - state_territory = forms.ChoiceField( + suborganization_state_territory = forms.ChoiceField( label="State, territory, or military post", required=False, choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices, @@ -66,26 +69,52 @@ def __init__(self, *args, **kwargs): def clean_sub_organization(self): """Require something to be selected when this is a federal agency.""" + is_suborganization = self.cleaned_data.get("is_suborganization", None) sub_organization = self.cleaned_data.get("sub_organization", None) - if self.cleaned_data.get("is_suborganization", None): - # TODO - logic for if other is selected, display other stuff - if not sub_organization: - # no answer was selected + print(f"sub org is: {sub_organization} vs is_suborg: {is_suborganization}") + if is_suborganization and not sub_organization: + raise forms.ValidationError( + "Select a suborganization.", + code="required", + ) + return sub_organization + + def clean_requested_suborganization(self): + field = self.cleaned_data.get("requested_suborganization") + if self.is_custom_suborg() and not field: + raise forms.ValidationError( + "Enter details for your organization name.", + code="required", + ) + return field + + def clean_suborganization_city(self): + field = self.cleaned_data.get("suborganization_city") + if self.is_custom_suborg(): + if not field: raise forms.ValidationError( - "Select a suborganization.", + "Enter details for your city.", code="required", ) - # Maybe we just represent this with none? - elif sub_organization == "other": - org_name = self.cleaned_data.get("organization_name", None) - city = self.cleaned_data.get("city", None) - state = self.cleaned_data.get("state_territory", None) - if not org_name or not city or not state: - raise forms.ValidationError( - "Enter details for your suborganization.", - code="required", - ) - return sub_organization + return field + + def clean_suborganization_state_territory(self): + field = self.cleaned_data.get("suborganization_state_territory") + if self.is_custom_suborg(): + if not field: + raise forms.ValidationError( + "Enter details for your state or territory.", + code="required", + ) + return field + + def is_custom_suborg(self): + """Checks that the select suborg is 'other', which is a custom field indicating + that we should create a new suborganization.""" + is_suborganization = self.cleaned_data.get("is_suborganization") + sub_organization = self.cleaned_data.get("sub_organization") + return is_suborganization and sub_organization == "other" + class RequestingEntityYesNoForm(BaseYesNoForm): """The yes/no field for the RequestingEntity form.""" @@ -107,10 +136,11 @@ def form_is_checked(self): Determines the initial checked state of the form based on the domain_request's attributes. """ - if self.domain_request.portfolio and (self.domain_request.sub_organization or self.domain_request.organization_name): - return self.domain_request.organization_name != self.domain_request.portfolio.organization_name + if self.domain_request.portfolio and self.domain_request.organization_name == self.domain_request.portfolio.organization_name: + return False + elif self.domain_request.is_suborganization(): + return True else: - # No pre-selection for new domain requests return None class OrganizationTypeForm(RegistrarForm): diff --git a/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py b/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py new file mode 100644 index 000000000..bc7235a41 --- /dev/null +++ b/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py @@ -0,0 +1,95 @@ +# Generated by Django 4.2.10 on 2024-10-23 15:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0133_domainrequest_rejection_reason_email_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="domainrequest", + name="requested_suborganization", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domainrequest", + name="suborganization_city", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domainrequest", + name="suborganization_state_territory", + field=models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state, territory, or military post", + ), + ), + ] diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index fb61e93e5..c19addd61 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -344,6 +344,24 @@ def get_action_needed_reason_label(cls, action_needed_reason: str): verbose_name="Suborganization", ) + requested_suborganization = models.CharField( + null=True, + blank=True, + ) + + suborganization_city = models.CharField( + null=True, + blank=True, + ) + + suborganization_state_territory = models.CharField( + max_length=2, + choices=StateTerritoryChoices.choices, + null=True, + blank=True, + verbose_name="state, territory, or military post", + ) + # This is the domain request user who created this domain request. creator = models.ForeignKey( "registrar.User", @@ -1104,6 +1122,15 @@ def reject_with_prejudice(self): self.creator.restrict_user() + # Form unlocking steps + # These methods control the conditions in which we should unlock certain domain wizard steps. + def unlock_requesting_entity(self) -> bool: + """Unlocks the requesting entity step """ + if self.portfolio and self.organization_name == self.portfolio.organization_name: + return True + else: + return self.is_suborganization() + # ## Form policies ### # # These methods control what questions need to be answered by applicants @@ -1174,14 +1201,23 @@ def is_federal(self) -> Union[bool, None]: return False def is_suborganization(self) -> bool: + """Determines if this record is a suborganization or not""" if self.portfolio: if self.sub_organization: return True - if self.organization_name != self.portfolio.organization_name: + if self.has_information_required_to_make_suborganization(): return True - return False + + def has_information_required_to_make_suborganization(self): + """Checks if we have all the information we need to create a new suborganization object. + Checks for a the existence of requested_suborganization, suborganization_city, suborganization_state_territory""" + return ( + self.requested_domain and + self.suborganization_city and + self.suborganization_state_territory + ) def to_dict(self): """This is to process to_dict for Domain Information, making it friendly diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index 6ad80fdc0..0b1c6e0ac 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -1,4 +1,5 @@ from django.db import models +from registrar.models import DomainRequest from .utility.time_stamped_model import TimeStampedModel diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index ab7b2337b..144ab77bf 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -11,13 +11,38 @@

Who will use the domain you’re requesting?

+ + {# forms.0 is a small yes/no form that toggles the visibility of "requesting entity" formset #} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} - {% input_with_errors forms.0.is_suborganization %} + {% with attr_required=True %} + {% input_with_errors forms.0.is_suborganization %} + {% endwith %} {% endwith %} - {# forms.0 is a small yes/no form that toggles the visibility of "requesting entity" formset #} - {% input_with_errors forms.1.sub_organization %} - {% input_with_errors forms.1.organization_name %} - {% input_with_errors forms.1.city %} - {% input_with_errors forms.1.state_territory %} + +
+

Add suborganization information

+

+ This information will be published in .gov’s public data. If you don’t see your suborganization in the list, + select “other” and enter the name or your suborganization. +

+ {% with attr_required=True %} + {% input_with_errors forms.1.sub_organization %} + {% endwith %} + + {% comment %} This will be toggled if a special value, "other", is selected. + Otherwise this field is invisible. + {% endcomment %} +
+ {% with attr_required=True %} + {% input_with_errors forms.1.requested_suborganization %} + {% endwith %} + {% with attr_required=True %} + {% input_with_errors forms.1.suborganization_city %} + {% endwith %} + {% with attr_required=True %} + {% input_with_errors forms.1.suborganization_state_territory %} + {% endwith %} +
+
{% endblock %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index e098ce935..f0baa9828 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -11,6 +11,7 @@ from registrar.forms.utility.wizard_form_helper import request_step_list from registrar.models import DomainRequest from registrar.models.contact import Contact +from registrar.models.suborganization import Suborganization from registrar.models.user import User from registrar.views.utility import StepsHelper from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView @@ -137,7 +138,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): } PORTFOLIO_UNLOCKING_STEPS = { - PortfolioDomainRequestStep.REQUESTING_ENTITY: lambda self: self.domain_request.organization_name is not None, + PortfolioDomainRequestStep.REQUESTING_ENTITY: lambda w: w.from_model("unlock_requesting_entity", False), PortfolioDomainRequestStep.CURRENT_SITES: lambda self: ( self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None ), @@ -590,6 +591,37 @@ class RequestingEntity(DomainRequestWizard): template_name = "domain_request_requesting_entity.html" forms = [forms.RequestingEntityYesNoForm, forms.RequestingEntityForm] + def save(self, forms: list): + requesting_entity_form = forms[1] + cleaned_data = requesting_entity_form.cleaned_data + is_suborganization = cleaned_data.get("is_suborganization") + sub_organization = cleaned_data.get("sub_organization") + requested_suborganization = cleaned_data.get("requested_suborganization") + + # If no suborganization presently exists but the user filled out org information then create a suborg automatically. + if is_suborganization and (sub_organization or requested_suborganization): + # Cleanup the organization name field, as this isn't for suborganizations. + self.domain_request.organization_name = None + + # Create or get the Suborganization. + # Then update the domain_request with the new or existing suborganization + if not sub_organization: + sub_organization, created = Suborganization.objects.get_or_create( + name=cleaned_data.get("requested_suborganization"), + portfolio=self.domain_request.portfolio, + ) + + self.domain_request.sub_organization = sub_organization + else: + # If the user doesn't intend to create a suborg, simply don't make one and do some data cleanup + self.domain_request.organization_name = self.domain_request.portfolio.organization_name + + self.domain_request.sub_organization = None + self.domain_request.requested_suborganization = None + self.domain_request.suborganization_city = None + self.domain_request.suborganization_state_territory = None + + super().save(forms) class PortfolioAdditionalDetails(DomainRequestWizard): template_name = "portfolio_domain_request_additional_details.html" From 888d77bf9682b869deed701ed337a34b0a010481 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:46:18 -0600 Subject: [PATCH 05/47] Validation logic --- src/registrar/assets/js/get-gov.js | 8 ++ src/registrar/forms/domain_request_wizard.py | 85 ++++++++++--------- src/registrar/models/domain_request.py | 6 ++ .../domain_request_requesting_entity.html | 6 +- src/registrar/views/domain_request.py | 9 -- 5 files changed, 63 insertions(+), 51 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index c68c31e15..0727c3401 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2400,6 +2400,8 @@ document.addEventListener('DOMContentLoaded', function() { // Within the suborganization section, we also have a div that contains orgname, city, and stateterritory const suborganizationDetailsFieldset = document.querySelector("#requesting-entity-fieldset__suborganization__details"); + var isCustomSuborganization = document.querySelector("#id_portfolio_requesting_entity-is_custom_suborganization") + // Use a variable to determine which option has been selected on the yes/no form. // Don't do anything if we are missing crucial page elements @@ -2421,8 +2423,10 @@ document.addEventListener('DOMContentLoaded', function() { // We should hide the org name fields when we select the special other value if (subOrgSelect.value === "other") { showElement(suborganizationDetailsFieldset); + isCustomSuborganization.value = "True"; } else { hideElement(suborganizationDetailsFieldset); + isCustomSuborganization.value = "False"; } }; @@ -2434,6 +2438,10 @@ document.addEventListener('DOMContentLoaded', function() { subOrgSelect.add(fakeOption); } + if (isCustomSuborganization.value === "True") { + subOrgSelect.value = "other" + } + // Add event listener to is_suborganization radio buttons isSuborgRadios.forEach(radio => { // Run this here for initial display. diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index a348029a9..d401360b8 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -61,6 +61,9 @@ class RequestingEntityForm(RegistrarForm): ) ) + # Add a hidden field to store that we are adding a custom suborg + is_custom_suborganization = forms.BooleanField(required=False, widget=forms.HiddenInput()) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -68,52 +71,52 @@ def __init__(self, *args, **kwargs): self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=self.domain_request.portfolio) def clean_sub_organization(self): - """Require something to be selected when this is a federal agency.""" - is_suborganization = self.cleaned_data.get("is_suborganization", None) - sub_organization = self.cleaned_data.get("sub_organization", None) - print(f"sub org is: {sub_organization} vs is_suborg: {is_suborganization}") - if is_suborganization and not sub_organization: - raise forms.ValidationError( - "Select a suborganization.", - code="required", - ) + sub_organization = self.cleaned_data.get("sub_organization") + is_custom = self.cleaned_data.get("is_custom_suborganization") + print(f"in clean: {sub_organization}") + if is_custom: + # If it's a custom suborganization, return None (equivalent to selecting nothing) + return None return sub_organization - def clean_requested_suborganization(self): - field = self.cleaned_data.get("requested_suborganization") - if self.is_custom_suborg() and not field: - raise forms.ValidationError( - "Enter details for your organization name.", - code="required", - ) - return field + def full_clean(self): + # Remove the custom other field before cleaning + data = self.data.copy() if self.data else None + suborganization = self.data.get('portfolio_requesting_entity-sub_organization') + is_suborganization = self.data.get("portfolio_requesting_entity-is_suborganization") + if suborganization: + if "other" in data['portfolio_requesting_entity-sub_organization']: + # Remove the 'other' value + data['portfolio_requesting_entity-sub_organization'] = "" + + # Set the modified data back to the form + self.data = data + + # Call the parent's full_clean method + super().full_clean() - def clean_suborganization_city(self): - field = self.cleaned_data.get("suborganization_city") - if self.is_custom_suborg(): - if not field: - raise forms.ValidationError( - "Enter details for your city.", - code="required", - ) - return field + def clean(self): + """Custom clean implementation to handle our desired logic flow for suborganization. + Given that these fields often corely on eachother, we need to do this in the parent function.""" + cleaned_data = super().clean() - def clean_suborganization_state_territory(self): - field = self.cleaned_data.get("suborganization_state_territory") - if self.is_custom_suborg(): - if not field: - raise forms.ValidationError( - "Enter details for your state or territory.", - code="required", - ) - return field - - def is_custom_suborg(self): - """Checks that the select suborg is 'other', which is a custom field indicating - that we should create a new suborganization.""" + suborganization = self.cleaned_data.get("sub_organization") is_suborganization = self.cleaned_data.get("is_suborganization") - sub_organization = self.cleaned_data.get("sub_organization") - return is_suborganization and sub_organization == "other" + is_custom_suborganization = self.cleaned_data.get("is_custom_suborganization") + if is_suborganization: + if is_custom_suborganization: + # Validate custom suborganization fields + if not cleaned_data.get("requested_suborganization"): + self.add_error("requested_suborganization", "Enter details for your organization name.") + if not cleaned_data.get("suborganization_city"): + self.add_error("suborganization_city", "Enter details for your city.") + if not cleaned_data.get("suborganization_state_territory"): + self.add_error("suborganization_state_territory", "Enter details for your state or territory.") + elif not suborganization: + self.add_error("sub_organization", "Select a suborganization.") + + cleaned_data = super().clean() + return cleaned_data class RequestingEntityYesNoForm(BaseYesNoForm): diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index c19addd61..e9ba3d7bf 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1210,6 +1210,12 @@ def is_suborganization(self) -> bool: return True return False + def is_custom_suborganization(self) -> bool: + if self.is_suborganization(): + return not self.sub_organization and self.has_information_required_to_make_suborganization() + else: + return False + def has_information_required_to_make_suborganization(self): """Checks if we have all the information we need to create a new suborganization object. Checks for a the existence of requested_suborganization, suborganization_city, suborganization_state_territory""" diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index 144ab77bf..70db337de 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -12,13 +12,17 @@

Who will use the domain you’re requesting?

+

+ Select one. * +

+ {# forms.0 is a small yes/no form that toggles the visibility of "requesting entity" formset #} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% with attr_required=True %} {% input_with_errors forms.0.is_suborganization %} {% endwith %} {% endwith %} - + {% input_with_errors forms.1.is_custom_suborganization %}

Add suborganization information

diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index f0baa9828..f6f9b9373 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -602,15 +602,6 @@ def save(self, forms: list): if is_suborganization and (sub_organization or requested_suborganization): # Cleanup the organization name field, as this isn't for suborganizations. self.domain_request.organization_name = None - - # Create or get the Suborganization. - # Then update the domain_request with the new or existing suborganization - if not sub_organization: - sub_organization, created = Suborganization.objects.get_or_create( - name=cleaned_data.get("requested_suborganization"), - portfolio=self.domain_request.portfolio, - ) - self.domain_request.sub_organization = sub_organization else: # If the user doesn't intend to create a suborg, simply don't make one and do some data cleanup From 514cd8af3125613b56d2c2d81a5fb1a894ad85aa Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:52:42 -0600 Subject: [PATCH 06/47] fix unlocking steps --- src/registrar/forms/domain_request_wizard.py | 1 - src/registrar/forms/utility/wizard_form_helper.py | 1 - src/registrar/models/domain_request.py | 10 +++++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index d401360b8..e0aa04483 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -83,7 +83,6 @@ def full_clean(self): # Remove the custom other field before cleaning data = self.data.copy() if self.data else None suborganization = self.data.get('portfolio_requesting_entity-sub_organization') - is_suborganization = self.data.get("portfolio_requesting_entity-is_suborganization") if suborganization: if "other" in data['portfolio_requesting_entity-sub_organization']: # Remove the 'other' value diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 0a1019aae..eedf5839b 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -261,7 +261,6 @@ def get_typed_choice_field(self): "required": self.required_error_message, }, ) - print(f"here are the form choices: {self.form_choices}, here is the initial: {self.get_initial_value()}") return choice_field diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index e9ba3d7bf..9c990ebd4 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1208,6 +1208,7 @@ def is_suborganization(self) -> bool: if self.has_information_required_to_make_suborganization(): return True + return False def is_custom_suborganization(self) -> bool: @@ -1219,11 +1220,10 @@ def is_custom_suborganization(self) -> bool: def has_information_required_to_make_suborganization(self): """Checks if we have all the information we need to create a new suborganization object. Checks for a the existence of requested_suborganization, suborganization_city, suborganization_state_territory""" - return ( - self.requested_domain and - self.suborganization_city and - self.suborganization_state_territory - ) + if self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory: + return True + else: + return False def to_dict(self): """This is to process to_dict for Domain Information, making it friendly From 263ec02bc32ba1d3e35014c8414c682692317375 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:14:03 -0600 Subject: [PATCH 07/47] Add requesting entity to review page --- .../templates/domain_request_requesting_entity.html | 9 ++++++++- .../includes/portfolio_request_review_steps.html | 7 ++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index 70db337de..04ceb4a1a 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -22,7 +22,14 @@

Who will use the domain you’re requesting?

{% input_with_errors forms.0.is_suborganization %} {% endwith %} {% endwith %} - {% input_with_errors forms.1.is_custom_suborganization %} + + {% comment %} Add an invisible form element to track whether the custom value "other" + was selected or not. This allows for persistence across page reloads without using session variables. + {% endcomment %} + {% with add_group_class="display-none" %} + {% input_with_errors forms.1.is_custom_suborganization %} + {% endwith %} +

Add suborganization information

diff --git a/src/registrar/templates/includes/portfolio_request_review_steps.html b/src/registrar/templates/includes/portfolio_request_review_steps.html index 9d3c5bdeb..dd0099ec7 100644 --- a/src/registrar/templates/includes/portfolio_request_review_steps.html +++ b/src/registrar/templates/includes/portfolio_request_review_steps.html @@ -8,9 +8,10 @@ {% endif %} {% if step == Step.REQUESTING_ENTITY %} - {% if domain_request.organization_name %} - {% with title=form_titles|get_item:step value=domain_request %} - {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url address='true' %} + {% if domain_request.requested_suborganization and domain_request.suborganization_city and domain_request.suborganization_state_territory %} + {% with title=form_titles|get_item:step value=domain_request.requested_suborganization %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} +

{{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}

{% endwith %} {% else %} {% with title=form_titles|get_item:step value="Incomplete"|safe %} From 086d1977b8f24ed77f9dc127305aaf0e98ac54b4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:15:14 -0600 Subject: [PATCH 08/47] migration --- .../0134_domainrequest_requested_suborganization_and_more.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py b/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py index bc7235a41..6070b4a82 100644 --- a/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py +++ b/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-10-23 15:01 +# Generated by Django 4.2.10 on 2024-10-23 19:15 from django.db import migrations, models From 5b629bd7edd7d4d9afabfb45a6b2cc8430acdfcc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:15:41 -0600 Subject: [PATCH 09/47] Delete 0134_domainrequest_requested_suborganization_and_more.py --- ...uest_requested_suborganization_and_more.py | 95 ------------------- 1 file changed, 95 deletions(-) delete mode 100644 src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py diff --git a/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py b/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py deleted file mode 100644 index 6070b4a82..000000000 --- a/src/registrar/migrations/0134_domainrequest_requested_suborganization_and_more.py +++ /dev/null @@ -1,95 +0,0 @@ -# Generated by Django 4.2.10 on 2024-10-23 19:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("registrar", "0133_domainrequest_rejection_reason_email_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="domainrequest", - name="requested_suborganization", - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name="domainrequest", - name="suborganization_city", - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name="domainrequest", - name="suborganization_state_territory", - field=models.CharField( - blank=True, - choices=[ - ("AL", "Alabama (AL)"), - ("AK", "Alaska (AK)"), - ("AS", "American Samoa (AS)"), - ("AZ", "Arizona (AZ)"), - ("AR", "Arkansas (AR)"), - ("CA", "California (CA)"), - ("CO", "Colorado (CO)"), - ("CT", "Connecticut (CT)"), - ("DE", "Delaware (DE)"), - ("DC", "District of Columbia (DC)"), - ("FL", "Florida (FL)"), - ("GA", "Georgia (GA)"), - ("GU", "Guam (GU)"), - ("HI", "Hawaii (HI)"), - ("ID", "Idaho (ID)"), - ("IL", "Illinois (IL)"), - ("IN", "Indiana (IN)"), - ("IA", "Iowa (IA)"), - ("KS", "Kansas (KS)"), - ("KY", "Kentucky (KY)"), - ("LA", "Louisiana (LA)"), - ("ME", "Maine (ME)"), - ("MD", "Maryland (MD)"), - ("MA", "Massachusetts (MA)"), - ("MI", "Michigan (MI)"), - ("MN", "Minnesota (MN)"), - ("MS", "Mississippi (MS)"), - ("MO", "Missouri (MO)"), - ("MT", "Montana (MT)"), - ("NE", "Nebraska (NE)"), - ("NV", "Nevada (NV)"), - ("NH", "New Hampshire (NH)"), - ("NJ", "New Jersey (NJ)"), - ("NM", "New Mexico (NM)"), - ("NY", "New York (NY)"), - ("NC", "North Carolina (NC)"), - ("ND", "North Dakota (ND)"), - ("MP", "Northern Mariana Islands (MP)"), - ("OH", "Ohio (OH)"), - ("OK", "Oklahoma (OK)"), - ("OR", "Oregon (OR)"), - ("PA", "Pennsylvania (PA)"), - ("PR", "Puerto Rico (PR)"), - ("RI", "Rhode Island (RI)"), - ("SC", "South Carolina (SC)"), - ("SD", "South Dakota (SD)"), - ("TN", "Tennessee (TN)"), - ("TX", "Texas (TX)"), - ("UM", "United States Minor Outlying Islands (UM)"), - ("UT", "Utah (UT)"), - ("VT", "Vermont (VT)"), - ("VI", "Virgin Islands (VI)"), - ("VA", "Virginia (VA)"), - ("WA", "Washington (WA)"), - ("WV", "West Virginia (WV)"), - ("WI", "Wisconsin (WI)"), - ("WY", "Wyoming (WY)"), - ("AA", "Armed Forces Americas (AA)"), - ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), - ("AP", "Armed Forces Pacific (AP)"), - ], - max_length=2, - null=True, - verbose_name="state, territory, or military post", - ), - ), - ] From 0374efb778eb1e01b47c9cd4e19e2200c1c791a5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:16:05 -0600 Subject: [PATCH 10/47] Fix merge conflict --- ...uest_requested_suborganization_and_more.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/registrar/migrations/0135_domainrequest_requested_suborganization_and_more.py diff --git a/src/registrar/migrations/0135_domainrequest_requested_suborganization_and_more.py b/src/registrar/migrations/0135_domainrequest_requested_suborganization_and_more.py new file mode 100644 index 000000000..92ca32697 --- /dev/null +++ b/src/registrar/migrations/0135_domainrequest_requested_suborganization_and_more.py @@ -0,0 +1,98 @@ +# Generated by Django 4.2.10 on 2024-10-23 19:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "registrar", + "0134_rename_portfolio_additional_permissions_portfolioinvitation_additional_permissions_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="domainrequest", + name="requested_suborganization", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domainrequest", + name="suborganization_city", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domainrequest", + name="suborganization_state_territory", + field=models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state, territory, or military post", + ), + ), + ] From 939b11ecee431d376ad5b31cd67835d9e8c027a9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:54:53 -0600 Subject: [PATCH 11/47] Fix edge cases for review page and add fields to admin --- src/registrar/admin.py | 3 +++ .../includes/portfolio_request_review_steps.html | 14 +++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 0b96b4c48..a4117e232 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1748,6 +1748,9 @@ def status_history(self, obj): "fields": [ "portfolio", "sub_organization", + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", "status_history", "status", "rejection_reason", diff --git a/src/registrar/templates/includes/portfolio_request_review_steps.html b/src/registrar/templates/includes/portfolio_request_review_steps.html index dd0099ec7..3f00f911c 100644 --- a/src/registrar/templates/includes/portfolio_request_review_steps.html +++ b/src/registrar/templates/includes/portfolio_request_review_steps.html @@ -8,16 +8,24 @@ {% endif %} {% if step == Step.REQUESTING_ENTITY %} - {% if domain_request.requested_suborganization and domain_request.suborganization_city and domain_request.suborganization_state_territory %} + {% if domain_request.portfolio and domain_request.organization_name == domain_request.portfolio.organization_name %} + {% with title=form_titles|get_item:step value=domain_request.portfolio.organization_name %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} + {% endwith %} + {% elif domain_request.sub_organization %} + {% with title=form_titles|get_item:step value=domain_request.sub_organization %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} + {% endwith %} + {% elif domain_request.requested_suborganization and domain_request.suborganization_city and domain_request.suborganization_state_territory %} {% with title=form_titles|get_item:step value=domain_request.requested_suborganization %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} -

{{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}

{% endwith %} +

{{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}

{% else %} {% with title=form_titles|get_item:step value="Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} {% endwith %} - {% endif%} + {% endif %} {% endif %} {% if step == Step.CURRENT_SITES %} From 150a59cdfc7a44ebd4ce68a92015aed6be55ede9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:02:31 -0600 Subject: [PATCH 12/47] Update portfolio_request_review_steps.html --- .../templates/includes/portfolio_request_review_steps.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/registrar/templates/includes/portfolio_request_review_steps.html b/src/registrar/templates/includes/portfolio_request_review_steps.html index 3f00f911c..c2bdb18cc 100644 --- a/src/registrar/templates/includes/portfolio_request_review_steps.html +++ b/src/registrar/templates/includes/portfolio_request_review_steps.html @@ -12,10 +12,14 @@ {% with title=form_titles|get_item:step value=domain_request.portfolio.organization_name %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} {% endwith %} + {% if domain_request.portfolio.city and domain_request.portfolio.state_territory %} +

{{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}}

+ {% endif %} {% elif domain_request.sub_organization %} {% with title=form_titles|get_item:step value=domain_request.sub_organization %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} {% endwith %} + {% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %} {% elif domain_request.requested_suborganization and domain_request.suborganization_city and domain_request.suborganization_state_territory %} {% with title=form_titles|get_item:step value=domain_request.requested_suborganization %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} From 5cb3298afd89fd5493a4ed05246cd01128ecd87a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:29:26 -0600 Subject: [PATCH 13/47] Update email content, update manage button content --- .../templates/domain_request_status.html | 6 ++- .../portfolio_domain_request_summary.txt | 41 +++++++++++++++++++ .../emails/submission_confirmation.txt | 7 +++- .../includes/portfolio_status_manage.html | 7 ++++ src/registrar/views/domain_request.py | 10 +++++ 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt create mode 100644 src/registrar/templates/includes/portfolio_status_manage.html diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index d332ce54e..d98220532 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -6,5 +6,9 @@ {% block content %} - {% include "includes/request_status_manage.html" %} + {% if not portfolio %} + {% include "includes/request_status_manage.html" %} + {% else %} + {% include "includes/portfolio_status_manage.html" %} + {% endif %} {% endblock %} diff --git a/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt b/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt new file mode 100644 index 000000000..e0b62f93e --- /dev/null +++ b/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt @@ -0,0 +1,41 @@ +SUMMARY OF YOUR DOMAIN REQUEST + +Requesting entity: +{% if domain_request.portfolio and domain_request.organization_name == domain_request.portfolio.organization_name %} + {{domain_request.portfolio.organization_name}} + {{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}} +{% elif domain_request.sub_organization %} + {{domain_request.sub_organization}} + {% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %} +{% elif domain_request.requested_suborganization and domain_request.suborganization_city and domain_request.suborganization_state_territory %} + {{domain_request.requested_suborganization}} + {{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}} +{% endif %} +Alternative domains: +{% for site in domain_request.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %} +{% endfor %}{% endif %} +Current websites: {% for site in domain_request.current_websites.all %} +{% spaceless %}{{ site.website }}{% endspaceless %} +{% endfor %}{% endif %} +.gov domain: +{{ domain_request.requested_domain.name }} +{% if domain_request.alternative_domains.all %} +Alternative domains: +{% for site in domain_request.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %} +{% endfor %}{% endif %} +Purpose of your domain: +{{ domain_request.purpose }} +Additional details: +{{ domain_request.additional_details }} + +Your contact information: +{% spaceless %}{% include "emails/includes/contact.txt" with contact=recipient %}{% endspaceless %} + +Other employees from your organization:{% for other in domain_request.other_contacts.all %} +{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %} +{% empty %} +{{ domain_request.no_other_contacts_rationale }} +{% endfor %}{% if domain_request.anything_else %} +Anything else? +{{ domain_request.anything_else }} +{% endif %} diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index c8ff4c7eb..2b51127f2 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -30,8 +30,11 @@ THANK YOU .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. ---------------------------------------------------------------- - -{% include 'emails/includes/domain_request_summary.txt' %} +{% if portfolio %} + {% include 'emails/includes/portfolio_domain_request_summary.txt' %} +{% else %} + {% include 'emails/includes/domain_request_summary.txt' %} +{% endif %} ---------------------------------------------------------------- The .gov team diff --git a/src/registrar/templates/includes/portfolio_status_manage.html b/src/registrar/templates/includes/portfolio_status_manage.html new file mode 100644 index 000000000..2f2bda8b3 --- /dev/null +++ b/src/registrar/templates/includes/portfolio_status_manage.html @@ -0,0 +1,7 @@ +{% extends "includes/request_status_manage.html" %} +{% load custom_filters %} +{% load static url_helpers %} + +{% block request_summary %} + {% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %} +{% endblock request_summary %} \ No newline at end of file diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index cf8c56303..e88eb4c75 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -857,6 +857,16 @@ def has_permission(self): return True + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # Create a temp wizard object to grab the step list + wizard = PortfolioDomainRequestWizard() + wizard.request = self.request + context["Step"] = PortfolioDomainRequestStep.__members__ + context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep) + context["form_titles"] = wizard.titles + return context + class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView): """This page will ask user to confirm if they want to withdraw From bde6c5e835e75f670975187eb243a398a406722d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:38:25 -0600 Subject: [PATCH 14/47] check for org on email --- src/registrar/models/domain_request.py | 6 ++++++ src/registrar/templates/emails/submission_confirmation.txt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index faa02a3c7..33a9f2dea 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -6,6 +6,7 @@ from django.db import models from django_fsm import FSMField, transition # type: ignore from django.utils import timezone +from waffle import flag_is_active from registrar.models.domain import Domain from registrar.models.federal_agency import FederalAgency from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper @@ -13,6 +14,8 @@ from registrar.utility.constants import BranchChoices from auditlog.models import LogEntry +from registrar.utility.waffle import flag_is_active_for_user + from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError from itertools import chain @@ -841,10 +844,13 @@ def _send_status_update_email( try: if not context: + has_organization_feature_flag = flag_is_active_for_user(recipient, "organization_feature") + is_org_user = has_organization_feature_flag and recipient.has_base_portfolio_permission(self.portfolio) context = { "domain_request": self, # This is the user that we refer to in the email "recipient": recipient, + "is_org_user": is_org_user, } if custom_email_content: diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index 2b51127f2..f4acae7e9 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -30,7 +30,7 @@ THANK YOU .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. ---------------------------------------------------------------- -{% if portfolio %} +{% if is_org_user %} {% include 'emails/includes/portfolio_domain_request_summary.txt' %} {% else %} {% include 'emails/includes/domain_request_summary.txt' %} From 5e23ebead50281aaa2928cbfe3a710e300fbc390 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:38:58 -0600 Subject: [PATCH 15/47] Fix some tests --- src/.pa11yci | 1 + src/registrar/templates/emails/submission_confirmation.txt | 1 + src/registrar/templates/includes/portfolio_status_manage.html | 2 +- src/registrar/tests/test_admin_request.py | 3 +++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/.pa11yci b/src/.pa11yci index c18704c07..bff786de5 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -20,6 +20,7 @@ "http://localhost:8080/request/anything_else/", "http://localhost:8080/request/requirements/", "http://localhost:8080/request/finished/", + "http://localhost:8080/request/requesting_entity/", "http://localhost:8080/user-profile/" ] } diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index f4acae7e9..911269bf1 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -30,6 +30,7 @@ THANK YOU .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. ---------------------------------------------------------------- + {% if is_org_user %} {% include 'emails/includes/portfolio_domain_request_summary.txt' %} {% else %} diff --git a/src/registrar/templates/includes/portfolio_status_manage.html b/src/registrar/templates/includes/portfolio_status_manage.html index 2f2bda8b3..120770156 100644 --- a/src/registrar/templates/includes/portfolio_status_manage.html +++ b/src/registrar/templates/includes/portfolio_status_manage.html @@ -4,4 +4,4 @@ {% block request_summary %} {% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %} -{% endblock request_summary %} \ No newline at end of file +{% endblock request_summary %} diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index 217756359..4b65c401d 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -1642,6 +1642,9 @@ def test_readonly_when_restricted_creator(self): "federal_agency", "portfolio", "sub_organization", + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", "creator", "investigator", "generic_org_type", From d9ec108f5800215a811e5162280ba90e0c3759aa Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:49:01 -0600 Subject: [PATCH 16/47] fix emails --- .../emails/includes/portfolio_domain_request_summary.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt b/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt index e0b62f93e..e1dff27ed 100644 --- a/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt +++ b/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt @@ -11,9 +11,7 @@ Requesting entity: {{domain_request.requested_suborganization}} {{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}} {% endif %} -Alternative domains: -{% for site in domain_request.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %} -{% endfor %}{% endif %} +{% if domain_request.current_websites.exists %} Current websites: {% for site in domain_request.current_websites.all %} {% spaceless %}{{ site.website }}{% endspaceless %} {% endfor %}{% endif %} @@ -25,8 +23,10 @@ Alternative domains: {% endfor %}{% endif %} Purpose of your domain: {{ domain_request.purpose }} +{% if domain_request.additional_details%} Additional details: {{ domain_request.additional_details }} +{% endif %} Your contact information: {% spaceless %}{% include "emails/includes/contact.txt" with contact=recipient %}{% endspaceless %} From 87d51c10028fa1525090d1f05c01a0a41c2d841d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:26:59 -0600 Subject: [PATCH 17/47] Fix bugs with email --- src/registrar/models/domain_request.py | 11 ++++-- src/registrar/models/portfolio.py | 13 +++++++ .../portfolio_domain_request_summary.txt | 34 ++++++------------- src/registrar/templatetags/custom_filters.py | 24 +++++++++++++ 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 33a9f2dea..68db5fb67 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1205,7 +1205,8 @@ def is_federal(self) -> Union[bool, None]: return False def is_suborganization(self) -> bool: - """Determines if this record is a suborganization or not""" + """Determines if this record is a suborganization or not by checking if a suborganization exists, + and if it doesn't, determining if properties like requested_suborganization exist.""" if self.portfolio: if self.sub_organization: return True @@ -1216,12 +1217,18 @@ def is_suborganization(self) -> bool: return False def is_custom_suborganization(self) -> bool: + """Used on the requesting entity form to determine if a user is trying to request + a new suborganization using the domain request form. + + This only occurs when no suborganization is selected, but they've filled out + the requested_suborganization, suborganization_city, and suborganization_state_territory fields. + """ if self.is_suborganization(): return not self.sub_organization and self.has_information_required_to_make_suborganization() else: return False - def has_information_required_to_make_suborganization(self): + def has_information_required_to_make_suborganization(self) -> bool: """Checks if we have all the information we need to create a new suborganization object. Checks for a the existence of requested_suborganization, suborganization_city, suborganization_state_territory""" if self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory: diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 8d820e105..82afcd4d6 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -2,6 +2,8 @@ from registrar.models.domain_request import DomainRequest from registrar.models.federal_agency import FederalAgency +from registrar.models.user import User +from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from .utility.time_stamped_model import TimeStampedModel @@ -131,6 +133,17 @@ def federal_type(self): def get_federal_type(cls, federal_agency): return federal_agency.federal_type if federal_agency else None + @property + def portfolio_admin_users(self): + """Gets all users with the role organization_admin for this particular portfolio. + Returns a queryset of User.""" + admin_ids = self.portfolio_users.filter( + roles__overlap=[ + UserPortfolioRoleChoices.ORGANIZATION_ADMIN, + ], + ).values_list("user__id", flat=True) + return User.objects.filter(id__in=admin_ids) + # == Getters for domains == # def get_domains(self, order_by=None): """Returns all DomainInformations associated with this portfolio""" diff --git a/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt b/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt index e1dff27ed..f2486dbe1 100644 --- a/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt +++ b/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt @@ -1,16 +1,7 @@ -SUMMARY OF YOUR DOMAIN REQUEST +{% load custom_filters %}SUMMARY OF YOUR DOMAIN REQUEST -Requesting entity: -{% if domain_request.portfolio and domain_request.organization_name == domain_request.portfolio.organization_name %} - {{domain_request.portfolio.organization_name}} - {{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}} -{% elif domain_request.sub_organization %} - {{domain_request.sub_organization}} - {% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %} -{% elif domain_request.requested_suborganization and domain_request.suborganization_city and domain_request.suborganization_state_territory %} - {{domain_request.requested_suborganization}} - {{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}} -{% endif %} +Requesting entity: {# if blockmakes a newline #} +{{ domain_request|display_requesting_entity }} {% if domain_request.current_websites.exists %} Current websites: {% for site in domain_request.current_websites.all %} {% spaceless %}{{ site.website }}{% endspaceless %} @@ -23,19 +14,16 @@ Alternative domains: {% endfor %}{% endif %} Purpose of your domain: {{ domain_request.purpose }} -{% if domain_request.additional_details%} +{% if domain_request.anything_else %} Additional details: -{{ domain_request.additional_details }} +{{ domain_request.anything_else }} {% endif %} - +{% if recipient %} Your contact information: {% spaceless %}{% include "emails/includes/contact.txt" with contact=recipient %}{% endspaceless %} - -Other employees from your organization:{% for other in domain_request.other_contacts.all %} -{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %} -{% empty %} -{{ domain_request.no_other_contacts_rationale }} -{% endfor %}{% if domain_request.anything_else %} -Anything else? -{{ domain_request.anything_else }} {% endif %} + +Administrators from your organization:{% for admin in domain_request.portfolio.portfolio_admin_users %} +{% spaceless %}{% if admin != recipient %}{% include "emails/includes/contact.txt" with contact=admin %}{% endif %}{% endspaceless %} +{% endfor %} + diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index b29dccb08..9e7298ac6 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -257,3 +257,27 @@ def portfolio_role_summary(user, portfolio): return user.portfolio_role_summary(portfolio) else: return [] + +@register.filter(name="display_requesting_entity") +def display_requesting_entity(domain_request): + """Workaround for a newline issue in .txt files (our emails) as if statements + count as a newline to the file. + Will output something that looks like: + MyOrganizationName + Boise, ID + """ + display = "" + if domain_request.portfolio and domain_request.organization_name == domain_request.portfolio.organization_name: + display = ( + f"{domain_request.portfolio.organization_name}\n" + f"{domain_request.portfolio.city}, {domain_request.portfolio.state_territory}" + ) + elif domain_request.sub_organization: + display = domain_request.sub_organization + elif domain_request.has_information_required_to_make_suborganization(): + display = ( + f"{domain_request.requested_suborganization}\n" + f"{domain_request.suborganization_city}, {domain_request.suborganization_state_territory}" + ) + + return display From 178d12711a13e25faf6b286a13e1eb5cea3dc2be Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:32:15 -0600 Subject: [PATCH 18/47] linting + add fields to django admin --- src/registrar/admin.py | 20 ++++- src/registrar/forms/domain_request_wizard.py | 25 ++++-- ...ion_requested_suborganization_and_more.py} | 84 ++++++++++++++++++- src/registrar/models/domain_information.py | 18 ++++ src/registrar/models/domain_request.py | 11 +-- src/registrar/templatetags/custom_filters.py | 1 + src/registrar/views/domain_request.py | 1 + 7 files changed, 143 insertions(+), 17 deletions(-) rename src/registrar/migrations/{0135_domainrequest_requested_suborganization_and_more.py => 0135_domaininformation_requested_suborganization_and_more.py} (52%) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index a4117e232..913820a14 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1478,7 +1478,21 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["portfolio", "sub_organization", "creator", "domain_request", "notes"]}), + ( + None, + { + "fields": [ + "portfolio", + "sub_organization", + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", + "creator", + "domain_request", + "notes", + ] + }, + ), (".gov domain", {"fields": ["domain"]}), ("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}), ("Background info", {"fields": ["anything_else"]}), @@ -1748,8 +1762,8 @@ def status_history(self, obj): "fields": [ "portfolio", "sub_organization", - "requested_suborganization", - "suborganization_city", + "requested_suborganization", + "suborganization_city", "suborganization_state_territory", "status_history", "status", diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index a8fa19146..c7b962f60 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -68,7 +68,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.domain_request.portfolio: - self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=self.domain_request.portfolio) + self.fields["sub_organization"].queryset = Suborganization.objects.filter( + portfolio=self.domain_request.portfolio + ) def clean_sub_organization(self): sub_organization = self.cleaned_data.get("sub_organization") @@ -82,15 +84,15 @@ def clean_sub_organization(self): def full_clean(self): # Remove the custom other field before cleaning data = self.data.copy() if self.data else None - suborganization = self.data.get('portfolio_requesting_entity-sub_organization') + suborganization = self.data.get("portfolio_requesting_entity-sub_organization") if suborganization: - if "other" in data['portfolio_requesting_entity-sub_organization']: + if "other" in data["portfolio_requesting_entity-sub_organization"]: # Remove the 'other' value - data['portfolio_requesting_entity-sub_organization'] = "" - + data["portfolio_requesting_entity-sub_organization"] = "" + # Set the modified data back to the form self.data = data - + # Call the parent's full_clean method super().full_clean() @@ -129,7 +131,10 @@ def __init__(self, *args, **kwargs): """Extend the initialization of the form from RegistrarForm __init__""" super().__init__(*args, **kwargs) if self.domain_request.portfolio: - self.form_choices = ((False, self.domain_request.portfolio), (True, "A suborganization. (choose from list)")) + self.form_choices = ( + (False, self.domain_request.portfolio), + (True, "A suborganization. (choose from list)"), + ) self.fields[self.field_name] = self.get_typed_choice_field() @property @@ -138,13 +143,17 @@ def form_is_checked(self): Determines the initial checked state of the form based on the domain_request's attributes. """ - if self.domain_request.portfolio and self.domain_request.organization_name == self.domain_request.portfolio.organization_name: + if ( + self.domain_request.portfolio + and self.domain_request.organization_name == self.domain_request.portfolio.organization_name + ): return False elif self.domain_request.is_suborganization(): return True else: return None + class OrganizationTypeForm(RegistrarForm): generic_org_type = forms.ChoiceField( # use the long names in the domain request form diff --git a/src/registrar/migrations/0135_domainrequest_requested_suborganization_and_more.py b/src/registrar/migrations/0135_domaininformation_requested_suborganization_and_more.py similarity index 52% rename from src/registrar/migrations/0135_domainrequest_requested_suborganization_and_more.py rename to src/registrar/migrations/0135_domaininformation_requested_suborganization_and_more.py index 92ca32697..b82619c78 100644 --- a/src/registrar/migrations/0135_domainrequest_requested_suborganization_and_more.py +++ b/src/registrar/migrations/0135_domaininformation_requested_suborganization_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-10-23 19:15 +# Generated by Django 4.2.10 on 2024-10-24 16:30 from django.db import migrations, models @@ -13,6 +13,88 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AddField( + model_name="domaininformation", + name="requested_suborganization", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domaininformation", + name="suborganization_city", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domaininformation", + name="suborganization_state_territory", + field=models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state, territory, or military post", + ), + ), migrations.AddField( model_name="domainrequest", name="requested_suborganization", diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 5f98197bd..a00098d66 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -75,6 +75,24 @@ class Meta: verbose_name="Suborganization", ) + requested_suborganization = models.CharField( + null=True, + blank=True, + ) + + suborganization_city = models.CharField( + null=True, + blank=True, + ) + + suborganization_state_territory = models.CharField( + max_length=2, + choices=StateTerritoryChoices.choices, + null=True, + blank=True, + verbose_name="state, territory, or military post", + ) + domain_request = models.OneToOneField( "registrar.DomainRequest", on_delete=models.PROTECT, diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 68db5fb67..d7c3a950b 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1129,7 +1129,7 @@ def reject_with_prejudice(self): # Form unlocking steps # These methods control the conditions in which we should unlock certain domain wizard steps. def unlock_requesting_entity(self) -> bool: - """Unlocks the requesting entity step """ + """Unlocks the requesting entity step""" if self.portfolio and self.organization_name == self.portfolio.organization_name: return True else: @@ -1215,11 +1215,11 @@ def is_suborganization(self) -> bool: return True return False - + def is_custom_suborganization(self) -> bool: """Used on the requesting entity form to determine if a user is trying to request a new suborganization using the domain request form. - + This only occurs when no suborganization is selected, but they've filled out the requested_suborganization, suborganization_city, and suborganization_state_territory fields. """ @@ -1227,10 +1227,11 @@ def is_custom_suborganization(self) -> bool: return not self.sub_organization and self.has_information_required_to_make_suborganization() else: return False - + def has_information_required_to_make_suborganization(self) -> bool: """Checks if we have all the information we need to create a new suborganization object. - Checks for a the existence of requested_suborganization, suborganization_city, suborganization_state_territory""" + Checks for a the existence of requested_suborganization, suborganization_city, suborganization_state_territory + """ if self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory: return True else: diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 9e7298ac6..50ff6c842 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -258,6 +258,7 @@ def portfolio_role_summary(user, portfolio): else: return [] + @register.filter(name="display_requesting_entity") def display_requesting_entity(domain_request): """Workaround for a newline issue in .txt files (our emails) as if statements diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index e88eb4c75..5a0d90240 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -614,6 +614,7 @@ def save(self, forms: list): super().save(forms) + class PortfolioAdditionalDetails(DomainRequestWizard): template_name = "portfolio_domain_request_additional_details.html" From 14bfeb7d9bfd34185714f8c897b20f49f2318ea0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:58:36 -0600 Subject: [PATCH 19/47] admin show/hide logic --- src/registrar/admin.py | 38 ++++++++++++ src/registrar/assets/js/get-gov-admin.js | 64 ++++++++++++++++++++ src/registrar/forms/domain_request_wizard.py | 1 - 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 913820a14..2fab7a735 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1885,6 +1885,27 @@ def status_history(self, obj): change_form_template = "django/admin/domain_request_change_form.html" + # While the organization feature is under development, we can gate some fields + # from analysts for now. Remove this array and the get_fieldset overrides once this is done. + # Not my code initially, credit to Nicolle. This was once removed and like a phoenix it has been reborn. + superuser_only_fields = [ + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", + ] + def get_fieldsets(self, request, obj=None): + fieldsets = super().get_fieldsets(request, obj) + + # Create a modified version of fieldsets to exclude certain fields + if not request.user.has_perm("registrar.full_access_permission"): + modified_fieldsets = [] + for name, data in fieldsets: + fields = data.get("fields", []) + fields = tuple(field for field in fields if field not in self.superuser_only_fields) + modified_fieldsets.append((name, {**data, "fields": fields})) + return modified_fieldsets + return fieldsets + # Trigger action when a fieldset is changed def save_model(self, request, obj, form, change): """Custom save_model definition that handles edge cases""" @@ -2297,6 +2318,15 @@ class DomainInformationInline(admin.StackedInline): analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields) autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields) + # While the organization feature is under development, we can gate some fields + # from analysts for now. Remove this array and the get_fieldset overrides once this is done. + # Not my code initially, credit to Nicolle. This was once removed and like a phoenix it has been reborn. + superuser_only_fields = [ + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", + ] + def get_domain_managers(self, obj): user_domain_roles = UserDomainRole.objects.filter(domain=obj.domain) user_ids = user_domain_roles.values_list("user_id", flat=True) @@ -2397,6 +2427,14 @@ def get_fieldsets(self, request, obj=None): # for permission-based field visibility. modified_fieldsets = copy.deepcopy(DomainInformationAdmin.get_fieldsets(self, request, obj=None)) + # Create a modified version of fieldsets to exclude certain fields + if not request.user.has_perm("registrar.full_access_permission"): + for name, data in modified_fieldsets: + fields = data.get("fields", []) + fields = tuple(field for field in fields if field not in self.superuser_only_fields) + modified_fieldsets.append((name, {**data, "fields": fields})) + return modified_fieldsets + # Modify fieldset sections in place for index, (title, options) in enumerate(modified_fieldsets): if title is None: diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index fd50fbb0c..9c770f40a 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -47,10 +47,49 @@ function addOrRemoveSessionBoolean(name, add){ // <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> // Event handlers. +/** Helper function that handles business logic for the suborganization field. + * Can be used anywhere the suborganization dropdown exists +*/ +function handleSuborganizationFields( + portfolioDropdownSelector="#id_portfolio", + suborgDropdownSelector="#id_sub_organization", + requestedSuborgFieldSelector=".field-requested_suborganization", + suborgCitySelector=".field-suborganization_city", + suborgStateTerritorySelector=".field-suborganization_state_territory" +) { + // These dropdown arecselect2 fields so they must be interacted with via jquery + const portfolioDropdown = django.jQuery(portfolioDropdownSelector) + const suborganizationDropdown = django.jQuery(suborgDropdownSelector) + const requestedSuborgField = document.querySelector(requestedSuborgFieldSelector); + const suborgCity = document.querySelector(suborgCitySelector); + const suborgStateTerritory = document.querySelector(suborgStateTerritorySelector); + if (!suborganizationDropdown || !requestedSuborgField || !suborgCity || !suborgStateTerritory) { + console.error("Requested suborg fields not found."); + return; + } + + function toggleSuborganizationFields() { + if (portfolioDropdown.val() && !suborganizationDropdown.val()) { + showElement(requestedSuborgField); + showElement(suborgCity); + showElement(suborgStateTerritory); + }else { + hideElement(requestedSuborgField); + hideElement(suborgCity); + hideElement(suborgStateTerritory); + } + } + + // Run the function once on page startup, then attach an event listener + toggleSuborganizationFields(); + suborganizationDropdown.on("change", toggleSuborganizationFields); + portfolioDropdown.on("change", toggleSuborganizationFields); +} // <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> // Initialization code. + /** An IIFE for pages in DjangoAdmin that use modals. * Dja strips out form elements, and modals generate their content outside * of the current form scope, so we need to "inject" these inputs. @@ -1170,3 +1209,28 @@ document.addEventListener('DOMContentLoaded', function() { }; } })(); + +/** An IIFE for dynamic DomainRequest fields +*/ +(function dynamicDomainRequestFields(){ + const domainRequestPage = document.getElementById("domainrequest_form"); + if (domainRequestPage) { + handleSuborganizationFields(); + } +})(); + + +/** An IIFE for dynamic DomainInformation fields +*/ +(function dynamicDomainInformationFields(){ + const domainInformationPage = document.getElementById("domaininformation_form"); + // DomainInformation is embedded inside domain so this should fire there too + const domainPage = document.getElementById("domain_form"); + if (domainInformationPage) { + handleSuborganizationFields(); + } + + if (domainPage) { + handleSuborganizationFields(portfolioDropdownSelector="#id_domain_info-0-portfolio", suborgDropdownSelector="#id_domain_info-0-sub_organization"); + } +})(); diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index c7b962f60..4aea46efe 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -75,7 +75,6 @@ def __init__(self, *args, **kwargs): def clean_sub_organization(self): sub_organization = self.cleaned_data.get("sub_organization") is_custom = self.cleaned_data.get("is_custom_suborganization") - print(f"in clean: {sub_organization}") if is_custom: # If it's a custom suborganization, return None (equivalent to selecting nothing) return None From 738ff0f95216db4574e6c0124de04db4c8342c85 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:12:07 -0600 Subject: [PATCH 20/47] unit test stuff --- src/registrar/admin.py | 17 ---- src/registrar/assets/js/get-gov.js | 13 +-- src/registrar/forms/domain_request_wizard.py | 9 ++ src/registrar/models/suborganization.py | 1 - src/registrar/tests/test_views_portfolio.py | 88 ++++++++++++++++++++ src/registrar/views/domain_request.py | 15 ++-- 6 files changed, 114 insertions(+), 29 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 2fab7a735..f0f8a3673 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2318,15 +2318,6 @@ class DomainInformationInline(admin.StackedInline): analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields) autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields) - # While the organization feature is under development, we can gate some fields - # from analysts for now. Remove this array and the get_fieldset overrides once this is done. - # Not my code initially, credit to Nicolle. This was once removed and like a phoenix it has been reborn. - superuser_only_fields = [ - "requested_suborganization", - "suborganization_city", - "suborganization_state_territory", - ] - def get_domain_managers(self, obj): user_domain_roles = UserDomainRole.objects.filter(domain=obj.domain) user_ids = user_domain_roles.values_list("user_id", flat=True) @@ -2427,14 +2418,6 @@ def get_fieldsets(self, request, obj=None): # for permission-based field visibility. modified_fieldsets = copy.deepcopy(DomainInformationAdmin.get_fieldsets(self, request, obj=None)) - # Create a modified version of fieldsets to exclude certain fields - if not request.user.has_perm("registrar.full_access_permission"): - for name, data in modified_fieldsets: - fields = data.get("fields", []) - fields = tuple(field for field in fields if field not in self.superuser_only_fields) - modified_fieldsets.append((name, {**data, "fields": fields})) - return modified_fieldsets - # Modify fieldset sections in place for index, (title, options) in enumerate(modified_fieldsets): if title is None: diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index cf6cbfb3a..64d12e400 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2410,6 +2410,10 @@ document.addEventListener('DOMContentLoaded', function() { } })(); +/** An IIFE that intializes the requesting entity page. + * This page has a radio button that dynamically toggles some fields + * Within that, the dropdown also toggles some additional form elements. +*/ (function handleRequestingEntityFieldset() { // Check if the requesting-entity-fieldset exists. // This determines if we are on the requesting entity page or not. @@ -2423,15 +2427,13 @@ document.addEventListener('DOMContentLoaded', function() { const subOrgSelect = document.querySelector(`#id_${formPrefix}-sub_organization`); // The suborganization section is its own div + // Within the suborganization section, we also have a div that contains orgname, city, and stateterritory. const suborganizationFieldset = document.querySelector("#requesting-entity-fieldset__suborganization"); - - // Within the suborganization section, we also have a div that contains orgname, city, and stateterritory const suborganizationDetailsFieldset = document.querySelector("#requesting-entity-fieldset__suborganization__details"); + // This variable determines if the user is trying to request a new suborganization or not var isCustomSuborganization = document.querySelector("#id_portfolio_requesting_entity-is_custom_suborganization") - // Use a variable to determine which option has been selected on the yes/no form. - // Don't do anything if we are missing crucial page elements if (!isSuborgRadios || !subOrgSelect || !suborganizationFieldset || !suborganizationDetailsFieldset) return; @@ -2486,5 +2488,4 @@ document.addEventListener('DOMContentLoaded', function() { subOrgSelect.addEventListener("change", () => { toggleSuborganizationDetails(); }); - -})(); \ No newline at end of file +})(); diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 4aea46efe..62b9056bf 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -22,6 +22,10 @@ class RequestingEntityForm(RegistrarForm): + """The requesting entity form contains a dropdown for suborganizations, + and some (hidden by default) input fields that allow the user to request for a suborganization. + All of these fields are not required by default, but as we use javascript to conditionally show + and hide some of these, they then become required in certain circumstances.""" sub_organization = forms.ModelChoiceField( label="Suborganization name", # not required because this field won't be filled out unless @@ -65,6 +69,7 @@ class RequestingEntityForm(RegistrarForm): is_custom_suborganization = forms.BooleanField(required=False, widget=forms.HiddenInput()) def __init__(self, *args, **kwargs): + """Override of init to add the suborganization queryset""" super().__init__(*args, **kwargs) if self.domain_request.portfolio: @@ -73,6 +78,8 @@ def __init__(self, *args, **kwargs): ) def clean_sub_organization(self): + """On suborganization clean, set the suborganization value to None if the user is requesting + a custom suborganization (as it doesn't exist yet)""" sub_organization = self.cleaned_data.get("sub_organization") is_custom = self.cleaned_data.get("is_custom_suborganization") if is_custom: @@ -81,6 +88,8 @@ def clean_sub_organization(self): return sub_organization def full_clean(self): + """Validation logic to remove the custom suborganization value before clean is triggered. + Without this override, the form will throw an 'invalid option' error.""" # Remove the custom other field before cleaning data = self.data.copy() if self.data else None suborganization = self.data.get("portfolio_requesting_entity-sub_organization") diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index 0b1c6e0ac..6ad80fdc0 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -1,5 +1,4 @@ from django.db import models -from registrar.models import DomainRequest from .utility.time_stamped_model import TimeStampedModel diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 13173565c..9d7122451 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1387,3 +1387,91 @@ def test_org_user_cannot_delete_others_domain_requests(self): # Check that the domain request still exists self.assertTrue(DomainRequest.objects.filter(pk=domain_request.pk).exists()) domain_request.delete() + + +class TestRequestingEntity(WebTest): + """The requesting entity page is a domain request form that only exists + within the context of a portfolio.""" + def setUp(self): + super().setUp() + self.client = Client() + self.user = create_test_user() + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + # Login the current user + self.app.set_user(self.user.username) + + def tearDown(self): + UserDomainRole.objects.all().delete() + DomainRequest.objects.all().delete() + DomainInformation.objects.all().delete() + Domain.objects.all().delete() + UserPortfolioPermission.objects.all().delete() + Portfolio.objects.all().delete() + User.objects.all().delete() + super().tearDown() + + # need a test that starts a new domain request + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_requesting_entity_page(self): + """Tests that the requesting entity page loads correctly""" + pass + + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_requesting_entity_page_submission(self): + """Tests that you can submit a form on this page""" + pass + + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_requesting_entity_page_errors(self): + """Tests that we get the expected form errors on requesting entity""" + domain_request = completed_domain_request(user=self.user, portfolio=self.portfolio) + UserPortfolioPermission.objects.create(portfolio=self.portfolio, user=self.user, roles=[ + UserPortfolioRoleChoices.ORGANIZATION_ADMIN + ]) + response = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow() + form = response.forms[0] + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Test missing suborganization selection + form['portfolio_requesting_entity-is_suborganization'] = True + form['portfolio_requesting_entity-sub_organization'] = "" + + response = form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.assertContains(response, "Select a suborganization.", status_code=200) + + # Test missing custom suborganization details + form['portfolio_requesting_entity-is_custom_suborganization'] = True + response = form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.assertContains(response, "Enter details for your organization name.", status_code=200) + self.assertContains(response, "Enter details for your city.", status_code=200) + self.assertContains(response, "Enter details for your state or territory.", status_code=200) + + domain_request.delete() + + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_requesting_entity_submission_email_sent(self, mock_send_email): + """Tests that an email is sent out on form submission""" + pass + + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_requesting_entity_viewonly(self): + """Tests the review steps page on under our viewonly context""" + pass + + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_requesting_entity_manage(self): + """Tests the review steps page on under our manage context""" + pass diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 5a0d90240..0a4728d81 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -592,6 +592,9 @@ class RequestingEntity(DomainRequestWizard): forms = [forms.RequestingEntityYesNoForm, forms.RequestingEntityForm] def save(self, forms: list): + """Override of save to clear or associate certain suborganization data + depending on what the user wishes to do. For instance, we want to add a suborganization + if the user selects one.""" requesting_entity_form = forms[1] cleaned_data = requesting_entity_form.cleaned_data is_suborganization = cleaned_data.get("is_suborganization") @@ -859,13 +862,15 @@ def has_permission(self): return True def get_context_data(self, **kwargs): + """Context override to add a step list to the context""" context = super().get_context_data(**kwargs) # Create a temp wizard object to grab the step list - wizard = PortfolioDomainRequestWizard() - wizard.request = self.request - context["Step"] = PortfolioDomainRequestStep.__members__ - context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep) - context["form_titles"] = wizard.titles + if self.request.user.is_org_user(self.request): + wizard = PortfolioDomainRequestWizard() + wizard.request = self.request + context["Step"] = PortfolioDomainRequestStep.__members__ + context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep) + context["form_titles"] = wizard.titles return context From d44bcadafb79063b9c22d495ce2d069c91d3965c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:37:55 -0600 Subject: [PATCH 21/47] unit tests --- src/registrar/admin.py | 1 + src/registrar/forms/domain_request_wizard.py | 1 + src/registrar/models/domain_request.py | 1 - .../emails/submission_confirmation.txt | 6 +- src/registrar/tests/test_views_portfolio.py | 252 ++++++++++++++++-- src/registrar/views/domain_request.py | 7 +- 6 files changed, 233 insertions(+), 35 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index f0f8a3673..fdadde436 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1893,6 +1893,7 @@ def status_history(self, obj): "suborganization_city", "suborganization_state_territory", ] + def get_fieldsets(self, request, obj=None): fieldsets = super().get_fieldsets(request, obj) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 62b9056bf..72421e7c9 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -26,6 +26,7 @@ class RequestingEntityForm(RegistrarForm): and some (hidden by default) input fields that allow the user to request for a suborganization. All of these fields are not required by default, but as we use javascript to conditionally show and hide some of these, they then become required in certain circumstances.""" + sub_organization = forms.ModelChoiceField( label="Suborganization name", # not required because this field won't be filled out unless diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index d7c3a950b..8ac12e085 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -6,7 +6,6 @@ from django.db import models from django_fsm import FSMField, transition # type: ignore from django.utils import timezone -from waffle import flag_is_active from registrar.models.domain import Domain from registrar.models.federal_agency import FederalAgency from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index 911269bf1..ef9736a9d 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -31,11 +31,7 @@ THANK YOU ---------------------------------------------------------------- -{% if is_org_user %} - {% include 'emails/includes/portfolio_domain_request_summary.txt' %} -{% else %} - {% include 'emails/includes/domain_request_summary.txt' %} -{% endif %} +{% if is_org_user %}{% include 'emails/includes/portfolio_domain_request_summary.txt' %}{% else %}{% include 'emails/includes/domain_request_summary.txt' %}{% endif %} ---------------------------------------------------------------- The .gov team diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 9d7122451..d770dd677 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -2,6 +2,7 @@ from api.tests.common import less_console_noise_decorator from registrar.config import settings from registrar.models import Portfolio, SeniorOfficial +from unittest.mock import MagicMock from django_webtest import WebTest # type: ignore from registrar.models import ( DomainRequest, @@ -9,12 +10,14 @@ DomainInformation, UserDomainRole, User, + Suborganization, + AllowedEmail, ) from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user_group import UserGroup from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices -from .common import MockSESClient, completed_domain_request, create_test_user +from .common import MockSESClient, completed_domain_request, create_test_user, create_user from waffle.testutils import override_flag from django.contrib.sessions.middleware import SessionMiddleware import boto3_mocking # type: ignore @@ -1392,86 +1395,283 @@ def test_org_user_cannot_delete_others_domain_requests(self): class TestRequestingEntity(WebTest): """The requesting entity page is a domain request form that only exists within the context of a portfolio.""" + def setUp(self): super().setUp() self.client = Client() - self.user = create_test_user() - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.user = create_user() self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + self.portfolio_2, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel Alaska") + self.suborganization, _ = Suborganization.objects.get_or_create( + name="Rocky road", + portfolio=self.portfolio, + ) + self.suborganization_2, _ = Suborganization.objects.get_or_create( + name="Vanilla", + portfolio=self.portfolio, + ) + self.unrelated_suborganization, _ = Suborganization.objects.get_or_create( + name="Cold", + portfolio=self.portfolio_2, + ) + self.portfolio_role = UserPortfolioPermission.objects.create( + portfolio=self.portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] ) # Login the current user self.app.set_user(self.user.username) + self.mock_client_class = MagicMock() + self.mock_client = self.mock_client_class.return_value + def tearDown(self): UserDomainRole.objects.all().delete() DomainRequest.objects.all().delete() DomainInformation.objects.all().delete() Domain.objects.all().delete() UserPortfolioPermission.objects.all().delete() + Suborganization.objects.all().delete() Portfolio.objects.all().delete() User.objects.all().delete() super().tearDown() - - # need a test that starts a new domain request + @override_flag("organization_feature", active=True) @override_flag("organization_requests", active=True) - def test_requesting_entity_page(self): - """Tests that the requesting entity page loads correctly""" - pass + @less_console_noise_decorator + def test_requesting_entity_page_new_request(self): + """Tests that the requesting entity page loads correctly when a new request is started""" + + response = self.app.get(reverse("domain-request:")) + + # Navigate past the intro page + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_form = response.forms[0] + response = intro_form.submit().follow() + + # Test the requesting entiy page + self.assertContains(response, "Who will use the domain you’re requesting?") + self.assertContains(response, "Add suborganization information") + # We expect to see the portfolio name in two places: + # the header, and as one of the radio button options. + self.assertContains(response, self.portfolio.organization_name, count=2) + + # We expect the dropdown list to contain the suborganizations that currently exist on this portfolio + self.assertContains(response, self.suborganization.name, count=1) + self.assertContains(response, self.suborganization_2.name, count=1) + + # However, we should only see suborgs that are on the actual portfolio + self.assertNotContains(response, self.unrelated_suborganization.name) @override_flag("organization_feature", active=True) @override_flag("organization_requests", active=True) - def test_requesting_entity_page_submission(self): - """Tests that you can submit a form on this page""" - pass + @less_console_noise_decorator + def test_requesting_entity_page_existing_suborg_submission(self): + """Tests that you can submit a form on this page and set a suborg""" + response = self.app.get(reverse("domain-request:")) + + # Navigate past the intro page + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + form = response.forms[0] + response = form.submit().follow() + + # Check that we're on the right page + self.assertContains(response, "Who will use the domain you’re requesting?") + form = response.forms[0] + + # Test selecting an existing suborg + form["portfolio_requesting_entity-is_suborganization"] = True + form["portfolio_requesting_entity-sub_organization"] = f"{self.suborganization.id}" + form["portfolio_requesting_entity-is_custom_suborganization"] = False + + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + response = form.submit().follow() + + # Ensure that the post occurred successfully by checking that we're on the following page. + self.assertContains(response, "Current websites") + created_domain_request_exists = DomainRequest.objects.filter( + organization_name__isnull=True, sub_organization=self.suborganization + ).exists() + self.assertTrue(created_domain_request_exists) @override_flag("organization_feature", active=True) @override_flag("organization_requests", active=True) + @less_console_noise_decorator + def test_requesting_entity_page_new_suborg_submission(self): + """Tests that you can submit a form on this page and set a new suborg""" + response = self.app.get(reverse("domain-request:")) + + # Navigate past the intro page + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + form = response.forms[0] + response = form.submit().follow() + + # Check that we're on the right page + self.assertContains(response, "Who will use the domain you’re requesting?") + form = response.forms[0] + + # Test selecting an existing suborg + form["portfolio_requesting_entity-is_suborganization"] = True + form["portfolio_requesting_entity-is_custom_suborganization"] = True + form["portfolio_requesting_entity-sub_organization"] = "" + + form["portfolio_requesting_entity-requested_suborganization"] = "moon" + form["portfolio_requesting_entity-suborganization_city"] = "kepler" + form["portfolio_requesting_entity-suborganization_state_territory"] = "AL" + + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + response = form.submit().follow() + + # Ensure that the post occurred successfully by checking that we're on the following page. + self.assertContains(response, "Current websites") + created_domain_request_exists = DomainRequest.objects.filter( + organization_name__isnull=True, + sub_organization__isnull=True, + requested_suborganization="moon", + suborganization_city="kepler", + suborganization_state_territory=DomainRequest.StateTerritoryChoices.ALABAMA, + ).exists() + self.assertTrue(created_domain_request_exists) + + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + @less_console_noise_decorator + def test_requesting_entity_page_organization_submission(self): + """Tests submitting an organization on the requesting org form""" + response = self.app.get(reverse("domain-request:")) + + # Navigate past the intro page + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + form = response.forms[0] + response = form.submit().follow() + + # Check that we're on the right page + self.assertContains(response, "Who will use the domain you’re requesting?") + form = response.forms[0] + + # Test selecting an existing suborg + form["portfolio_requesting_entity-is_suborganization"] = False + + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + response = form.submit().follow() + + # Ensure that the post occurred successfully by checking that we're on the following page. + self.assertContains(response, "Current websites") + created_domain_request_exists = DomainRequest.objects.filter( + organization_name=self.portfolio.organization_name, + ).exists() + self.assertTrue(created_domain_request_exists) + + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + @less_console_noise_decorator def test_requesting_entity_page_errors(self): """Tests that we get the expected form errors on requesting entity""" domain_request = completed_domain_request(user=self.user, portfolio=self.portfolio) - UserPortfolioPermission.objects.create(portfolio=self.portfolio, user=self.user, roles=[ - UserPortfolioRoleChoices.ORGANIZATION_ADMIN - ]) + UserPortfolioPermission.objects.create( + portfolio=self.portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) response = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow() form = response.forms[0] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # Test missing suborganization selection - form['portfolio_requesting_entity-is_suborganization'] = True - form['portfolio_requesting_entity-sub_organization'] = "" + form["portfolio_requesting_entity-is_suborganization"] = True + form["portfolio_requesting_entity-sub_organization"] = "" response = form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.assertContains(response, "Select a suborganization.", status_code=200) # Test missing custom suborganization details - form['portfolio_requesting_entity-is_custom_suborganization'] = True + form["portfolio_requesting_entity-is_custom_suborganization"] = True response = form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.assertContains(response, "Enter details for your organization name.", status_code=200) self.assertContains(response, "Enter details for your city.", status_code=200) self.assertContains(response, "Enter details for your state or territory.", status_code=200) - domain_request.delete() - @override_flag("organization_feature", active=True) @override_flag("organization_requests", active=True) - def test_requesting_entity_submission_email_sent(self, mock_send_email): - """Tests that an email is sent out on form submission""" - pass + @boto3_mocking.patching + @less_console_noise_decorator + def test_requesting_entity_submission_email_sent(self): + """Tests that an email is sent out on successful form submission""" + AllowedEmail.objects.create(email=self.user.email) + domain_request = completed_domain_request( + user=self.user, + # This is the additional details field + has_anything_else=True, + ) + domain_request.portfolio = self.portfolio + domain_request.requested_suborganization = "moon" + domain_request.suborganization_city = "kepler" + domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA + domain_request.save() + domain_request.refresh_from_db() + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + domain_request.submit() + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + self.assertNotIn("Anything else", body) + self.assertIn("kepler, AL", body) + self.assertIn("Requesting entity:", body) + self.assertIn("Administrators from your organization:", body) @override_flag("organization_feature", active=True) @override_flag("organization_requests", active=True) + @boto3_mocking.patching + @less_console_noise_decorator def test_requesting_entity_viewonly(self): """Tests the review steps page on under our viewonly context""" - pass + domain_request = completed_domain_request( + user=create_test_user(), + # This is the additional details field + has_anything_else=True, + ) + domain_request.portfolio = self.portfolio + domain_request.requested_suborganization = "moon" + domain_request.suborganization_city = "kepler" + domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA + domain_request.save() + domain_request.refresh_from_db() + + domain_request.submit() + + response = self.app.get(reverse("domain-request-status-viewonly", kwargs={"pk": domain_request.pk})) + self.assertContains(response, "Requesting entity") + self.assertContains(response, "moon") + self.assertContains(response, "kepler, AL") @override_flag("organization_feature", active=True) @override_flag("organization_requests", active=True) + @boto3_mocking.patching + @less_console_noise_decorator def test_requesting_entity_manage(self): """Tests the review steps page on under our manage context""" - pass + domain_request = completed_domain_request( + user=self.user, + # This is the additional details field + has_anything_else=True, + ) + domain_request.portfolio = self.portfolio + domain_request.requested_suborganization = "moon" + domain_request.suborganization_city = "kepler" + domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA + domain_request.save() + domain_request.refresh_from_db() + + domain_request.submit() + + response = self.app.get(reverse("domain-request-status", kwargs={"pk": domain_request.pk})) + self.assertContains(response, "Requesting entity") + self.assertContains(response, "moon") + self.assertContains(response, "kepler, AL") diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 0a4728d81..bdb496c9e 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -11,7 +11,6 @@ from registrar.forms.utility.wizard_form_helper import request_step_list from registrar.models import DomainRequest from registrar.models.contact import Contact -from registrar.models.suborganization import Suborganization from registrar.models.user import User from registrar.views.utility import StepsHelper from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView @@ -601,14 +600,16 @@ def save(self, forms: list): sub_organization = cleaned_data.get("sub_organization") requested_suborganization = cleaned_data.get("requested_suborganization") - # If no suborganization presently exists but the user filled out org information then create a suborg automatically. + # If no suborganization presently exists but the user filled out + # org information then create a suborg automatically. if is_suborganization and (sub_organization or requested_suborganization): # Cleanup the organization name field, as this isn't for suborganizations. self.domain_request.organization_name = None self.domain_request.sub_organization = sub_organization else: # If the user doesn't intend to create a suborg, simply don't make one and do some data cleanup - self.domain_request.organization_name = self.domain_request.portfolio.organization_name + if self.domain_request.portfolio: + self.domain_request.organization_name = self.domain_request.portfolio.organization_name self.domain_request.sub_organization = None self.domain_request.requested_suborganization = None From 934d06dfb5a56ef93fdc03df4df9a41bf51de107 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:52:22 -0600 Subject: [PATCH 22/47] lint and fix existing unit tests --- src/registrar/tests/test_views_portfolio.py | 3 --- src/registrar/tests/test_views_request.py | 2 -- src/registrar/views/domain_request.py | 2 +- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index d770dd677..6bf6ad783 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1573,9 +1573,6 @@ def test_requesting_entity_page_organization_submission(self): def test_requesting_entity_page_errors(self): """Tests that we get the expected form errors on requesting entity""" domain_request = completed_domain_request(user=self.user, portfolio=self.portfolio) - UserPortfolioPermission.objects.create( - portfolio=self.portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] - ) response = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow() form = response.forms[0] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 17e6bcbe6..73e538df3 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -2887,8 +2887,6 @@ def test_domain_request_withdraw_portfolio_redirects_correctly(self): detail_page = self.app.get(f"/domain-request/{domain_request.id}") self.assertContains(detail_page, "city.gov") self.assertContains(detail_page, "city1.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") self.assertContains(detail_page, "Status:") # click the "Withdraw request" button mock_client = MockSESClient() diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index bdb496c9e..d39833fab 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -600,7 +600,7 @@ def save(self, forms: list): sub_organization = cleaned_data.get("sub_organization") requested_suborganization = cleaned_data.get("requested_suborganization") - # If no suborganization presently exists but the user filled out + # If no suborganization presently exists but the user filled out # org information then create a suborg automatically. if is_suborganization and (sub_organization or requested_suborganization): # Cleanup the organization name field, as this isn't for suborganizations. From 0933fe4035e9b72237b15a2fd57b50ae29ca8305 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:22:49 -0600 Subject: [PATCH 23/47] fix weird spaces --- .../emails/includes/portfolio_domain_request_summary.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt b/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt index f2486dbe1..866fde50f 100644 --- a/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt +++ b/src/registrar/templates/emails/includes/portfolio_domain_request_summary.txt @@ -25,5 +25,4 @@ Your contact information: Administrators from your organization:{% for admin in domain_request.portfolio.portfolio_admin_users %} {% spaceless %}{% if admin != recipient %}{% include "emails/includes/contact.txt" with contact=admin %}{% endif %}{% endspaceless %} -{% endfor %} - +{% endfor %} \ No newline at end of file From 9531076f87934e5f7c972abe0bb847257ae9a2c4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:30:41 -0600 Subject: [PATCH 24/47] Update src/registrar/forms/domain_request_wizard.py --- src/registrar/forms/domain_request_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 72421e7c9..0ec0c6dac 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -107,7 +107,7 @@ def full_clean(self): def clean(self): """Custom clean implementation to handle our desired logic flow for suborganization. - Given that these fields often corely on eachother, we need to do this in the parent function.""" + Given that these fields often rely on eachother, we need to do this in the parent function.""" cleaned_data = super().clean() suborganization = self.cleaned_data.get("sub_organization") From d0aff60db0291278ef97e7d52917f287890f5b2c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:05:06 -0600 Subject: [PATCH 25/47] Update src/registrar/assets/js/get-gov-admin.js Co-authored-by: Rachid Mrad <107004823+rachidatecs@users.noreply.github.com> --- src/registrar/assets/js/get-gov-admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 9c770f40a..ed597d55f 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -57,7 +57,7 @@ function handleSuborganizationFields( suborgCitySelector=".field-suborganization_city", suborgStateTerritorySelector=".field-suborganization_state_territory" ) { - // These dropdown arecselect2 fields so they must be interacted with via jquery + // These dropdown are select2 fields so they must be interacted with via jquery const portfolioDropdown = django.jQuery(portfolioDropdownSelector) const suborganizationDropdown = django.jQuery(suborgDropdownSelector) const requestedSuborgField = document.querySelector(requestedSuborgFieldSelector); From b09e0cad4b6f0d49ea407977ba5f29fb1d4afbf8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:25:23 -0600 Subject: [PATCH 26/47] Fix bug with both org name and suborg --- src/registrar/forms/domain_request_wizard.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 0ec0c6dac..255eff332 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -152,13 +152,13 @@ def form_is_checked(self): Determines the initial checked state of the form based on the domain_request's attributes. """ - if ( + if self.domain_request.is_suborganization(): + return True + elif ( self.domain_request.portfolio and self.domain_request.organization_name == self.domain_request.portfolio.organization_name ): return False - elif self.domain_request.is_suborganization(): - return True else: return None From bc8789bc1b2e0bc8e616f6096d791269f64c071a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:22:39 -0600 Subject: [PATCH 27/47] Simplify logic and use better names --- src/registrar/assets/js/get-gov.js | 64 ++++---- src/registrar/forms/domain_request_wizard.py | 57 ++++--- src/registrar/models/domain_request.py | 74 ++++----- .../domain_request_requesting_entity.html | 10 +- .../templates/domain_request_status.html | 6 +- .../portfolio_request_review_steps.html | 28 ++-- .../includes/portfolio_status_manage.html | 7 - .../includes/request_status_manage.html | 144 +++++++++--------- src/registrar/templatetags/custom_filters.py | 14 +- src/registrar/tests/test_views_portfolio.py | 14 +- src/registrar/views/domain_request.py | 4 +- 11 files changed, 205 insertions(+), 217 deletions(-) delete mode 100644 src/registrar/templates/includes/portfolio_status_manage.html diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 6302e57f7..6182bc828 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2741,49 +2741,45 @@ document.addEventListener('DOMContentLoaded', function() { * This page has a radio button that dynamically toggles some fields * Within that, the dropdown also toggles some additional form elements. */ -(function handleRequestingEntityFieldset() { - // Check if the requesting-entity-fieldset exists. +(function handleRequestingEntityFieldset() { + // Sadly, these ugly ids are the auto generated with this prefix + const formPrefix = "portfolio_requesting_entity" + // This determines if we are on the requesting entity page or not. - const fieldset = document.getElementById("requesting-entity-fieldset"); - if (!fieldset) return; + const isSubOrgFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`); + if (!isSubOrgFieldset) return; // Get the is_suborganization radio buttons - // Sadly, these ugly ids are the auto generated - const formPrefix = "portfolio_requesting_entity" - const isSuborgRadios = document.querySelectorAll(`input[name="${formPrefix}-is_suborganization"]`); - const subOrgSelect = document.querySelector(`#id_${formPrefix}-sub_organization`); + const isSuborgRadios = isSubOrgFieldset.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`); + const subOrgSelect = document.getElementById(`id_${formPrefix}-sub_organization`); // The suborganization section is its own div // Within the suborganization section, we also have a div that contains orgname, city, and stateterritory. - const suborganizationFieldset = document.querySelector("#requesting-entity-fieldset__suborganization"); - const suborganizationDetailsFieldset = document.querySelector("#requesting-entity-fieldset__suborganization__details"); + const suborganizationContainer = document.getElementById("suborganization-container"); + const suborganizationDetailsContainer = document.getElementById("suborganization-container__details"); - // This variable determines if the user is trying to request a new suborganization or not - var isCustomSuborganization = document.querySelector("#id_portfolio_requesting_entity-is_custom_suborganization") + // This variable determines if the user is trying to *create* a new suborganization or not. + var isRequestingSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`) // Don't do anything if we are missing crucial page elements - if (!isSuborgRadios || !subOrgSelect || !suborganizationFieldset || !suborganizationDetailsFieldset) return; + if (!isSuborgRadios || !subOrgSelect || !suborganizationContainer || !suborganizationDetailsContainer) return; // Function to toggle suborganization based on is_suborganization selection function toggleSuborganization(radio) { if (radio && radio.checked && radio.value === "True") { - showElement(suborganizationFieldset); - toggleSuborganizationDetails(); - } else { - hideElement(suborganizationFieldset); - hideElement(suborganizationDetailsFieldset); - } - }; + showElement(suborganizationContainer); - // Function to toggle organization details based on sub_organization selection - function toggleSuborganizationDetails () { - // We should hide the org name fields when we select the special other value - if (subOrgSelect.value === "other") { - showElement(suborganizationDetailsFieldset); - isCustomSuborganization.value = "True"; + // Handle custom suborganizations + if (subOrgSelect.value === "other") { + showElement(suborganizationDetailsContainer); + isRequestingSuborganization.value = "True"; + } else { + hideElement(suborganizationDetailsContainer); + isRequestingSuborganization.value = "False"; + } } else { - hideElement(suborganizationDetailsFieldset); - isCustomSuborganization.value = "False"; + hideElement(suborganizationContainer); + hideElement(suborganizationDetailsContainer); } }; @@ -2795,7 +2791,8 @@ document.addEventListener('DOMContentLoaded', function() { subOrgSelect.add(fakeOption); } - if (isCustomSuborganization.value === "True") { + console.log(isRequestingSuborganization.value) + if (isRequestingSuborganization.value === "True") { subOrgSelect.value = "other" } @@ -2813,6 +2810,13 @@ document.addEventListener('DOMContentLoaded', function() { // Add event listener to the suborg dropdown to show/hide the suborg details section subOrgSelect.addEventListener("change", () => { - toggleSuborganizationDetails(); + // Handle the custom suborganization field + if (subOrgSelect.value === "other") { + showElement(suborganizationDetailsContainer); + isRequestingSuborganization.value = "True"; + } else { + hideElement(suborganizationDetailsContainer); + isRequestingSuborganization.value = "False"; + } }); })(); diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 255eff332..e6188eb33 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -27,6 +27,9 @@ class RequestingEntityForm(RegistrarForm): All of these fields are not required by default, but as we use javascript to conditionally show and hide some of these, they then become required in certain circumstances.""" + # Add a hidden field to store if the user is requesting a new suborganization + is_requesting_new_suborganization = forms.BooleanField(required=False, widget=forms.HiddenInput()) + sub_organization = forms.ModelChoiceField( label="Suborganization name", # not required because this field won't be filled out unless @@ -57,17 +60,6 @@ class RequestingEntityForm(RegistrarForm): "required": ("Select the state, territory, or military post where your organization is located.") }, ) - is_suborganization = forms.NullBooleanField( - widget=forms.RadioSelect( - choices=[ - (True, "Yes"), - (False, "No"), - ], - ) - ) - - # Add a hidden field to store that we are adding a custom suborg - is_custom_suborganization = forms.BooleanField(required=False, widget=forms.HiddenInput()) def __init__(self, *args, **kwargs): """Override of init to add the suborganization queryset""" @@ -81,23 +73,25 @@ def __init__(self, *args, **kwargs): def clean_sub_organization(self): """On suborganization clean, set the suborganization value to None if the user is requesting a custom suborganization (as it doesn't exist yet)""" - sub_organization = self.cleaned_data.get("sub_organization") - is_custom = self.cleaned_data.get("is_custom_suborganization") - if is_custom: - # If it's a custom suborganization, return None (equivalent to selecting nothing) + + # If it's a new suborganization, return None (equivalent to selecting nothing) + if self.cleaned_data.get("is_requesting_new_suborganization"): return None - return sub_organization + + # Otherwise just return the suborg as normal + return self.cleaned_data.get("sub_organization") def full_clean(self): """Validation logic to remove the custom suborganization value before clean is triggered. Without this override, the form will throw an 'invalid option' error.""" # Remove the custom other field before cleaning data = self.data.copy() if self.data else None + + # Remove the 'other' value from suborganization if it exists. + # This is a special value that tracks if the user is requesting a new suborg. suborganization = self.data.get("portfolio_requesting_entity-sub_organization") - if suborganization: - if "other" in data["portfolio_requesting_entity-sub_organization"]: - # Remove the 'other' value - data["portfolio_requesting_entity-sub_organization"] = "" + if suborganization and "other" in suborganization: + data["portfolio_requesting_entity-sub_organization"] = "" # Set the modified data back to the form self.data = data @@ -110,11 +104,16 @@ def clean(self): Given that these fields often rely on eachother, we need to do this in the parent function.""" cleaned_data = super().clean() + # Do some custom error validation if the requesting entity is a suborg. + # Otherwise, just validate as normal. suborganization = self.cleaned_data.get("sub_organization") - is_suborganization = self.cleaned_data.get("is_suborganization") - is_custom_suborganization = self.cleaned_data.get("is_custom_suborganization") - if is_suborganization: - if is_custom_suborganization: + is_requesting_new_suborganization = self.cleaned_data.get("is_requesting_new_suborganization") + + # Get the value of the yes/no checkbox from RequestingEntityYesNoForm. + # Since self.data stores this as a string, we need to convert "True" => True. + requesting_entity_is_suborganization = self.data.get("portfolio_requesting_entity-requesting_entity_is_suborganization") + if requesting_entity_is_suborganization == "True": + if is_requesting_new_suborganization: # Validate custom suborganization fields if not cleaned_data.get("requested_suborganization"): self.add_error("requested_suborganization", "Enter details for your organization name.") @@ -125,7 +124,6 @@ def clean(self): elif not suborganization: self.add_error("sub_organization", "Select a suborganization.") - cleaned_data = super().clean() return cleaned_data @@ -134,7 +132,7 @@ class RequestingEntityYesNoForm(BaseYesNoForm): # This first option will change dynamically form_choices = ((False, "Current Organization"), (True, "A suborganization. (choose from list)")) - field_name = "is_suborganization" + field_name = "requesting_entity_is_suborganization" def __init__(self, *args, **kwargs): """Extend the initialization of the form from RegistrarForm __init__""" @@ -152,12 +150,9 @@ def form_is_checked(self): Determines the initial checked state of the form based on the domain_request's attributes. """ - if self.domain_request.is_suborganization(): + if self.domain_request.requesting_entity_is_suborganization(): return True - elif ( - self.domain_request.portfolio - and self.domain_request.organization_name == self.domain_request.portfolio.organization_name - ): + elif self.domain_request.requesting_entity_is_portfolio(): return False else: return None diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 8ac12e085..a04cea526 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1125,16 +1125,49 @@ def reject_with_prejudice(self): self.creator.restrict_user() - # Form unlocking steps + def requesting_entity_is_portfolio(self) -> bool: + """Determines if this record is requesting that a portfolio be their organization.""" + return self.portfolio and self.organization_name == self.portfolio.organization_name + + def requesting_entity_is_suborganization(self) -> bool: + """Used to determine if this domain request is also requesting that it be tied to a suborganization. + Checks if this record has a suborganization or not by checking if a suborganization exists, + and if it doesn't, determining if properties like requested_suborganization exist. + """ + + if self.portfolio: + if self.sub_organization: + return True + if self.is_requesting_new_suborganization(): + return True + return False + + def is_requesting_new_suborganization(self) -> bool: + """Used on the requesting entity form to determine if a user is trying to request + a new suborganization using the domain request form. + + This only occurs when no suborganization is selected, but they've filled out + the requested_suborganization, suborganization_city, and suborganization_state_territory fields. + """ + # If a suborganization already exists, it can't possibly be a new one + if self.sub_organization: + return False + return bool(self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory) + + # ## Form unlocking steps ## # + # # These methods control the conditions in which we should unlock certain domain wizard steps. + def unlock_requesting_entity(self) -> bool: """Unlocks the requesting entity step""" - if self.portfolio and self.organization_name == self.portfolio.organization_name: + if self.requesting_entity_is_suborganization(): + return True + elif self.requesting_entity_is_portfolio(): return True else: - return self.is_suborganization() + return False - # ## Form policies ### + # ## Form policies ## # # # These methods control what questions need to be answered by applicants # during the domain request flow. They are policies about the domain request so @@ -1203,39 +1236,6 @@ def is_federal(self) -> Union[bool, None]: return True return False - def is_suborganization(self) -> bool: - """Determines if this record is a suborganization or not by checking if a suborganization exists, - and if it doesn't, determining if properties like requested_suborganization exist.""" - if self.portfolio: - if self.sub_organization: - return True - - if self.has_information_required_to_make_suborganization(): - return True - - return False - - def is_custom_suborganization(self) -> bool: - """Used on the requesting entity form to determine if a user is trying to request - a new suborganization using the domain request form. - - This only occurs when no suborganization is selected, but they've filled out - the requested_suborganization, suborganization_city, and suborganization_state_territory fields. - """ - if self.is_suborganization(): - return not self.sub_organization and self.has_information_required_to_make_suborganization() - else: - return False - - def has_information_required_to_make_suborganization(self) -> bool: - """Checks if we have all the information we need to create a new suborganization object. - Checks for a the existence of requested_suborganization, suborganization_city, suborganization_state_territory - """ - if self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory: - return True - else: - return False - def to_dict(self): """This is to process to_dict for Domain Information, making it friendly to "copy" it diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index 04ceb4a1a..d09e8ab89 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -7,7 +7,7 @@ {% endblock %} {% block form_fields %} -
+

Who will use the domain you’re requesting?

@@ -19,7 +19,7 @@

Who will use the domain you’re requesting?

{# forms.0 is a small yes/no form that toggles the visibility of "requesting entity" formset #} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% with attr_required=True %} - {% input_with_errors forms.0.is_suborganization %} + {% input_with_errors forms.0.requesting_entity_is_suborganization %} {% endwith %} {% endwith %} @@ -27,10 +27,10 @@

Who will use the domain you’re requesting?

was selected or not. This allows for persistence across page reloads without using session variables. {% endcomment %} {% with add_group_class="display-none" %} - {% input_with_errors forms.1.is_custom_suborganization %} + {% input_with_errors forms.1.is_requesting_new_suborganization %} {% endwith %} -
+

Add suborganization information

This information will be published in .gov’s public data. If you don’t see your suborganization in the list, @@ -43,7 +43,7 @@

Add suborganization information

{% comment %} This will be toggled if a special value, "other", is selected. Otherwise this field is invisible. {% endcomment %} -
+
{% with attr_required=True %} {% input_with_errors forms.1.requested_suborganization %} {% endwith %} diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index d98220532..d332ce54e 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -6,9 +6,5 @@ {% block content %} - {% if not portfolio %} - {% include "includes/request_status_manage.html" %} - {% else %} - {% include "includes/portfolio_status_manage.html" %} - {% endif %} + {% include "includes/request_status_manage.html" %} {% endblock %} diff --git a/src/registrar/templates/includes/portfolio_request_review_steps.html b/src/registrar/templates/includes/portfolio_request_review_steps.html index c2bdb18cc..ed727fee8 100644 --- a/src/registrar/templates/includes/portfolio_request_review_steps.html +++ b/src/registrar/templates/includes/portfolio_request_review_steps.html @@ -8,28 +8,24 @@ {% endif %} {% if step == Step.REQUESTING_ENTITY %} - {% if domain_request.portfolio and domain_request.organization_name == domain_request.portfolio.organization_name %} - {% with title=form_titles|get_item:step value=domain_request.portfolio.organization_name %} - {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} - {% endwith %} + {% with title=form_titles|get_item:step %} + {% if domain_request.sub_organization %} + {% include "includes/summary_item.html" with value=domain_request.sub_organization edit_link=domain_request_url %} + {% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %} + {% elif domain_request.requesting_entity_is_suborganization %} + {% include "includes/summary_item.html" with value=domain_request.requested_suborganization edit_link=domain_request_url %} +

{{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}

+ {% elif domain_request.requesting_entity_is_portfolio %} + {% include "includes/summary_item.html" with value=domain_request.portfolio.organization_name edit_link=domain_request_url %} {% if domain_request.portfolio.city and domain_request.portfolio.state_territory %}

{{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}}

{% endif %} - {% elif domain_request.sub_organization %} - {% with title=form_titles|get_item:step value=domain_request.sub_organization %} - {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} - {% endwith %} - {% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %} - {% elif domain_request.requested_suborganization and domain_request.suborganization_city and domain_request.suborganization_state_territory %} - {% with title=form_titles|get_item:step value=domain_request.requested_suborganization %} - {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} - {% endwith %} -

{{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}

{% else %} - {% with title=form_titles|get_item:step value="Incomplete"|safe %} - {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %} + {% with value="Incomplete"|safe %} + {% include "includes/summary_item.html" with edit_link=domain_request_url %} {% endwith %} {% endif %} + {% endwith %} {% endif %} {% if step == Step.CURRENT_SITES %} diff --git a/src/registrar/templates/includes/portfolio_status_manage.html b/src/registrar/templates/includes/portfolio_status_manage.html deleted file mode 100644 index 120770156..000000000 --- a/src/registrar/templates/includes/portfolio_status_manage.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "includes/request_status_manage.html" %} -{% load custom_filters %} -{% load static url_helpers %} - -{% block request_summary %} - {% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %} -{% endblock request_summary %} diff --git a/src/registrar/templates/includes/request_status_manage.html b/src/registrar/templates/includes/request_status_manage.html index 2a254df4b..fc2fd8f12 100644 --- a/src/registrar/templates/includes/request_status_manage.html +++ b/src/registrar/templates/includes/request_status_manage.html @@ -145,92 +145,96 @@

Summary of your domain request

{% endblock request_summary_header%} {% block request_summary %} - {% with heading_level='h3' %} - {% with org_type=DomainRequest.get_generic_org_type_display %} - {% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %} - {% endwith %} + {% if portfolio %} + {% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %} + {% else %} + {% with heading_level='h3' %} + {% with org_type=DomainRequest.get_generic_org_type_display %} + {% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %} + {% endwith %} - {% if DomainRequest.tribe_name %} - {% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %} + {% if DomainRequest.tribe_name %} + {% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %} - {% if DomainRequest.federally_recognized_tribe %} -

Federally-recognized tribe

- {% endif %} + {% if DomainRequest.federally_recognized_tribe %} +

Federally-recognized tribe

+ {% endif %} - {% if DomainRequest.state_recognized_tribe %} -

State-recognized tribe

- {% endif %} + {% if DomainRequest.state_recognized_tribe %} +

State-recognized tribe

+ {% endif %} - {% endif %} + {% endif %} - {% if DomainRequest.get_federal_type_display %} - {% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %} - {% endif %} + {% if DomainRequest.get_federal_type_display %} + {% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %} + {% endif %} - {% if DomainRequest.is_election_board %} - {% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %} - {% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %} - {% endwith %} - {% endif %} + {% if DomainRequest.is_election_board %} + {% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %} + {% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %} + {% endwith %} + {% endif %} - {% if DomainRequest.organization_name %} - {% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %} - {% endif %} + {% if DomainRequest.organization_name %} + {% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %} + {% endif %} - {% if DomainRequest.about_your_organization %} - {% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %} - {% endif %} + {% if DomainRequest.about_your_organization %} + {% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %} + {% endif %} - {% if DomainRequest.senior_official %} - {% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %} - {% endif %} + {% if DomainRequest.senior_official %} + {% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %} + {% endif %} - {% if DomainRequest.current_websites.all %} - {% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %} - {% endif %} + {% if DomainRequest.current_websites.all %} + {% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %} + {% endif %} - {% if DomainRequest.requested_domain %} - {% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %} - {% endif %} + {% if DomainRequest.requested_domain %} + {% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %} + {% endif %} - {% if DomainRequest.alternative_domains.all %} - {% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %} - {% endif %} + {% if DomainRequest.alternative_domains.all %} + {% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %} + {% endif %} - {% if DomainRequest.purpose %} - {% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %} - {% endif %} + {% if DomainRequest.purpose %} + {% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %} + {% endif %} - {% if DomainRequest.creator %} - {% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %} - {% endif %} + {% if DomainRequest.creator %} + {% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %} + {% endif %} - {% if DomainRequest.other_contacts.all %} - {% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %} - {% else %} - {% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %} - {% endif %} + {% if DomainRequest.other_contacts.all %} + {% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %} + {% else %} + {% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %} + {% endif %} - {# We always show this field even if None #} - {% if DomainRequest %} -

CISA Regional Representative

-
    - {% if DomainRequest.cisa_representative_first_name %} - {{ DomainRequest.get_formatted_cisa_rep_name }} - {% else %} - No - {% endif %} -
-

Anything else

-
    - {% if DomainRequest.anything_else %} - {{DomainRequest.anything_else}} - {% else %} - No - {% endif %} -
- {% endif %} - {% endwith %} + {# We always show this field even if None #} + {% if DomainRequest %} +

CISA Regional Representative

+
    + {% if DomainRequest.cisa_representative_first_name %} + {{ DomainRequest.get_formatted_cisa_rep_name }} + {% else %} + No + {% endif %} +
+

Anything else

+
    + {% if DomainRequest.anything_else %} + {{DomainRequest.anything_else}} + {% else %} + No + {% endif %} +
+ {% endif %} + {% endwith %} + {% endif %} {% endblock request_summary%}
\ No newline at end of file diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 50ff6c842..bd977d581 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -268,17 +268,17 @@ def display_requesting_entity(domain_request): Boise, ID """ display = "" - if domain_request.portfolio and domain_request.organization_name == domain_request.portfolio.organization_name: - display = ( - f"{domain_request.portfolio.organization_name}\n" - f"{domain_request.portfolio.city}, {domain_request.portfolio.state_territory}" - ) - elif domain_request.sub_organization: + if domain_request.sub_organization: display = domain_request.sub_organization - elif domain_request.has_information_required_to_make_suborganization(): + elif domain_request.requesting_entity_is_suborganization(): display = ( f"{domain_request.requested_suborganization}\n" f"{domain_request.suborganization_city}, {domain_request.suborganization_state_territory}" ) + elif domain_request.requesting_entity_is_portfolio(): + display = ( + f"{domain_request.portfolio.organization_name}\n" + f"{domain_request.portfolio.city}, {domain_request.portfolio.state_territory}" + ) return display diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 2469653bf..1ca2d2bcd 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1685,9 +1685,9 @@ def test_requesting_entity_page_existing_suborg_submission(self): form = response.forms[0] # Test selecting an existing suborg - form["portfolio_requesting_entity-is_suborganization"] = True + form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True form["portfolio_requesting_entity-sub_organization"] = f"{self.suborganization.id}" - form["portfolio_requesting_entity-is_custom_suborganization"] = False + form["portfolio_requesting_entity-is_requesting_new_suborganization"] = False session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -1718,8 +1718,8 @@ def test_requesting_entity_page_new_suborg_submission(self): form = response.forms[0] # Test selecting an existing suborg - form["portfolio_requesting_entity-is_suborganization"] = True - form["portfolio_requesting_entity-is_custom_suborganization"] = True + form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True + form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True form["portfolio_requesting_entity-sub_organization"] = "" form["portfolio_requesting_entity-requested_suborganization"] = "moon" @@ -1759,7 +1759,7 @@ def test_requesting_entity_page_organization_submission(self): form = response.forms[0] # Test selecting an existing suborg - form["portfolio_requesting_entity-is_suborganization"] = False + form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = False session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -1784,7 +1784,7 @@ def test_requesting_entity_page_errors(self): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # Test missing suborganization selection - form["portfolio_requesting_entity-is_suborganization"] = True + form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True form["portfolio_requesting_entity-sub_organization"] = "" response = form.submit() @@ -1792,7 +1792,7 @@ def test_requesting_entity_page_errors(self): self.assertContains(response, "Select a suborganization.", status_code=200) # Test missing custom suborganization details - form["portfolio_requesting_entity-is_custom_suborganization"] = True + form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True response = form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.assertContains(response, "Enter details for your organization name.", status_code=200) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index d39833fab..16ca7a69f 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -596,13 +596,13 @@ def save(self, forms: list): if the user selects one.""" requesting_entity_form = forms[1] cleaned_data = requesting_entity_form.cleaned_data - is_suborganization = cleaned_data.get("is_suborganization") + requesting_entity_is_suborganization = cleaned_data.get("requesting_entity_is_suborganization") sub_organization = cleaned_data.get("sub_organization") requested_suborganization = cleaned_data.get("requested_suborganization") # If no suborganization presently exists but the user filled out # org information then create a suborg automatically. - if is_suborganization and (sub_organization or requested_suborganization): + if requesting_entity_is_suborganization and (sub_organization or requested_suborganization): # Cleanup the organization name field, as this isn't for suborganizations. self.domain_request.organization_name = None self.domain_request.sub_organization = sub_organization From ee71ba4c48d55648dfbbabb6763fe8aeb6d57deb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:59:24 -0600 Subject: [PATCH 28/47] cleanup --- src/registrar/assets/js/get-gov.js | 6 +- src/registrar/forms/domain_request_wizard.py | 19 +- ...tion_requested_suborganization_and_more.py | 180 ------------------ src/registrar/views/domain_request.py | 2 - 4 files changed, 13 insertions(+), 194 deletions(-) delete mode 100644 src/registrar/migrations/0135_domaininformation_requested_suborganization_and_more.py diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 6182bc828..81bb30e38 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -22,8 +22,7 @@ var SUCCESS = "success"; * */ const hideElement = (element) => { - if (element && !element.classList.contains("display-none")) - element.classList.add('display-none'); + element.classList.add('display-none'); }; /** @@ -31,8 +30,7 @@ const hideElement = (element) => { * */ const showElement = (element) => { - if (element && element.classList.contains("display-none")) - element.classList.remove('display-none'); + element.classList.remove('display-none'); }; /** diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index e6188eb33..2bc31fc37 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -27,21 +27,21 @@ class RequestingEntityForm(RegistrarForm): All of these fields are not required by default, but as we use javascript to conditionally show and hide some of these, they then become required in certain circumstances.""" - # Add a hidden field to store if the user is requesting a new suborganization + # Add a hidden field to store if the user is requesting a new suborganization. + # This hidden boolean is used for our javascript to communicate to us and to it. + # If true, the suborganization form will auto select a js value "Other". + # If this selection is made on the form (tracked by js), then it will toggle the form value of this. + # In other words, this essentially tracks if the suborganization field == "Other". + # "Other" is just an imaginary value that is otherwise invalid. + # Note the logic in `def clean` and line 2744 in get-gov.js is_requesting_new_suborganization = forms.BooleanField(required=False, widget=forms.HiddenInput()) sub_organization = forms.ModelChoiceField( label="Suborganization name", - # not required because this field won't be filled out unless - # it is a federal agency. Use clean to check programatically - # if it has been filled in when required. required=False, queryset=Suborganization.objects.none(), empty_label="--Select--", ) - - # We are using the current sub_organization naming convention here. - # We may want to refactor this to suborganization eventually. requested_suborganization = forms.CharField( label="Requested suborganization", required=False, @@ -147,7 +147,10 @@ def __init__(self, *args, **kwargs): @property def form_is_checked(self): """ - Determines the initial checked state of the form based on the domain_request's attributes. + Determines if the requesting entity is a suborganization, or a portfolio. + For suborganizations, users have the ability to request a new one if the + desired suborg doesn't exist. We expose additional fields that denote this, + like `requested_suborganization`. So we also check on those. """ if self.domain_request.requesting_entity_is_suborganization(): diff --git a/src/registrar/migrations/0135_domaininformation_requested_suborganization_and_more.py b/src/registrar/migrations/0135_domaininformation_requested_suborganization_and_more.py deleted file mode 100644 index b82619c78..000000000 --- a/src/registrar/migrations/0135_domaininformation_requested_suborganization_and_more.py +++ /dev/null @@ -1,180 +0,0 @@ -# Generated by Django 4.2.10 on 2024-10-24 16:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "registrar", - "0134_rename_portfolio_additional_permissions_portfolioinvitation_additional_permissions_and_more", - ), - ] - - operations = [ - migrations.AddField( - model_name="domaininformation", - name="requested_suborganization", - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name="domaininformation", - name="suborganization_city", - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name="domaininformation", - name="suborganization_state_territory", - field=models.CharField( - blank=True, - choices=[ - ("AL", "Alabama (AL)"), - ("AK", "Alaska (AK)"), - ("AS", "American Samoa (AS)"), - ("AZ", "Arizona (AZ)"), - ("AR", "Arkansas (AR)"), - ("CA", "California (CA)"), - ("CO", "Colorado (CO)"), - ("CT", "Connecticut (CT)"), - ("DE", "Delaware (DE)"), - ("DC", "District of Columbia (DC)"), - ("FL", "Florida (FL)"), - ("GA", "Georgia (GA)"), - ("GU", "Guam (GU)"), - ("HI", "Hawaii (HI)"), - ("ID", "Idaho (ID)"), - ("IL", "Illinois (IL)"), - ("IN", "Indiana (IN)"), - ("IA", "Iowa (IA)"), - ("KS", "Kansas (KS)"), - ("KY", "Kentucky (KY)"), - ("LA", "Louisiana (LA)"), - ("ME", "Maine (ME)"), - ("MD", "Maryland (MD)"), - ("MA", "Massachusetts (MA)"), - ("MI", "Michigan (MI)"), - ("MN", "Minnesota (MN)"), - ("MS", "Mississippi (MS)"), - ("MO", "Missouri (MO)"), - ("MT", "Montana (MT)"), - ("NE", "Nebraska (NE)"), - ("NV", "Nevada (NV)"), - ("NH", "New Hampshire (NH)"), - ("NJ", "New Jersey (NJ)"), - ("NM", "New Mexico (NM)"), - ("NY", "New York (NY)"), - ("NC", "North Carolina (NC)"), - ("ND", "North Dakota (ND)"), - ("MP", "Northern Mariana Islands (MP)"), - ("OH", "Ohio (OH)"), - ("OK", "Oklahoma (OK)"), - ("OR", "Oregon (OR)"), - ("PA", "Pennsylvania (PA)"), - ("PR", "Puerto Rico (PR)"), - ("RI", "Rhode Island (RI)"), - ("SC", "South Carolina (SC)"), - ("SD", "South Dakota (SD)"), - ("TN", "Tennessee (TN)"), - ("TX", "Texas (TX)"), - ("UM", "United States Minor Outlying Islands (UM)"), - ("UT", "Utah (UT)"), - ("VT", "Vermont (VT)"), - ("VI", "Virgin Islands (VI)"), - ("VA", "Virginia (VA)"), - ("WA", "Washington (WA)"), - ("WV", "West Virginia (WV)"), - ("WI", "Wisconsin (WI)"), - ("WY", "Wyoming (WY)"), - ("AA", "Armed Forces Americas (AA)"), - ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), - ("AP", "Armed Forces Pacific (AP)"), - ], - max_length=2, - null=True, - verbose_name="state, territory, or military post", - ), - ), - migrations.AddField( - model_name="domainrequest", - name="requested_suborganization", - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name="domainrequest", - name="suborganization_city", - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name="domainrequest", - name="suborganization_state_territory", - field=models.CharField( - blank=True, - choices=[ - ("AL", "Alabama (AL)"), - ("AK", "Alaska (AK)"), - ("AS", "American Samoa (AS)"), - ("AZ", "Arizona (AZ)"), - ("AR", "Arkansas (AR)"), - ("CA", "California (CA)"), - ("CO", "Colorado (CO)"), - ("CT", "Connecticut (CT)"), - ("DE", "Delaware (DE)"), - ("DC", "District of Columbia (DC)"), - ("FL", "Florida (FL)"), - ("GA", "Georgia (GA)"), - ("GU", "Guam (GU)"), - ("HI", "Hawaii (HI)"), - ("ID", "Idaho (ID)"), - ("IL", "Illinois (IL)"), - ("IN", "Indiana (IN)"), - ("IA", "Iowa (IA)"), - ("KS", "Kansas (KS)"), - ("KY", "Kentucky (KY)"), - ("LA", "Louisiana (LA)"), - ("ME", "Maine (ME)"), - ("MD", "Maryland (MD)"), - ("MA", "Massachusetts (MA)"), - ("MI", "Michigan (MI)"), - ("MN", "Minnesota (MN)"), - ("MS", "Mississippi (MS)"), - ("MO", "Missouri (MO)"), - ("MT", "Montana (MT)"), - ("NE", "Nebraska (NE)"), - ("NV", "Nevada (NV)"), - ("NH", "New Hampshire (NH)"), - ("NJ", "New Jersey (NJ)"), - ("NM", "New Mexico (NM)"), - ("NY", "New York (NY)"), - ("NC", "North Carolina (NC)"), - ("ND", "North Dakota (ND)"), - ("MP", "Northern Mariana Islands (MP)"), - ("OH", "Ohio (OH)"), - ("OK", "Oklahoma (OK)"), - ("OR", "Oregon (OR)"), - ("PA", "Pennsylvania (PA)"), - ("PR", "Puerto Rico (PR)"), - ("RI", "Rhode Island (RI)"), - ("SC", "South Carolina (SC)"), - ("SD", "South Dakota (SD)"), - ("TN", "Tennessee (TN)"), - ("TX", "Texas (TX)"), - ("UM", "United States Minor Outlying Islands (UM)"), - ("UT", "Utah (UT)"), - ("VT", "Vermont (VT)"), - ("VI", "Virgin Islands (VI)"), - ("VA", "Virginia (VA)"), - ("WA", "Washington (WA)"), - ("WV", "West Virginia (WV)"), - ("WI", "Wisconsin (WI)"), - ("WY", "Wyoming (WY)"), - ("AA", "Armed Forces Americas (AA)"), - ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), - ("AP", "Armed Forces Pacific (AP)"), - ], - max_length=2, - null=True, - verbose_name="state, territory, or military post", - ), - ), - ] diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 16ca7a69f..209a6c100 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -600,8 +600,6 @@ def save(self, forms: list): sub_organization = cleaned_data.get("sub_organization") requested_suborganization = cleaned_data.get("requested_suborganization") - # If no suborganization presently exists but the user filled out - # org information then create a suborg automatically. if requesting_entity_is_suborganization and (sub_organization or requested_suborganization): # Cleanup the organization name field, as this isn't for suborganizations. self.domain_request.organization_name = None From a94a5b20759f8cb3a5f9094fd4b512e7a81fdf3f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:59:53 -0600 Subject: [PATCH 29/47] Readd migration after merge --- ...tion_requested_suborganization_and_more.py | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 src/registrar/migrations/0136_domaininformation_requested_suborganization_and_more.py diff --git a/src/registrar/migrations/0136_domaininformation_requested_suborganization_and_more.py b/src/registrar/migrations/0136_domaininformation_requested_suborganization_and_more.py new file mode 100644 index 000000000..dc6ec9d5d --- /dev/null +++ b/src/registrar/migrations/0136_domaininformation_requested_suborganization_and_more.py @@ -0,0 +1,177 @@ +# Generated by Django 4.2.10 on 2024-10-30 17:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0135_alter_federalagency_agency_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="domaininformation", + name="requested_suborganization", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domaininformation", + name="suborganization_city", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domaininformation", + name="suborganization_state_territory", + field=models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state, territory, or military post", + ), + ), + migrations.AddField( + model_name="domainrequest", + name="requested_suborganization", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domainrequest", + name="suborganization_city", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="domainrequest", + name="suborganization_state_territory", + field=models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state, territory, or military post", + ), + ), + ] From c465b7fb67c5ff2995a1cc30ecc4e062ba3a5acf Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:20:26 -0600 Subject: [PATCH 30/47] fix bug with form save Since cleaned data was not being touched, the form was not resetting values correctly --- src/registrar/assets/js/get-gov.js | 1 - src/registrar/forms/domain_request_wizard.py | 4 ++- src/registrar/tests/test_views_portfolio.py | 1 - src/registrar/views/domain_request.py | 27 ++++++++++++-------- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 81bb30e38..be658ad3e 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2789,7 +2789,6 @@ document.addEventListener('DOMContentLoaded', function() { subOrgSelect.add(fakeOption); } - console.log(isRequestingSuborganization.value) if (isRequestingSuborganization.value === "True") { subOrgSelect.value = "other" } diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 2bc31fc37..b27a004c0 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -111,7 +111,9 @@ def clean(self): # Get the value of the yes/no checkbox from RequestingEntityYesNoForm. # Since self.data stores this as a string, we need to convert "True" => True. - requesting_entity_is_suborganization = self.data.get("portfolio_requesting_entity-requesting_entity_is_suborganization") + requesting_entity_is_suborganization = self.data.get( + "portfolio_requesting_entity-requesting_entity_is_suborganization" + ) if requesting_entity_is_suborganization == "True": if is_requesting_new_suborganization: # Validate custom suborganization fields diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 1ca2d2bcd..45357cbf7 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1717,7 +1717,6 @@ def test_requesting_entity_page_new_suborg_submission(self): self.assertContains(response, "Who will use the domain you’re requesting?") form = response.forms[0] - # Test selecting an existing suborg form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True form["portfolio_requesting_entity-sub_organization"] = "" diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 209a6c100..7fb2d9b16 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -594,25 +594,32 @@ def save(self, forms: list): """Override of save to clear or associate certain suborganization data depending on what the user wishes to do. For instance, we want to add a suborganization if the user selects one.""" + yesno_form = forms[0] requesting_entity_form = forms[1] + + yesno_cleaned_data = yesno_form.cleaned_data + requesting_entity_is_suborganization = yesno_cleaned_data.get("requesting_entity_is_suborganization") + cleaned_data = requesting_entity_form.cleaned_data - requesting_entity_is_suborganization = cleaned_data.get("requesting_entity_is_suborganization") sub_organization = cleaned_data.get("sub_organization") requested_suborganization = cleaned_data.get("requested_suborganization") if requesting_entity_is_suborganization and (sub_organization or requested_suborganization): # Cleanup the organization name field, as this isn't for suborganizations. - self.domain_request.organization_name = None - self.domain_request.sub_organization = sub_organization + requesting_entity_form.cleaned_data.update({"organization_name": None}) else: # If the user doesn't intend to create a suborg, simply don't make one and do some data cleanup - if self.domain_request.portfolio: - self.domain_request.organization_name = self.domain_request.portfolio.organization_name - - self.domain_request.sub_organization = None - self.domain_request.requested_suborganization = None - self.domain_request.suborganization_city = None - self.domain_request.suborganization_state_territory = None + requesting_entity_form.cleaned_data.update( + { + "organization_name": ( + self.domain_request.portfolio.organization_name if self.domain_request.portfolio else None + ), + "sub_organization": None, + "requested_suborganization": None, + "suborganization_city": None, + "suborganization_state_territory": None, + } + ) super().save(forms) From dfb59a6a9d3ab4be10a941dbc32db390594f9a52 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:21:35 -0600 Subject: [PATCH 31/47] lint model --- src/registrar/models/domain_request.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index a04cea526..96d98ed65 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1152,7 +1152,9 @@ def is_requesting_new_suborganization(self) -> bool: # If a suborganization already exists, it can't possibly be a new one if self.sub_organization: return False - return bool(self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory) + return bool( + self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory + ) # ## Form unlocking steps ## # # From 4f1febf24820589b1339c5cb8b92bbbcef2b8eaa Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:27:04 -0600 Subject: [PATCH 32/47] Update domain_request.py --- src/registrar/models/domain_request.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 96d98ed65..c8c21ea33 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1127,7 +1127,10 @@ def reject_with_prejudice(self): def requesting_entity_is_portfolio(self) -> bool: """Determines if this record is requesting that a portfolio be their organization.""" - return self.portfolio and self.organization_name == self.portfolio.organization_name + if self.portfolio and self.organization_name == self.portfolio.organization_name: + return True + else: + return False def requesting_entity_is_suborganization(self) -> bool: """Used to determine if this domain request is also requesting that it be tied to a suborganization. @@ -1152,9 +1155,11 @@ def is_requesting_new_suborganization(self) -> bool: # If a suborganization already exists, it can't possibly be a new one if self.sub_organization: return False - return bool( - self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory - ) + + if self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory: + return True + else: + return False # ## Form unlocking steps ## # # From bb9cb527d203ece5049135c4d59f437c8f86d567 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:33:30 -0600 Subject: [PATCH 33/47] Cleanup bool logic to be more concise --- src/registrar/models/domain_request.py | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index c8c21ea33..48b9f23c7 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1137,13 +1137,10 @@ def requesting_entity_is_suborganization(self) -> bool: Checks if this record has a suborganization or not by checking if a suborganization exists, and if it doesn't, determining if properties like requested_suborganization exist. """ - - if self.portfolio: - if self.sub_organization: - return True - if self.is_requesting_new_suborganization(): - return True - return False + if self.portfolio and (self.sub_organization or self.is_requesting_new_suborganization()): + return True + else: + return False def is_requesting_new_suborganization(self) -> bool: """Used on the requesting entity form to determine if a user is trying to request @@ -1152,11 +1149,15 @@ def is_requesting_new_suborganization(self) -> bool: This only occurs when no suborganization is selected, but they've filled out the requested_suborganization, suborganization_city, and suborganization_state_territory fields. """ - # If a suborganization already exists, it can't possibly be a new one - if self.sub_organization: - return False - if self.requested_suborganization and self.suborganization_city and self.suborganization_state_territory: + # If a suborganization already exists, it can't possibly be a new one. + # As well, we need all required fields to exist. + required_fields = [ + self.requested_suborganization, + self.suborganization_city, + self.suborganization_state_territory + ] + if not self.sub_organization and all(required_fields): return True else: return False @@ -1167,9 +1168,7 @@ def is_requesting_new_suborganization(self) -> bool: def unlock_requesting_entity(self) -> bool: """Unlocks the requesting entity step""" - if self.requesting_entity_is_suborganization(): - return True - elif self.requesting_entity_is_portfolio(): + if self.requesting_entity_is_suborganization() or self.requesting_entity_is_portfolio(): return True else: return False From 98842c1aa0c5e71cb6d861c19314a3d9895e5a11 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:40:25 -0600 Subject: [PATCH 34/47] add comments --- src/registrar/forms/domain_request_wizard.py | 3 ++- src/registrar/views/domain_request.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index b27a004c0..c188a7ab0 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -154,7 +154,8 @@ def form_is_checked(self): desired suborg doesn't exist. We expose additional fields that denote this, like `requested_suborganization`. So we also check on those. """ - + # True means that the requesting entity is a suborganization, + # whereas False means that the requesting entity is a portfolio. if self.domain_request.requesting_entity_is_suborganization(): return True elif self.domain_request.requesting_entity_is_portfolio(): diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 7fb2d9b16..41e9a07b8 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -594,16 +594,19 @@ def save(self, forms: list): """Override of save to clear or associate certain suborganization data depending on what the user wishes to do. For instance, we want to add a suborganization if the user selects one.""" - yesno_form = forms[0] - requesting_entity_form = forms[1] + # Get the yes/no dropdown value + yesno_form = forms[0] yesno_cleaned_data = yesno_form.cleaned_data requesting_entity_is_suborganization = yesno_cleaned_data.get("requesting_entity_is_suborganization") + # Get the suborg value, and the requested suborg value + requesting_entity_form = forms[1] cleaned_data = requesting_entity_form.cleaned_data sub_organization = cleaned_data.get("sub_organization") requested_suborganization = cleaned_data.get("requested_suborganization") + # Do some data cleanup, depending on what option was checked if requesting_entity_is_suborganization and (sub_organization or requested_suborganization): # Cleanup the organization name field, as this isn't for suborganizations. requesting_entity_form.cleaned_data.update({"organization_name": None}) From 603e2eb767a031df7d5705e9db1ae6d138fea771 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:28:05 -0600 Subject: [PATCH 35/47] simplify javascript (a lot) --- src/registrar/assets/js/get-gov.js | 87 +++++++++++------------------- 1 file changed, 31 insertions(+), 56 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index be658ad3e..2bbcc781d 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2742,78 +2742,53 @@ document.addEventListener('DOMContentLoaded', function() { (function handleRequestingEntityFieldset() { // Sadly, these ugly ids are the auto generated with this prefix const formPrefix = "portfolio_requesting_entity" - - // This determines if we are on the requesting entity page or not. - const isSubOrgFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`); - if (!isSubOrgFieldset) return; - - // Get the is_suborganization radio buttons - const isSuborgRadios = isSubOrgFieldset.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`); - const subOrgSelect = document.getElementById(`id_${formPrefix}-sub_organization`); - - // The suborganization section is its own div - // Within the suborganization section, we also have a div that contains orgname, city, and stateterritory. + const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`); + const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`); + const select = document.getElementById(`id_${formPrefix}-sub_organization`); const suborganizationContainer = document.getElementById("suborganization-container"); const suborganizationDetailsContainer = document.getElementById("suborganization-container__details"); + if (!radios || !select || !suborganizationContainer || !suborganizationDetailsContainer) return; - // This variable determines if the user is trying to *create* a new suborganization or not. - var isRequestingSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`) + // requestingSuborganization: This just broadly determines if they're requesting a suborg at all + // requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not. + var requestingSuborganization = false; + var requestingNewSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`); - // Don't do anything if we are missing crucial page elements - if (!isSuborgRadios || !subOrgSelect || !suborganizationContainer || !suborganizationDetailsContainer) return; - - // Function to toggle suborganization based on is_suborganization selection function toggleSuborganization(radio) { - if (radio && radio.checked && radio.value === "True") { + requestingSuborganization = radio?.checked && radio.value === "True"; + if (requestingSuborganization) { showElement(suborganizationContainer); - - // Handle custom suborganizations - if (subOrgSelect.value === "other") { - showElement(suborganizationDetailsContainer); - isRequestingSuborganization.value = "True"; - } else { - hideElement(suborganizationDetailsContainer); - isRequestingSuborganization.value = "False"; - } - } else { + }else { hideElement(suborganizationContainer); + } + } + + function toggleSuborganizationDetails() { + if (requestingSuborganization && select.value === "other") { + showElement(suborganizationDetailsContainer); + requestingNewSuborganization.value = "True"; + }else { hideElement(suborganizationDetailsContainer); + requestingNewSuborganization.value = "False"; } - }; + } - // Add fake "other" option to sub_organization select - if (subOrgSelect && !Array.from(subOrgSelect.options).some(option => option.value === "other")) { - const fakeOption = document.createElement("option"); - fakeOption.value = "other"; - fakeOption.text = "Other (enter your organization manually)"; - subOrgSelect.add(fakeOption); + // Add fake "other" option to sub_organization select + if (select && !Array.from(select.options).some(option => option.value === "other")) { + select.add(new Option("Other (enter your organization manually)", "other")); } - if (isRequestingSuborganization.value === "True") { - subOrgSelect.value = "other" + if (requestingNewSuborganization.value === "True") { + select.value = "other"; } - // Add event listener to is_suborganization radio buttons - isSuborgRadios.forEach(radio => { - // Run this here for initial display. - // Since there are only two radio buttons and since this has (practically speaking) no performance impact, this is fine to do. + // Add event listener to is_suborganization radio buttons, and run for initial display + radios.forEach(radio => { toggleSuborganization(radio); - - // Add an event listener to each to show/hide the relevant fields - radio.addEventListener("click", () => { - toggleSuborganization(radio); - }); + radio.addEventListener("click", () => toggleSuborganization(radio)); }); // Add event listener to the suborg dropdown to show/hide the suborg details section - subOrgSelect.addEventListener("change", () => { - // Handle the custom suborganization field - if (subOrgSelect.value === "other") { - showElement(suborganizationDetailsContainer); - isRequestingSuborganization.value = "True"; - } else { - hideElement(suborganizationDetailsContainer); - isRequestingSuborganization.value = "False"; - } - }); + toggleSuborganizationDetails(); + select.addEventListener("change", () => toggleSuborganizationDetails()); })(); From b01e707b82a8de44e6781c41871bc2b59af895c2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:55:39 -0600 Subject: [PATCH 36/47] further simplify --- src/registrar/assets/js/get-gov.js | 35 ++++++++++-------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 2bbcc781d..d462af5ac 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2745,32 +2745,20 @@ document.addEventListener('DOMContentLoaded', function() { const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`); const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`); const select = document.getElementById(`id_${formPrefix}-sub_organization`); - const suborganizationContainer = document.getElementById("suborganization-container"); - const suborganizationDetailsContainer = document.getElementById("suborganization-container__details"); - if (!radios || !select || !suborganizationContainer || !suborganizationDetailsContainer) return; + const suborgContainer = document.getElementById("suborganization-container"); + const suborgDetailsContainer = document.getElementById("suborganization-container__details"); + if (!radios || !select || !suborgContainer || !suborgDetailsContainer) return; // requestingSuborganization: This just broadly determines if they're requesting a suborg at all // requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not. - var requestingSuborganization = false; + var requestingSuborganization = Array.from(radios).find(radio => radio.checked)?.value === "True"; var requestingNewSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`); - function toggleSuborganization(radio) { - requestingSuborganization = radio?.checked && radio.value === "True"; - if (requestingSuborganization) { - showElement(suborganizationContainer); - }else { - hideElement(suborganizationContainer); - } - } - - function toggleSuborganizationDetails() { - if (requestingSuborganization && select.value === "other") { - showElement(suborganizationDetailsContainer); - requestingNewSuborganization.value = "True"; - }else { - hideElement(suborganizationDetailsContainer); - requestingNewSuborganization.value = "False"; - } + function toggleSuborganization(radio=null) { + if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True"; + requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer); + requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False"; + requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer); } // Add fake "other" option to sub_organization select @@ -2783,12 +2771,11 @@ document.addEventListener('DOMContentLoaded', function() { } // Add event listener to is_suborganization radio buttons, and run for initial display + toggleSuborganization(); radios.forEach(radio => { - toggleSuborganization(radio); radio.addEventListener("click", () => toggleSuborganization(radio)); }); // Add event listener to the suborg dropdown to show/hide the suborg details section - toggleSuborganizationDetails(); - select.addEventListener("change", () => toggleSuborganizationDetails()); + select.addEventListener("change", () => toggleSuborganization()); })(); From f0ba59611e60e90f64e41c4a376a09907328d9ea Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 31 Oct 2024 08:14:37 -0600 Subject: [PATCH 37/47] Initial logic --- 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 48b9f23c7..c2a8df257 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1155,7 +1155,7 @@ def is_requesting_new_suborganization(self) -> bool: required_fields = [ self.requested_suborganization, self.suborganization_city, - self.suborganization_state_territory + self.suborganization_state_territory, ] if not self.sub_organization and all(required_fields): return True From 510938430b2f389d148c8497fde0168faf4b3473 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:05:28 -0600 Subject: [PATCH 38/47] Add some comments --- src/registrar/forms/domain_request_wizard.py | 5 +++++ src/registrar/models/domain_request.py | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index c188a7ab0..16e5b66ea 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -27,6 +27,8 @@ class RequestingEntityForm(RegistrarForm): All of these fields are not required by default, but as we use javascript to conditionally show and hide some of these, they then become required in certain circumstances.""" + # IMPORTANT: This is tied to DomainRequest.is_requesting_new_suborganization(). + # This is due to the from_database method on DomainRequestWizard. # Add a hidden field to store if the user is requesting a new suborganization. # This hidden boolean is used for our javascript to communicate to us and to it. # If true, the suborganization form will auto select a js value "Other". @@ -134,6 +136,9 @@ class RequestingEntityYesNoForm(BaseYesNoForm): # This first option will change dynamically form_choices = ((False, "Current Organization"), (True, "A suborganization. (choose from list)")) + + # IMPORTANT: This is tied to DomainRequest.is_requesting_new_suborganization(). + # This is due to the from_database method on DomainRequestWizard. field_name = "requesting_entity_is_suborganization" def __init__(self, *args, **kwargs): diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index c2a8df257..c3fc5335d 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1126,7 +1126,8 @@ def reject_with_prejudice(self): self.creator.restrict_user() def requesting_entity_is_portfolio(self) -> bool: - """Determines if this record is requesting that a portfolio be their organization.""" + """Determines if this record is requesting that a portfolio be their organization. + Used for the RequestingEntity page.""" if self.portfolio and self.organization_name == self.portfolio.organization_name: return True else: @@ -1136,6 +1137,7 @@ def requesting_entity_is_suborganization(self) -> bool: """Used to determine if this domain request is also requesting that it be tied to a suborganization. Checks if this record has a suborganization or not by checking if a suborganization exists, and if it doesn't, determining if properties like requested_suborganization exist. + Used for the RequestingEntity page. """ if self.portfolio and (self.sub_organization or self.is_requesting_new_suborganization()): return True @@ -1148,6 +1150,7 @@ def is_requesting_new_suborganization(self) -> bool: This only occurs when no suborganization is selected, but they've filled out the requested_suborganization, suborganization_city, and suborganization_state_territory fields. + Used for the RequestingEntity page. """ # If a suborganization already exists, it can't possibly be a new one. @@ -1167,7 +1170,7 @@ def is_requesting_new_suborganization(self) -> bool: # These methods control the conditions in which we should unlock certain domain wizard steps. def unlock_requesting_entity(self) -> bool: - """Unlocks the requesting entity step""" + """Unlocks the requesting entity step. Used for the RequestingEntity page.""" if self.requesting_entity_is_suborganization() or self.requesting_entity_is_portfolio(): return True else: From 1d345987ca9b85e17c0d63298a766ea23ae936b1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 08:49:32 -0600 Subject: [PATCH 39/47] PR suggestions (part 1) --- src/registrar/forms/domain_request_wizard.py | 19 +++++----- ...tion_requested_suborganization_and_more.py | 25 +++++++++++++ src/registrar/models/domain_information.py | 18 ---------- src/registrar/models/domain_request.py | 35 ++++++++++--------- .../templates/includes/header_extended.html | 2 +- .../portfolio_request_review_steps.html | 2 +- src/registrar/views/domain_request.py | 10 +++++- 7 files changed, 62 insertions(+), 49 deletions(-) create mode 100644 src/registrar/migrations/0137_remove_domaininformation_requested_suborganization_and_more.py diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 16e5b66ea..f74622b51 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -35,7 +35,7 @@ class RequestingEntityForm(RegistrarForm): # If this selection is made on the form (tracked by js), then it will toggle the form value of this. # In other words, this essentially tracks if the suborganization field == "Other". # "Other" is just an imaginary value that is otherwise invalid. - # Note the logic in `def clean` and line 2744 in get-gov.js + # Note the logic in `def clean` and `handleRequestingEntityFieldset` in get-gov.js is_requesting_new_suborganization = forms.BooleanField(required=False, widget=forms.HiddenInput()) sub_organization = forms.ModelChoiceField( @@ -43,24 +43,22 @@ class RequestingEntityForm(RegistrarForm): required=False, queryset=Suborganization.objects.none(), empty_label="--Select--", + error_messages={ + "required": ("Requesting entity is required.") + }, ) requested_suborganization = forms.CharField( label="Requested suborganization", required=False, - error_messages={"required": "Enter the name of your organization."}, ) suborganization_city = forms.CharField( label="City", required=False, - error_messages={"required": "Enter the city where your organization is located."}, ) suborganization_state_territory = forms.ChoiceField( label="State, territory, or military post", required=False, choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices, - error_messages={ - "required": ("Select the state, territory, or military post where your organization is located.") - }, ) def __init__(self, *args, **kwargs): @@ -147,17 +145,16 @@ def __init__(self, *args, **kwargs): if self.domain_request.portfolio: self.form_choices = ( (False, self.domain_request.portfolio), - (True, "A suborganization. (choose from list)"), + (True, "A suborganization (choose from list)"), ) self.fields[self.field_name] = self.get_typed_choice_field() @property def form_is_checked(self): """ - Determines if the requesting entity is a suborganization, or a portfolio. - For suborganizations, users have the ability to request a new one if the - desired suborg doesn't exist. We expose additional fields that denote this, - like `requested_suborganization`. So we also check on those. + Determines the initial checked state of the form. + Returns True (checked) if the requesting entity is a suborganization, + and False if it is a portfolio. Returns None if neither condition is met. """ # True means that the requesting entity is a suborganization, # whereas False means that the requesting entity is a portfolio. diff --git a/src/registrar/migrations/0137_remove_domaininformation_requested_suborganization_and_more.py b/src/registrar/migrations/0137_remove_domaininformation_requested_suborganization_and_more.py new file mode 100644 index 000000000..dfa716ef4 --- /dev/null +++ b/src/registrar/migrations/0137_remove_domaininformation_requested_suborganization_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.10 on 2024-11-01 14:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0136_domaininformation_requested_suborganization_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="domaininformation", + name="requested_suborganization", + ), + migrations.RemoveField( + model_name="domaininformation", + name="suborganization_city", + ), + migrations.RemoveField( + model_name="domaininformation", + name="suborganization_state_territory", + ), + ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 7dc257b22..7dadf26ac 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -75,24 +75,6 @@ class Meta: verbose_name="Suborganization", ) - requested_suborganization = models.CharField( - null=True, - blank=True, - ) - - suborganization_city = models.CharField( - null=True, - blank=True, - ) - - suborganization_state_territory = models.CharField( - max_length=2, - choices=StateTerritoryChoices.choices, - null=True, - blank=True, - verbose_name="state, territory, or military post", - ) - domain_request = models.OneToOneField( "registrar.DomainRequest", on_delete=models.PROTECT, diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index c3fc5335d..2a0c65505 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1127,30 +1127,30 @@ def reject_with_prejudice(self): def requesting_entity_is_portfolio(self) -> bool: """Determines if this record is requesting that a portfolio be their organization. - Used for the RequestingEntity page.""" + Used for the RequestingEntity page. + Returns True if the portfolio exists and if organization_name matches portfolio.organization_name. + """ if self.portfolio and self.organization_name == self.portfolio.organization_name: return True - else: - return False + return False def requesting_entity_is_suborganization(self) -> bool: """Used to determine if this domain request is also requesting that it be tied to a suborganization. - Checks if this record has a suborganization or not by checking if a suborganization exists, - and if it doesn't, determining if properties like requested_suborganization exist. - Used for the RequestingEntity page. + Returns True if portfolio exists and either sub_organization exists, + or if is_requesting_new_suborganization() is true. + Returns False otherwise. """ if self.portfolio and (self.sub_organization or self.is_requesting_new_suborganization()): return True - else: - return False + return False def is_requesting_new_suborganization(self) -> bool: """Used on the requesting entity form to determine if a user is trying to request - a new suborganization using the domain request form. + a new suborganization using the domain request form, rather than one that already exists. - This only occurs when no suborganization is selected, but they've filled out - the requested_suborganization, suborganization_city, and suborganization_state_territory fields. - Used for the RequestingEntity page. + Returns True if a sub_organization does not exist and if requested_suborganization, + suborganization_city, and suborganization_state_territory all exist. + Returns False otherwise. """ # If a suborganization already exists, it can't possibly be a new one. @@ -1162,19 +1162,20 @@ def is_requesting_new_suborganization(self) -> bool: ] if not self.sub_organization and all(required_fields): return True - else: - return False + return False # ## Form unlocking steps ## # # # These methods control the conditions in which we should unlock certain domain wizard steps. def unlock_requesting_entity(self) -> bool: - """Unlocks the requesting entity step. Used for the RequestingEntity page.""" + """Unlocks the requesting entity step. Used for the RequestingEntity page. + Returns true if requesting_entity_is_suborganization() and requesting_entity_is_portfolio(). + Returns False otherwise. + """ if self.requesting_entity_is_suborganization() or self.requesting_entity_is_portfolio(): return True - else: - return False + return False # ## Form policies ## # # diff --git a/src/registrar/templates/includes/header_extended.html b/src/registrar/templates/includes/header_extended.html index 7c0f55dc3..a954eb30f 100644 --- a/src/registrar/templates/includes/header_extended.html +++ b/src/registrar/templates/includes/header_extended.html @@ -93,7 +93,7 @@ {% endif %} - {% if has_organization_members_flag %} + {% if has_organization_members_flag and not hide_members %}
  • Members diff --git a/src/registrar/templates/includes/portfolio_request_review_steps.html b/src/registrar/templates/includes/portfolio_request_review_steps.html index ed727fee8..175a9bdc1 100644 --- a/src/registrar/templates/includes/portfolio_request_review_steps.html +++ b/src/registrar/templates/includes/portfolio_request_review_steps.html @@ -14,7 +14,7 @@ {% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %} {% elif domain_request.requesting_entity_is_suborganization %} {% include "includes/summary_item.html" with value=domain_request.requested_suborganization edit_link=domain_request_url %} -

    {{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}

    +

    {{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}

    {% elif domain_request.requesting_entity_is_portfolio %} {% include "includes/summary_item.html" with value=domain_request.portfolio.organization_name edit_link=domain_request_url %} {% if domain_request.portfolio.city and domain_request.portfolio.state_territory %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 41e9a07b8..b1c5c9c89 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -319,7 +319,15 @@ def get(self, request, *args, **kwargs): # Clear context so the prop getter won't create a request here. # Creating a request will be handled in the post method for the # intro page. - return render(request, "domain_request_intro.html", {"hide_requests": True, "hide_domains": True}) + return render( + request, + "domain_request_intro.html", + { + "hide_requests": True, + "hide_domains": True, + "hide_members": True, + }, + ) else: return self.goto(self.steps.first) From 83720c359fd6448dc4647f610934bf5a126c7ef7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 08:54:32 -0600 Subject: [PATCH 40/47] Update admin.py --- src/registrar/admin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index fdadde436..84609468f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1484,9 +1484,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): "fields": [ "portfolio", "sub_organization", - "requested_suborganization", - "suborganization_city", - "suborganization_state_territory", "creator", "domain_request", "notes", From b05a62ecd195bad8e7585c7cb7985bb298e4dad5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:10:28 -0600 Subject: [PATCH 41/47] Rework readonly fields for analysts + hide when org flag is off --- src/registrar/admin.py | 25 +++++++++++++------------ src/registrar/models/domain_request.py | 6 ++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 84609468f..fe59c7d1f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -28,6 +28,7 @@ from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial from registrar.utility.constants import BranchChoices from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes +from registrar.utility.waffle import flag_is_active_for_user from registrar.views.utility.mixins import OrderableFieldsMixin from django.contrib.admin.views.main import ORDER_VAR from registrar.widgets import NoAutocompleteFilteredSelectMultiple @@ -1863,6 +1864,9 @@ def status_history(self, obj): "cisa_representative_first_name", "cisa_representative_last_name", "cisa_representative_email", + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", ] autocomplete_fields = [ "approved_domain", @@ -1882,24 +1886,21 @@ def status_history(self, obj): change_form_template = "django/admin/domain_request_change_form.html" - # While the organization feature is under development, we can gate some fields - # from analysts for now. Remove this array and the get_fieldset overrides once this is done. - # Not my code initially, credit to Nicolle. This was once removed and like a phoenix it has been reborn. - superuser_only_fields = [ - "requested_suborganization", - "suborganization_city", - "suborganization_state_territory", - ] - def get_fieldsets(self, request, obj=None): fieldsets = super().get_fieldsets(request, obj) - # Create a modified version of fieldsets to exclude certain fields - if not request.user.has_perm("registrar.full_access_permission"): + # Hide certain suborg fields behind the organization feature flag + # if it is not enabled + if not flag_is_active_for_user(request.user, "organization_feature"): + excluded_fields = [ + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", + ] modified_fieldsets = [] for name, data in fieldsets: fields = data.get("fields", []) - fields = tuple(field for field in fields if field not in self.superuser_only_fields) + fields = tuple(field for field in fields if field not in excluded_fields) modified_fieldsets.append((name, {**data, "fields": fields})) return modified_fieldsets return fieldsets diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 2a0c65505..ca4b322be 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1135,7 +1135,8 @@ def requesting_entity_is_portfolio(self) -> bool: return False def requesting_entity_is_suborganization(self) -> bool: - """Used to determine if this domain request is also requesting that it be tied to a suborganization. + """Determines if this record is also requesting that it be tied to a suborganization. + Used for the RequestingEntity page. Returns True if portfolio exists and either sub_organization exists, or if is_requesting_new_suborganization() is true. Returns False otherwise. @@ -1145,8 +1146,9 @@ def requesting_entity_is_suborganization(self) -> bool: return False def is_requesting_new_suborganization(self) -> bool: - """Used on the requesting entity form to determine if a user is trying to request + """Determines if a user is trying to request a new suborganization using the domain request form, rather than one that already exists. + Used for the RequestingEntity page. Returns True if a sub_organization does not exist and if requested_suborganization, suborganization_city, and suborganization_state_territory all exist. From 7cdfb7a35fcbae398a2786643068ec1a3272ad5f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:15:40 -0600 Subject: [PATCH 42/47] fix test + lint --- src/registrar/forms/domain_request_wizard.py | 14 ++++++-------- src/registrar/tests/test_admin_request.py | 5 ++++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index f74622b51..010890b04 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -43,9 +43,6 @@ class RequestingEntityForm(RegistrarForm): required=False, queryset=Suborganization.objects.none(), empty_label="--Select--", - error_messages={ - "required": ("Requesting entity is required.") - }, ) requested_suborganization = forms.CharField( label="Requested suborganization", @@ -118,13 +115,13 @@ def clean(self): if is_requesting_new_suborganization: # Validate custom suborganization fields if not cleaned_data.get("requested_suborganization"): - self.add_error("requested_suborganization", "Enter details for your organization name.") + self.add_error("requested_suborganization", "Organization name is required.") if not cleaned_data.get("suborganization_city"): - self.add_error("suborganization_city", "Enter details for your city.") + self.add_error("suborganization_city", "City is required.") if not cleaned_data.get("suborganization_state_territory"): - self.add_error("suborganization_state_territory", "Enter details for your state or territory.") + self.add_error("suborganization_state_territory", "State or territory is required.") elif not suborganization: - self.add_error("sub_organization", "Select a suborganization.") + self.add_error("sub_organization", "Suborganization is required.") return cleaned_data @@ -138,6 +135,7 @@ class RequestingEntityYesNoForm(BaseYesNoForm): # IMPORTANT: This is tied to DomainRequest.is_requesting_new_suborganization(). # This is due to the from_database method on DomainRequestWizard. field_name = "requesting_entity_is_suborganization" + required_error_message = "Requesting entity is required." def __init__(self, *args, **kwargs): """Extend the initialization of the form from RegistrarForm __init__""" @@ -153,7 +151,7 @@ def __init__(self, *args, **kwargs): def form_is_checked(self): """ Determines the initial checked state of the form. - Returns True (checked) if the requesting entity is a suborganization, + Returns True (checked) if the requesting entity is a suborganization, and False if it is a portfolio. Returns None if neither condition is met. """ # True means that the requesting entity is a suborganization, diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index 4b65c401d..57d7d9ac6 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -1689,7 +1689,7 @@ def test_readonly_fields_for_analyst(self): request.user = self.staffuser readonly_fields = self.admin.get_readonly_fields(request) - + self.maxDiff = None expected_fields = [ "other_contacts", "current_websites", @@ -1709,6 +1709,9 @@ def test_readonly_fields_for_analyst(self): "cisa_representative_first_name", "cisa_representative_last_name", "cisa_representative_email", + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", ] self.assertEqual(readonly_fields, expected_fields) From c59289d2150498444d01244e78afaecf9670de7d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:18:46 -0600 Subject: [PATCH 43/47] Update domain_request_wizard.py --- src/registrar/forms/domain_request_wizard.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 010890b04..d9754c87e 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -115,13 +115,13 @@ def clean(self): if is_requesting_new_suborganization: # Validate custom suborganization fields if not cleaned_data.get("requested_suborganization"): - self.add_error("requested_suborganization", "Organization name is required.") + self.add_error("requested_suborganization", "Enter details for your organization name.") if not cleaned_data.get("suborganization_city"): - self.add_error("suborganization_city", "City is required.") + self.add_error("suborganization_city", "Enter details for your city.") if not cleaned_data.get("suborganization_state_territory"): - self.add_error("suborganization_state_territory", "State or territory is required.") + self.add_error("suborganization_state_territory", "Enter details for your state or territory.") elif not suborganization: - self.add_error("sub_organization", "Suborganization is required.") + self.add_error("sub_organization", "Select a suborganization.") return cleaned_data From 7141eaed924ae2cd26b7382cd96e7312c0ea1ea9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:02:16 -0600 Subject: [PATCH 44/47] consolidate migration --- ...est_requested_suborganization_and_more.py} | 84 +------------------ ...tion_requested_suborganization_and_more.py | 25 ------ 2 files changed, 1 insertion(+), 108 deletions(-) rename src/registrar/migrations/{0136_domaininformation_requested_suborganization_and_more.py => 0136_domainrequest_requested_suborganization_and_more.py} (50%) delete mode 100644 src/registrar/migrations/0137_remove_domaininformation_requested_suborganization_and_more.py diff --git a/src/registrar/migrations/0136_domaininformation_requested_suborganization_and_more.py b/src/registrar/migrations/0136_domainrequest_requested_suborganization_and_more.py similarity index 50% rename from src/registrar/migrations/0136_domaininformation_requested_suborganization_and_more.py rename to src/registrar/migrations/0136_domainrequest_requested_suborganization_and_more.py index dc6ec9d5d..e1b130b4f 100644 --- a/src/registrar/migrations/0136_domaininformation_requested_suborganization_and_more.py +++ b/src/registrar/migrations/0136_domainrequest_requested_suborganization_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-10-30 17:59 +# Generated by Django 4.2.10 on 2024-11-01 17:01 from django.db import migrations, models @@ -10,88 +10,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name="domaininformation", - name="requested_suborganization", - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name="domaininformation", - name="suborganization_city", - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name="domaininformation", - name="suborganization_state_territory", - field=models.CharField( - blank=True, - choices=[ - ("AL", "Alabama (AL)"), - ("AK", "Alaska (AK)"), - ("AS", "American Samoa (AS)"), - ("AZ", "Arizona (AZ)"), - ("AR", "Arkansas (AR)"), - ("CA", "California (CA)"), - ("CO", "Colorado (CO)"), - ("CT", "Connecticut (CT)"), - ("DE", "Delaware (DE)"), - ("DC", "District of Columbia (DC)"), - ("FL", "Florida (FL)"), - ("GA", "Georgia (GA)"), - ("GU", "Guam (GU)"), - ("HI", "Hawaii (HI)"), - ("ID", "Idaho (ID)"), - ("IL", "Illinois (IL)"), - ("IN", "Indiana (IN)"), - ("IA", "Iowa (IA)"), - ("KS", "Kansas (KS)"), - ("KY", "Kentucky (KY)"), - ("LA", "Louisiana (LA)"), - ("ME", "Maine (ME)"), - ("MD", "Maryland (MD)"), - ("MA", "Massachusetts (MA)"), - ("MI", "Michigan (MI)"), - ("MN", "Minnesota (MN)"), - ("MS", "Mississippi (MS)"), - ("MO", "Missouri (MO)"), - ("MT", "Montana (MT)"), - ("NE", "Nebraska (NE)"), - ("NV", "Nevada (NV)"), - ("NH", "New Hampshire (NH)"), - ("NJ", "New Jersey (NJ)"), - ("NM", "New Mexico (NM)"), - ("NY", "New York (NY)"), - ("NC", "North Carolina (NC)"), - ("ND", "North Dakota (ND)"), - ("MP", "Northern Mariana Islands (MP)"), - ("OH", "Ohio (OH)"), - ("OK", "Oklahoma (OK)"), - ("OR", "Oregon (OR)"), - ("PA", "Pennsylvania (PA)"), - ("PR", "Puerto Rico (PR)"), - ("RI", "Rhode Island (RI)"), - ("SC", "South Carolina (SC)"), - ("SD", "South Dakota (SD)"), - ("TN", "Tennessee (TN)"), - ("TX", "Texas (TX)"), - ("UM", "United States Minor Outlying Islands (UM)"), - ("UT", "Utah (UT)"), - ("VT", "Vermont (VT)"), - ("VI", "Virgin Islands (VI)"), - ("VA", "Virginia (VA)"), - ("WA", "Washington (WA)"), - ("WV", "West Virginia (WV)"), - ("WI", "Wisconsin (WI)"), - ("WY", "Wyoming (WY)"), - ("AA", "Armed Forces Americas (AA)"), - ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), - ("AP", "Armed Forces Pacific (AP)"), - ], - max_length=2, - null=True, - verbose_name="state, territory, or military post", - ), - ), migrations.AddField( model_name="domainrequest", name="requested_suborganization", diff --git a/src/registrar/migrations/0137_remove_domaininformation_requested_suborganization_and_more.py b/src/registrar/migrations/0137_remove_domaininformation_requested_suborganization_and_more.py deleted file mode 100644 index dfa716ef4..000000000 --- a/src/registrar/migrations/0137_remove_domaininformation_requested_suborganization_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.10 on 2024-11-01 14:25 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("registrar", "0136_domaininformation_requested_suborganization_and_more"), - ] - - operations = [ - migrations.RemoveField( - model_name="domaininformation", - name="requested_suborganization", - ), - migrations.RemoveField( - model_name="domaininformation", - name="suborganization_city", - ), - migrations.RemoveField( - model_name="domaininformation", - name="suborganization_state_territory", - ), - ] From a1d37ee207e3d16b14a5c1d4dde486965cd01590 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:02:43 -0600 Subject: [PATCH 45/47] Update src/registrar/templates/includes/portfolio_request_review_steps.html Co-authored-by: Rachid Mrad <107004823+rachidatecs@users.noreply.github.com> --- .../templates/includes/portfolio_request_review_steps.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/portfolio_request_review_steps.html b/src/registrar/templates/includes/portfolio_request_review_steps.html index 175a9bdc1..d0bdd7ba0 100644 --- a/src/registrar/templates/includes/portfolio_request_review_steps.html +++ b/src/registrar/templates/includes/portfolio_request_review_steps.html @@ -18,7 +18,7 @@ {% elif domain_request.requesting_entity_is_portfolio %} {% include "includes/summary_item.html" with value=domain_request.portfolio.organization_name edit_link=domain_request_url %} {% if domain_request.portfolio.city and domain_request.portfolio.state_territory %} -

    {{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}}

    +

    {{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}}

    {% endif %} {% else %} {% with value="Incomplete"|safe %} From 706dd4fe2e9dcc0076bb46aa39aaa0d204b5753a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:17:38 -0600 Subject: [PATCH 46/47] error messages! @abroddrick --- src/registrar/forms/domain_request_wizard.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index d9754c87e..0b560f19c 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -115,11 +115,11 @@ def clean(self): if is_requesting_new_suborganization: # Validate custom suborganization fields if not cleaned_data.get("requested_suborganization"): - self.add_error("requested_suborganization", "Enter details for your organization name.") + self.add_error("requested_suborganization", "Requested suborganization is required") if not cleaned_data.get("suborganization_city"): - self.add_error("suborganization_city", "Enter details for your city.") + self.add_error("suborganization_city", "City is required") if not cleaned_data.get("suborganization_state_territory"): - self.add_error("suborganization_state_territory", "Enter details for your state or territory.") + self.add_error("suborganization_state_territory", "State, territory, or military post is required") elif not suborganization: self.add_error("sub_organization", "Select a suborganization.") From e4c15ee28b30817d201130ed0e79384d78ff65e9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:40:19 -0600 Subject: [PATCH 47/47] Fix unit test --- src/registrar/forms/domain_request_wizard.py | 8 ++++---- src/registrar/tests/test_views_portfolio.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 0b560f19c..bfbc22124 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -115,13 +115,13 @@ def clean(self): if is_requesting_new_suborganization: # Validate custom suborganization fields if not cleaned_data.get("requested_suborganization"): - self.add_error("requested_suborganization", "Requested suborganization is required") + self.add_error("requested_suborganization", "Requested suborganization is required.") if not cleaned_data.get("suborganization_city"): - self.add_error("suborganization_city", "City is required") + self.add_error("suborganization_city", "City is required.") if not cleaned_data.get("suborganization_state_territory"): - self.add_error("suborganization_state_territory", "State, territory, or military post is required") + self.add_error("suborganization_state_territory", "State, territory, or military post is required.") elif not suborganization: - self.add_error("sub_organization", "Select a suborganization.") + self.add_error("sub_organization", "Suborganization is required.") return cleaned_data diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 45357cbf7..402d23b70 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1788,15 +1788,15 @@ def test_requesting_entity_page_errors(self): response = form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.assertContains(response, "Select a suborganization.", status_code=200) + self.assertContains(response, "Suborganization is required.", status_code=200) # Test missing custom suborganization details form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True response = form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.assertContains(response, "Enter details for your organization name.", status_code=200) - self.assertContains(response, "Enter details for your city.", status_code=200) - self.assertContains(response, "Enter details for your state or territory.", status_code=200) + self.assertContains(response, "Requested suborganization is required.", status_code=200) + self.assertContains(response, "City is required.", status_code=200) + self.assertContains(response, "State, territory, or military post is required.", status_code=200) @override_flag("organization_feature", active=True) @override_flag("organization_requests", active=True)