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/73] 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/73] 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/73] 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 b61d3936b596066aae9c9c1ebfbeee8ffc3d00b1 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 21 Oct 2024 22:08:35 -0600 Subject: [PATCH 04/73] New Member form is assembled (just needs validation) --- src/registrar/assets/js/get-gov.js | 9 + src/registrar/config/urls.py | 10 +- src/registrar/forms/portfolio.py | 71 ++++- .../templates/portfolio_members.html | 2 +- .../templates/portfolio_members_add_new.html | 117 ++++++++ .../templates/portfolio_no_domains.html | 2 +- src/registrar/views/portfolios.py | 255 +++++++++++++++++- .../views/utility/permission_views.py | 2 +- 8 files changed, 446 insertions(+), 22 deletions(-) create mode 100644 src/registrar/templates/portfolio_members_add_new.html diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 8a07b3f27..6be718c23 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -912,6 +912,15 @@ function setupUrbanizationToggle(stateTerritoryField) { HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null) })(); + +/** + * An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly + * + */ +(function newMemberFormListener() { + HookupYesNoListener("new_member-permission_level",'new-member-admin-permissions', 'new-member-basic-permissions') +})(); + /** * An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms * diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 4d1be6f31..26c3f516e 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -86,11 +86,11 @@ views.PortfolioMembersView.as_view(), name="members", ), - # path( - # "no-organization-members/", - # views.PortfolioNoMembersView.as_view(), - # name="no-portfolio-members", - # ), + path( + "members/new-member/", + views.NewMemberView.as_view(), + name="new-member", + ), path( "requests/", views.PortfolioDomainRequestsView.as_view(), diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 999d76d51..4bc7ec046 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -3,8 +3,11 @@ import logging from django import forms from django.core.validators import RegexValidator +from django.core.validators import MaxLengthValidator -from ..models import DomainInformation, Portfolio, SeniorOfficial +from registrar.models.user_portfolio_permission import UserPortfolioPermission + +from ..models import DomainInformation, Portfolio, SeniorOfficial, User logger = logging.getLogger(__name__) @@ -99,3 +102,69 @@ def clean(self): cleaned_data = super().clean() cleaned_data.pop("full_name", None) return cleaned_data + + +class NewMemberForm(forms.ModelForm): + admin_org_domain_request_permissions = forms.ChoiceField( + label="Select permission", + choices=[('view_only', 'View all requests'), ('view_and_create', 'View all requests plus create requests')], + widget=forms.RadioSelect, + required=True) + admin_org_members_permissions = forms.ChoiceField( + label="Select permission", choices=[('view_only', 'View all members'), ('view_and_create', 'View all members plus manage members')], widget=forms.RadioSelect, required=True) + basic_org_domain_request_permissions = forms.ChoiceField( + label="Select permission", choices=[('view_only', 'View all requests'), ('view_and_create', 'View all requests plus create requests'),('no_access', 'No access')], widget=forms.RadioSelect, required=True) + + email = forms.EmailField( + label="Enter the email of the member you'd like to invite", + max_length=None, + error_messages={ + "invalid": ("Enter an email address in the required format, like name@example.com."), + "required": ("Enter an email address in the required format, like name@example.com."), + }, + validators=[ + MaxLengthValidator( + 320, + message="Response must be less than 320 characters.", + ) + ], + required=True + ) + + class Meta: + model = User + fields = ['email'] #, 'grade', 'sport'] + + def __init__(self, *args, **kwargs): + super(NewMemberForm, self).__init__(*args, **kwargs) + # self.fields['sport'].choices = [] + + def clean(self): + cleaned_data = super().clean() + + # Lowercase the value of the 'email' field + email_value = cleaned_data.get("email") + if email_value: + cleaned_data["email"] = email_value.lower() + + # Check for an existing user (if there isn't any, send an invite) + if email_value: + try: + existingUser = User.objects.get(email=email_value) + except existingUser.DoesNotExist: + raise forms.ValidationError("User with this email does not exist.") + + # grade = cleaned_data.get('grade') + # sport = cleaned_data.get('sport') + + # # Handle sport options based on grade + # if grade == 'Junior': + # self.fields['sport'].choices = [('Basketball', 'Basketball'), ('Football', 'Football')] + # elif grade == 'Varsity': + # self.fields['sport'].choices = [('Swimming', 'Swimming'), ('Tennis', 'Tennis')] + + # # Ensure both sport and grade are selected and valid + # if not grade or not sport: + # raise forms.ValidationError("Both grade and sport must be selected.") + + return cleaned_data \ No newline at end of file diff --git a/src/registrar/templates/portfolio_members.html b/src/registrar/templates/portfolio_members.html index 82e06c808..5cddc026f 100644 --- a/src/registrar/templates/portfolio_members.html +++ b/src/registrar/templates/portfolio_members.html @@ -20,7 +20,7 @@

Members

- Add a new member diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html new file mode 100644 index 000000000..97e92b560 --- /dev/null +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -0,0 +1,117 @@ +{% extends 'portfolio_base.html' %} +{% load static url_helpers %} +{% load field_helpers %} + +{% block title %} Members | New Member {% endblock %} + +{% block wrapper_class %} + {{ block.super }} dashboard--grey-1 +{% endblock %} + +{% block portfolio_content %} +{% block messages %} + {% include "includes/form_messages.html" %} +{% endblock messages%} + +

+ +{% block new_member_header %} +

Add a new member

+{% endblock new_member_header %} + +{% include "includes/required_fields.html" %} + +{% block form_fields %} +
+
+ +

Email

+
+ + {% csrf_token %} + {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} + {% input_with_errors form.email %} + {% endwith %} + + +
+ +
+ +

Member Access

+
+ + + Select the level of access for this member. * +
+ + + + +
+
+ + + +
+ +

Admin access permissions

+

Member permissions available for admin-level acccess.

+
+ +

Organization domain requests

+ {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} + {% input_with_errors form.admin_org_domain_request_permissions %} + {% endwith %} + +

Organization Members

+ {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} + {% input_with_errors form.admin_org_members_permissions %} + {% endwith %} +
+ + +
+ +

Basic member permissions

+

Member permissions available for basic-level access

+ {% input_with_errors form.basic_org_domain_request_permissions %} +
+ +
+ +
+ + +
+ +
+{% endblock form_fields%} +{% endblock portfolio_content%} + + diff --git a/src/registrar/templates/portfolio_no_domains.html b/src/registrar/templates/portfolio_no_domains.html index 75ff3a91f..bc42a0e39 100644 --- a/src/registrar/templates/portfolio_no_domains.html +++ b/src/registrar/templates/portfolio_no_domains.html @@ -1,4 +1,4 @@ -{% extends 'portfolio_base.html' %} +{% extends 'portfolio_no_domains.html' %} {% load static %} diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 552fdb6ff..49925b2ef 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -1,12 +1,15 @@ import logging +from django.db import IntegrityError from django.http import Http404 -from django.shortcuts import render +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.contrib import messages -from registrar.forms.portfolio import PortfolioOrgAddressForm, PortfolioSeniorOfficialForm +from registrar.forms import portfolio as portfolioForms from registrar.models import Portfolio, User +from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from registrar.utility.email import EmailSendingError from registrar.views.utility.permission_views import ( PortfolioDomainRequestsPermissionView, PortfolioDomainsPermissionView, @@ -42,15 +45,6 @@ def get(self, request): return render(request, "portfolio_requests.html") -class PortfolioMembersView(PortfolioMembersPermissionView, View): - - template_name = "portfolio_members.html" - - def get(self, request): - """Add additional context data to the template.""" - return render(request, "portfolio_members.html") - - class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View): """Some users have access to the underlying portfolio, but not any domains. This is a custom view which explains that to the user - and denotes who to contact. @@ -116,7 +110,7 @@ class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin): model = Portfolio template_name = "portfolio_organization.html" - form_class = PortfolioOrgAddressForm + form_class = portfolioForms.PortfolioOrgAddressForm context_object_name = "portfolio" def get_context_data(self, **kwargs): @@ -179,7 +173,7 @@ class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin): model = Portfolio template_name = "portfolio_senior_official.html" - form_class = PortfolioSeniorOfficialForm + form_class = portfolioForms.PortfolioSeniorOfficialForm context_object_name = "portfolio" def get_object(self, queryset=None): @@ -200,3 +194,238 @@ def get(self, request, *args, **kwargs): self.object = self.get_object() form = self.get_form() return self.render_to_response(self.get_context_data(form=form)) + + +class PortfolioMembersView(PortfolioMembersPermissionView, View): + + template_name = "portfolio_members.html" + + def get(self, request): + """Add additional context data to the template.""" + return render(request, "portfolio_members.html") + + +class NewMemberView(PortfolioMembersPermissionView, FormMixin): + + # template_name = "portfolio_members_add_new.html" + # form = portfolioForms.NewMemberForm #[forms.NewMemberToggleForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm] + + model = UserPortfolioPermission + template_name = "portfolio_members_add_new.html" + form_class = portfolioForms.NewMemberForm + context_object_name = "userPortfolioPermission" + + def get_object(self, queryset=None): + """Get the portfolio object based on the session.""" + portfolio = self.request.session.get("portfolio") + if portfolio is None: + raise Http404("No organization found for this user") + return portfolio + + def get_form_kwargs(self): + """Include the instance in the form kwargs.""" + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.get_object() + return kwargs + + def get(self, request, *args, **kwargs): + """Handle GET requests to display the form.""" + self.object = self.get_object() + form = self.get_form() + return self.render_to_response(self.get_context_data(form=form)) + + ########################################## + # TODO: future ticket + # (save/invite new member) + ########################################## + + # def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True): + # """Performs the sending of the member invitation email + # email: string- email to send to + # add_success: bool- default True indicates: + # adding a success message to the view if the email sending succeeds + + # raises EmailSendingError + # """ + + # # Set a default email address to send to for staff + # requestor_email = settings.DEFAULT_FROM_EMAIL + + # # Check if the email requestor has a valid email address + # if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "": + # requestor_email = requestor.email + # elif not requestor.is_staff: + # messages.error(self.request, "Can't send invitation email. No email is associated with your account.") + # logger.error( + # f"Can't send email to '{email}' on domain '{self.object}'." + # f"No email exists for the requestor '{requestor.username}'.", + # exc_info=True, + # ) + # return None + + # # Check to see if an invite has already been sent + # try: + # invite = MemberInvitation.objects.get(email=email, domain=self.object) + # # check if the invite has already been accepted + # if invite.status == MemberInvitation.MemberInvitationStatus.RETRIEVED: + # add_success = False + # messages.warning( + # self.request, + # f"{email} is already a manager for this domain.", + # ) + # else: + # add_success = False + # # else if it has been sent but not accepted + # messages.warning(self.request, f"{email} has already been invited to this domain") + # except Exception: + # logger.error("An error occured") + + # try: + # send_templated_email( + # "emails/member_invitation.txt", + # "emails/member_invitation_subject.txt", + # to_address=email, + # context={ + # "portfolio": self.object, + # "requestor_email": requestor_email, + # }, + # ) + # except EmailSendingError as exc: + # logger.warn( + # "Could not sent email invitation to %s for domain %s", + # email, + # self.object, + # exc_info=True, + # ) + # raise EmailSendingError("Could not send email invitation.") from exc + # else: + # if add_success: + # messages.success(self.request, f"{email} has been invited to this domain.") + + # def _make_invitation(self, email_address: str, requestor: User): + # """Make a Member invitation for this email and redirect with a message.""" + # try: + # self._send_member_invitation_email(email=email_address, requestor=requestor) + # except EmailSendingError: + # messages.warning(self.request, "Could not send email invitation.") + # else: + # # (NOTE: only create a MemberInvitation if the e-mail sends correctly) + # MemberInvitation.objects.get_or_create(email=email_address, domain=self.object) + # return redirect(self.get_success_url()) + + # def form_valid(self, form): + + # """Add the specified user as a member + # for this portfolio. + # Throws EmailSendingError.""" + # requested_email = form.cleaned_data["email"] + # requestor = self.request.user + # # look up a user with that email + # try: + # requested_user = User.objects.get(email=requested_email) + # except User.DoesNotExist: + # # no matching user, go make an invitation + # return self._make_invitation(requested_email, requestor) + # else: + # # if user already exists then just send an email + # try: + # self._send_member_invitation_email(requested_email, requestor, add_success=False) + # except EmailSendingError: + # logger.warn( + # "Could not send email invitation (EmailSendingError)", + # self.object, + # exc_info=True, + # ) + # messages.warning(self.request, "Could not send email invitation.") + # except Exception: + # logger.warn( + # "Could not send email invitation (Other Exception)", + # self.object, + # exc_info=True, + # ) + # messages.warning(self.request, "Could not send email invitation.") + + # try: + # UserPortfolioPermission.objects.create( + # user=requested_user, + # portfolio=self.object, + # role=UserDomainRole.Roles.MANAGER, + # ) + # except IntegrityError: + # messages.warning(self.request, f"{requested_email} is already a member of this portfolio") + # else: + # messages.success(self.request, f"Added user {requested_email}.") + # return redirect(self.get_success_url()) + + + + + + + + + + + + + +# class NewMemberView(PortfolioMembersPermissionView, FormMixin): +# form = portfolioForms.NewMemberForm +# template_name = 'portfolio_members_add_new.html' # Assuming you have a template file for the form + +# # model = UserPortfolioPermission +# # template_name = "portfolio_members_add_new.html" +# # form_class = portfolioForms.NewMemberForm +# # context_object_name = "userPortfolioPermission" + +# def get_success_url(self): +# return reverse('success') # Redirect after successful submission + +# def get_context_data(self, **kwargs): +# """Add additional context data to the template.""" +# #TODO: Add permissions to context +# context = super().get_context_data(**kwargs) +# portfolio = self.request.session.get("portfolio") +# context["has_invite_members_permission"] = self.request.user.has_edit_members_portfolio_permission(portfolio) +# return context + +# def form_valid(self, form): +# # Get the cleaned data from the form +# cleaned_data = form.cleaned_data +# email = cleaned_data.get('email') +# # grade = cleaned_data.get('grade') +# # sport = cleaned_data.get('sport') + +# ########################################## +# # TODO: future ticket +# # (validate and save/invite new member here) +# ########################################## + +# # Lookup member by email +# # member = get_object_or_404(User, email=email) + +# # Check existing portfolio permissions +# # TODO: future ticket -- check for existing portfolio permissions, multipe portfolio flags, etc. +# # school = self.get_context_data()['school'] + +# # Update student school information +# # student.school = school +# # student.save() + +# # Create or update the SportEnrollment for this student +# # SportEnrollment.objects.create( +# # student=student, +# # grade=grade, +# # sport=sport +# # ) + +# return super().form_valid(form) + +# def form_invalid(self, form): +# # If the form is invalid, show errors +# return self.render_to_response(self.get_context_data(form=form)) + + +# def get(self, request): +# return render(request, "portfolio_members_add_new.html") + diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 414e58275..6175b6104 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -253,7 +253,7 @@ class PortfolioDomainRequestsPermissionView(PortfolioDomainRequestsPermission, P class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC): - """Abstract base view for portfolio domain request views that enforces permissions. + """Abstract base view for portfolio members views that enforces permissions. This abstract view cannot be instantiated. Actual views must specify `template_name`. From 14d0876ed4b2d4e31bf43d484df0a056936a9968 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 21 Oct 2024 22:28:09 -0600 Subject: [PATCH 05/73] We have basic validation.....but there is a snag with hidden fields --- src/registrar/forms/portfolio.py | 4 --- .../templates/portfolio_members_add_new.html | 18 +++++++------ src/registrar/views/portfolios.py | 26 +++++++++++++++++++ 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 4bc7ec046..ab3b3a269 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -135,10 +135,6 @@ class Meta: model = User fields = ['email'] #, 'grade', 'sport'] - def __init__(self, *args, **kwargs): - super(NewMemberForm, self).__init__(*args, **kwargs) - # self.fields['sport'].choices = [] - def clean(self): cleaned_data = super().clean() diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 97e92b560..29f6fc2d7 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -9,10 +9,13 @@ {% endblock %} {% block portfolio_content %} + + {% block messages %} {% include "includes/form_messages.html" %} {% endblock messages%} + + {% block new_member_header %}

Add a new member

{% endblock new_member_header %} {% include "includes/required_fields.html" %} -{% block form_fields %} -
+

Email

@@ -45,12 +48,12 @@

Email

+

Member Access

- Select the level of access for this member. *
@@ -70,8 +73,7 @@

Member Access

- - +

Admin access permissions

@@ -89,7 +91,7 @@

Organization Members

{% endwith %}
- +

Basic member permissions

@@ -99,6 +101,7 @@

Basic member permissions

+
-
-{% endblock form_fields%} + {% endblock portfolio_content%} diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 49925b2ef..0674080d1 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -233,6 +233,32 @@ def get(self, request, *args, **kwargs): self.object = self.get_object() form = self.get_form() return self.render_to_response(self.get_context_data(form=form)) + + def post(self, request, *args, **kwargs): + """Handle POST requests to process form submission.""" + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + """Handle the case when the form is valid.""" + # self.object = form.save(commit=False) + # self.object.creator = self.request.user + # self.object.save() + # messages.success(self.request, "The organization information for this portfolio has been updated.") + return super().form_valid(form) + + def form_invalid(self, form): + """Handle the case when the form is invalid.""" + return self.render_to_response(self.get_context_data(form=form)) + + def get_success_url(self): + """Redirect to the overview page for the portfolio.""" + return reverse("members") + ########################################## # TODO: future ticket From 0af4727c56684faa53a7c64d1f71697d897ca92b Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 21 Oct 2024 23:17:22 -0600 Subject: [PATCH 06/73] linted again (and some experiments with form validation) --- src/registrar/templates/portfolio_members_add_new.html | 1 + src/registrar/views/portfolios.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 29f6fc2d7..67d3da3dd 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -11,6 +11,7 @@ {% block portfolio_content %} +{% include "includes/form_errors.html" with form=form %} {% block messages %} {% include "includes/form_messages.html" %} {% endblock messages%} diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 98e0fc43c..92e6f82a5 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -357,10 +357,8 @@ def get(self, request): class NewMemberView(PortfolioMembersPermissionView, FormMixin): - model = UserPortfolioPermission template_name = "portfolio_members_add_new.html" form_class = portfolioForms.NewMemberForm - context_object_name = "userPortfolioPermission" def get_object(self, queryset=None): """Get the portfolio object based on the session.""" 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 07/73] 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 08/73] 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 09/73] 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 10/73] 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 11/73] 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 12/73] 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 13/73] 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 14/73] 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 15/73] 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 16/73] 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 17/73] 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 de0e0d55c6288f0a2423c0648a42faa70b901df9 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 23 Oct 2024 23:58:48 -0600 Subject: [PATCH 18/73] finally created overrides for those custom radio buttons that work --- src/registrar/assets/js/get-gov.js | 3 +- src/registrar/forms/portfolio.py | 35 +++++++++++++++++++ .../templates/portfolio_members_add_new.html | 33 +++++++++-------- src/registrar/views/portfolios.py | 2 +- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index cca25bbfa..e6f1e3e6c 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -918,7 +918,8 @@ function setupUrbanizationToggle(stateTerritoryField) { * */ (function newMemberFormListener() { - HookupYesNoListener("new_member-permission_level",'new-member-admin-permissions', 'new-member-basic-permissions') + // HookupYesNoListener("new_member-permission_level",'new-member-admin-permissions', 'new-member-basic-permissions') + HookupYesNoListener("member_access_level",'new-member-admin-permissions', 'new-member-basic-permissions') })(); /** diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index b5c93ab59..c67586529 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -165,17 +165,32 @@ class Meta: class NewMemberForm(forms.ModelForm): + member_access_level = forms.ChoiceField( + label="Select permission", + choices=[("True", "Admin Access"), ("False", "Basic Access")], + widget=forms.RadioSelect(attrs={'class': 'usa-radio__input usa-radio__input--tile'}), + required=True, + error_messages={ + "required": "Member access level is required", + }, + ) admin_org_domain_request_permissions = forms.ChoiceField( label="Select permission", choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")], widget=forms.RadioSelect, required=True, + error_messages={ + "required": "Domain request permission is required", + }, ) admin_org_members_permissions = forms.ChoiceField( label="Select permission", choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")], widget=forms.RadioSelect, required=True, + error_messages={ + "required": "Member permission is required", + }, ) basic_org_domain_request_permissions = forms.ChoiceField( label="Select permission", @@ -186,6 +201,9 @@ class NewMemberForm(forms.ModelForm): ], widget=forms.RadioSelect, required=True, + error_messages={ + "required": "Member permission is required", + }, ) email = forms.EmailField( @@ -223,4 +241,21 @@ def clean(self): # except User.DoesNotExist: # raise forms.ValidationError("User with this email does not exist.") + # Get the grade and sport from POST data + permission_level = cleaned_data.get("member_access_level") + # permission_level = self.data.get('new_member-permission_level') + if not permission_level: + for field in self.fields: + if field in self.errors and field != "email" and field != "member_access_level": + del self.errors[field] + return cleaned_data + + # Validate the sport based on the selected grade + if permission_level == "True": + #remove the error messages pertaining to basic permission inputs + del self.errors["basic_org_domain_request_permissions"] + else: + #remove the error messages pertaining to admin permission inputs + del self.errors["admin_org_domain_request_permissions"] + del self.errors["admin_org_members_permissions"] return cleaned_data diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 67d3da3dd..3a390a470 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -56,22 +56,25 @@

Member Access

Select the level of access for this member. * + + {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
- - - - + {% for radio in form.member_access_level %} + {{ radio.tag }} + + {% endfor %}
+ {% endwith %} +
@@ -86,7 +89,7 @@

Organization domain requests

{% input_with_errors form.admin_org_domain_request_permissions %} {% endwith %} -

Organization Members

+

Organization members

{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} {% input_with_errors form.admin_org_members_permissions %} {% endwith %} diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 92e6f82a5..bd9a10dd8 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -401,7 +401,7 @@ def form_invalid(self, form): return self.render_to_response(self.get_context_data(form=form)) def get_success_url(self): - """Redirect to the overview page for the portfolio.""" + """Redirect to members table.""" return reverse("members") ########################################## From 5250cee1a036286974c611321c04460a35c98757 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 24 Oct 2024 00:12:03 -0600 Subject: [PATCH 19/73] refactored radio toggler listeners to be more flexible --- src/registrar/assets/js/get-gov.js | 65 ++++++++++++++++++++++-------- src/registrar/forms/portfolio.py | 2 +- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e6f1e3e6c..6f8ab2fa6 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -297,28 +297,56 @@ function clearValidators(el) { * radio button is false (hides this element if true) * **/ function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToShowIfNo) { + HookupRadioTogglerListener(radioButtonName, { + 'True': elementIdToShowIfYes, + 'False': elementIdToShowIfNo + }); +} + +/** + * Hookup listeners for radio togglers in form fields. + * + * Parameters: + * - radioButtonName: The "name=" value for the radio buttons being used as togglers + * - valueToElementMap: An object where keys are the values of the radio buttons, + * and values are the corresponding DOM element IDs to show. All other elements will be hidden. + * + * Usage Example: + * Assuming you have radio buttons with values 'option1', 'option2', and 'option3', + * and corresponding DOM IDs 'section1', 'section2', 'section3'. + * + * HookupValueBasedListener('exampleRadioGroup', { + * 'option1': 'section1', + * 'option2': 'section2', + * 'option3': 'section3' + * }); + **/ +function HookupRadioTogglerListener(radioButtonName, valueToElementMap) { // Get the radio buttons let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]'); + + // Extract the list of all element IDs from the valueToElementMap + let allElementIds = Object.values(valueToElementMap); function handleRadioButtonChange() { - // Check the value of the selected radio button - // Attempt to find the radio button element that is checked + // Find the checked radio button let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked'); - - // Check if the element exists before accessing its value let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; - switch (selectedValue) { - case 'True': - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1); - break; - - case 'False': - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2); - break; + // Hide all elements by default + allElementIds.forEach(function (elementId) { + let element = document.getElementById(elementId); + if (element) { + element.style.display = 'none'; + } + }); - default: - toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0); + // Show the relevant element for the selected value + if (selectedValue && valueToElementMap[selectedValue]) { + let elementToShow = document.getElementById(valueToElementMap[selectedValue]); + if (elementToShow) { + elementToShow.style.display = 'block'; + } } } @@ -328,11 +356,12 @@ function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToS radioButton.addEventListener('change', handleRadioButtonChange); }); - // initialize + // Initialize by checking the current state handleRadioButtonChange(); } } + // A generic display none/block toggle function that takes an integer param to indicate how the elements toggle function toggleTwoDomElements(ele1, ele2, index) { let element1 = document.getElementById(ele1); @@ -918,8 +947,10 @@ function setupUrbanizationToggle(stateTerritoryField) { * */ (function newMemberFormListener() { - // HookupYesNoListener("new_member-permission_level",'new-member-admin-permissions', 'new-member-basic-permissions') - HookupYesNoListener("member_access_level",'new-member-admin-permissions', 'new-member-basic-permissions') + HookupRadioTogglerListener('member_access_level', { + 'admin': 'new-member-admin-permissions', + 'basic': 'new-member-basic-permissions' + }); })(); /** diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index c67586529..4d59110ed 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -167,7 +167,7 @@ class Meta: class NewMemberForm(forms.ModelForm): member_access_level = forms.ChoiceField( label="Select permission", - choices=[("True", "Admin Access"), ("False", "Basic Access")], + choices=[("admin", "Admin Access"), ("basic", "Basic Access")], widget=forms.RadioSelect(attrs={'class': 'usa-radio__input usa-radio__input--tile'}), required=True, error_messages={ From 5fc0342426faa72d6584d0baf19f1d398876b242 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 24 Oct 2024 00:57:36 -0600 Subject: [PATCH 20/73] linted --- src/registrar/forms/portfolio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 4d59110ed..e99c2db71 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -168,7 +168,7 @@ class NewMemberForm(forms.ModelForm): member_access_level = forms.ChoiceField( label="Select permission", choices=[("admin", "Admin Access"), ("basic", "Basic Access")], - widget=forms.RadioSelect(attrs={'class': 'usa-radio__input usa-radio__input--tile'}), + widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}), required=True, error_messages={ "required": "Member access level is required", @@ -252,10 +252,10 @@ def clean(self): # Validate the sport based on the selected grade if permission_level == "True": - #remove the error messages pertaining to basic permission inputs + # remove the error messages pertaining to basic permission inputs del self.errors["basic_org_domain_request_permissions"] else: - #remove the error messages pertaining to admin permission inputs + # remove the error messages pertaining to admin permission inputs del self.errors["admin_org_domain_request_permissions"] del self.errors["admin_org_members_permissions"] return cleaned_data 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 21/73] 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 22/73] 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 23/73] 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 24/73] 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 25/73] 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 26/73] 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 27/73] 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 28/73] 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 29/73] 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 30/73] 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 aeb496ceaec0a4ec27ebca04559fd255f82f1abd Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:34:44 -0600 Subject: [PATCH 31/73] initial changes --- src/registrar/admin.py | 34 +++++++++++++++++++----- src/registrar/assets/js/get-gov-admin.js | 3 +++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 0b96b4c48..d3530101d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -5,6 +5,7 @@ from django.db.models import Value, CharField, Q from django.db.models.functions import Concat, Coalesce from django.http import HttpResponseRedirect +from registrar.models.federal_agency import FederalAgency from registrar.utility.admin_helpers import ( get_action_needed_reason_default_email, get_rejection_reason_default_email, @@ -3192,6 +3193,9 @@ def get_fieldsets(self, request, obj=None): return self.add_fieldsets return super().get_fieldsets(request, obj) + # TODO - I think the solution to this may just be to set this as editable. + # Then in the underlying page override, we need a fake readonly display that + # is toggled on or off. def get_readonly_fields(self, request, obj=None): """Set the read-only state on form elements. We have 2 conditions that determine which fields are read-only: @@ -3206,6 +3210,14 @@ def get_readonly_fields(self, request, obj=None): # straightforward and the readonly_fields list can control their behavior readonly_fields.extend([field.name for field in self.model._meta.fields]) + # Make senior_official readonly for federal organizations + if obj and obj.organization_type == obj.OrganizationChoices.FEDERAL: + if "senior_official" not in readonly_fields: + readonly_fields.append("senior_official") + elif "senior_official" in readonly_fields: + # Remove senior_official from readonly_fields if org is non-federal + readonly_fields.remove("senior_official") + if request.user.has_perm("registrar.full_access_permission"): return readonly_fields @@ -3228,7 +3240,7 @@ def change_view(self, request, object_id, form_url="", extra_context=None): extra_context["domain_requests"] = obj.get_domain_requests(order_by=["requested_domain__name"]) return super().change_view(request, object_id, form_url, extra_context) - def save_model(self, request, obj, form, change): + def save_model(self, request, obj: Portfolio, form, change): if hasattr(obj, "creator") is False: # ---- update creator ---- @@ -3243,12 +3255,20 @@ def save_model(self, request, obj, form, change): if is_federal and obj.organization_name is None: obj.organization_name = obj.federal_agency.agency - # Remove this line when senior_official is no longer readonly in /admin. - if obj.federal_agency: - if obj.federal_agency.so_federal_agency.exists(): - obj.senior_official = obj.federal_agency.so_federal_agency.first() - else: - obj.senior_official = None + # TODO - this should be handled almost entirely in javascript + # Handle the federal agency and senior official fields + if obj.organization_type == obj.OrganizationChoices.FEDERAL: + if obj.federal_agency: + if obj.federal_agency.so_federal_agency.exists(): + obj.senior_official = obj.federal_agency.so_federal_agency.first() + else: + obj.senior_official = None + else: + if obj.federal_agency and obj.federal_agency.agency != "Non-Federal Agency": + if obj.federal_agency.so_federal_agency.first() == obj.senior_official: + obj.senior_official = None + obj.federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() + super().save_model(request, obj, form, change) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index fd50fbb0c..a3c8b1410 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -927,6 +927,7 @@ document.addEventListener('DOMContentLoaded', function() { // This is the additional information that exists beneath the SO element. var contactList = document.querySelector(".field-senior_official .dja-address-contact-list"); + const federalAgencyContainer = document.querySelector(".field-federal_agency"); document.addEventListener('DOMContentLoaded', function() { let isPortfolioPage = document.getElementById("portfolio_form"); @@ -975,11 +976,13 @@ document.addEventListener('DOMContentLoaded', function() { let selectedValue = organizationType.value; if (selectedValue === "federal") { hideElement(organizationNameContainer); + showElement(federalAgencyContainer); if (federalType) { showElement(federalType); } } else { showElement(organizationNameContainer); + hideElement(federalAgencyContainer); if (federalType) { hideElement(federalType); } From 80b24698800fbd70265d78ef33175c519bca284e Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:11:13 -0700 Subject: [PATCH 32/73] Add screenreader only text to ds delete button --- src/registrar/templates/domain_dsdata.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 6e18bce13..ba742ab09 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -63,11 +63,12 @@

DS data record {{forloop.counter}}

- +
From c2a07a8983682f7b80285112522b56799f135ccc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:08:00 -0600 Subject: [PATCH 33/73] unit tests --- src/registrar/admin.py | 9 ++--- src/registrar/tests/test_admin.py | 60 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index d3530101d..2151d6f3c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -3193,9 +3193,6 @@ def get_fieldsets(self, request, obj=None): return self.add_fieldsets return super().get_fieldsets(request, obj) - # TODO - I think the solution to this may just be to set this as editable. - # Then in the underlying page override, we need a fake readonly display that - # is toggled on or off. def get_readonly_fields(self, request, obj=None): """Set the read-only state on form elements. We have 2 conditions that determine which fields are read-only: @@ -3241,7 +3238,6 @@ def change_view(self, request, object_id, form_url="", extra_context=None): return super().change_view(request, object_id, form_url, extra_context) def save_model(self, request, obj: Portfolio, form, change): - if hasattr(obj, "creator") is False: # ---- update creator ---- # Set the creator field to the current admin user @@ -3255,8 +3251,8 @@ def save_model(self, request, obj: Portfolio, form, change): if is_federal and obj.organization_name is None: obj.organization_name = obj.federal_agency.agency - # TODO - this should be handled almost entirely in javascript - # Handle the federal agency and senior official fields + # Set the senior official field to the senior official on the federal agency + # when federal - otherwise, clear the field. if obj.organization_type == obj.OrganizationChoices.FEDERAL: if obj.federal_agency: if obj.federal_agency.so_federal_agency.exists(): @@ -3269,7 +3265,6 @@ def save_model(self, request, obj: Portfolio, form, change): obj.senior_official = None obj.federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() - super().save_model(request, obj, form, change) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 9c5e3b582..2677462df 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2103,6 +2103,66 @@ def test_portfolio_members_display(self): display_members = self.admin.display_members(self.portfolio) self.assertIn(f'2 members', display_members) + @less_console_noise_decorator + def test_senior_official_readonly_for_federal_org(self): + """Test that senior_official field is readonly for federal organizations""" + request = self.factory.get("/") + request.user = self.superuser + + # Create a federal portfolio + portfolio = Portfolio.objects.create( + organization_name="Test Federal Org", + organization_type=DomainRequest.OrganizationChoices.FEDERAL, + creator=self.superuser, + ) + + readonly_fields = self.admin.get_readonly_fields(request, portfolio) + self.assertIn("senior_official", readonly_fields) + + # Change to non-federal org + portfolio.organization_type = DomainRequest.OrganizationChoices.CITY + readonly_fields = self.admin.get_readonly_fields(request, portfolio) + self.assertNotIn("senior_official", readonly_fields) + + @less_console_noise_decorator + def test_senior_official_auto_assignment(self): + """Test automatic senior official assignment based on organization type and federal agency""" + request = self.factory.get("/") + request.user = self.superuser + + # Create a federal agency with a senior official + federal_agency = FederalAgency.objects.create(agency="Test Agency") + senior_official = SeniorOfficial.objects.create( + first_name="Test", + last_name="Official", + title="Some guy", + email="test@example.gov", + federal_agency=federal_agency, + ) + + # Create a federal portfolio + portfolio = Portfolio.objects.create( + organization_name="Test Federal Org", + organization_type=DomainRequest.OrganizationChoices.FEDERAL, + creator=self.superuser, + ) + + # Test that the federal org gets senior official from agency when federal + portfolio.federal_agency = federal_agency + self.admin.save_model(request, portfolio, form=None, change=False) + self.assertEqual(portfolio.senior_official, senior_official) + + # Test non-federal org clears senior official when not city + portfolio.organization_type = DomainRequest.OrganizationChoices.CITY + self.admin.save_model(request, portfolio, form=None, change=True) + self.assertIsNone(portfolio.senior_official) + self.assertEqual(portfolio.federal_agency.agency, "Non-Federal Agency") + + # Cleanup + senior_official.delete() + federal_agency.delete() + portfolio.delete() + class TestTransferUser(WebTest): """User transfer custom admin page""" From 641692d9633d2cdced344596c8deb37cdb6fe9b1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:05:56 -0600 Subject: [PATCH 34/73] lint --- src/registrar/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 2151d6f3c..77ecb079a 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -3241,7 +3241,7 @@ def save_model(self, request, obj: Portfolio, form, change): if hasattr(obj, "creator") is False: # ---- update creator ---- # Set the creator field to the current admin user - obj.creator = request.user if request.user.is_authenticated else None + obj.creator = request.user if request.user.is_authenticated else None # type: ignore # ---- update organization name ---- # org name will be the same as federal agency, if it is federal, # otherwise it will be the actual org name. If nothing is entered for @@ -3263,7 +3263,7 @@ def save_model(self, request, obj: Portfolio, form, change): if obj.federal_agency and obj.federal_agency.agency != "Non-Federal Agency": if obj.federal_agency.so_federal_agency.first() == obj.senior_official: obj.senior_official = None - obj.federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() + obj.federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() # type: ignore super().save_model(request, obj, form, change) From 1bea78f872ff8fe53cdaa06919c1955c288d49d7 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:16:19 -0700 Subject: [PATCH 35/73] Add aria label option for inputs --- .../templates/django/forms/widgets/input.html | 1 + src/registrar/templates/domain_nameservers.html | 3 ++- .../templates/includes/input_with_errors.html | 2 +- src/registrar/templatetags/field_helpers.py | 10 +++++++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/django/forms/widgets/input.html b/src/registrar/templates/django/forms/widgets/input.html index f47fc6415..e7b43655d 100644 --- a/src/registrar/templates/django/forms/widgets/input.html +++ b/src/registrar/templates/django/forms/widgets/input.html @@ -4,6 +4,7 @@ {# hint: spacing in the class string matters #} class="{{ uswds_input_class }}{% if classes %} {{ classes }}{% endif %}" {% if widget.value != None %}value="{{ widget.value|stringformat:'s' }}"{% endif %} + {% if aria_label %}aria-label="{{ aria_label }} {{ label }}"{% endif %} {% if sublabel_text %}aria-describedby="{{ widget.attrs.id }}__sublabel"{% endif %} {% include "django/forms/widgets/attrs.html" %} /> diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index fd6256f39..cc1fc0164 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -47,7 +47,7 @@

DNS name servers

{% endwith %}
- {% with sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_group_class="usa-form-group--unstyled-error" %} + {% with label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_group_class="usa-form-group--unstyled-error" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label %} {% input_with_errors form.ip %} {% endwith %}
@@ -56,6 +56,7 @@

DNS name servers

Delete + Name server {{forloop.counter}} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index d1e53968e..a7df4c052 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -66,7 +66,7 @@ {% if toggleable_input %} {% include "includes/toggleable_input.html" %} {% endif %} - + {# this is the input field, itself #} {% include widget.template_name %} diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index 68a803711..8a80a75b9 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -24,6 +24,7 @@ def input_with_errors(context, field=None): # noqa: C901 add_label_class: append to input element's label's `class` attribute add_legend_class: append to input element's legend's `class` attribute add_group_class: append to input element's surrounding tag's `class` attribute + add_aria_label: append to input element's `aria_label` attribute attr_* - adds or replaces any single html attribute for the input add_error_attr_* - like `attr_*` but only if field.errors is not empty toggleable_input: shows a simple edit button, and adds display-none to the input field. @@ -55,6 +56,7 @@ def input_with_errors(context, field=None): # noqa: C901 label_classes = [] legend_classes = [] group_classes = [] + aria_labels = [] # this will be converted to an attribute string described_by = [] @@ -98,6 +100,9 @@ def input_with_errors(context, field=None): # noqa: C901 if "display-none" not in classes: classes.append("display-none") + elif key == "add_aria_label": + aria_labels.append(value) + attrs["id"] = field.auto_id # do some work for various edge cases @@ -151,7 +156,10 @@ def input_with_errors(context, field=None): # noqa: C901 # ensure we don't overwrite existing attribute value if "aria-describedby" in attrs: described_by.append(attrs["aria-describedby"]) - attrs["aria-describedby"] = " ".join(described_by) + attrs["aria_describedby"] = " ".join(described_by) + + if aria_labels: + context["aria_label"] = " ".join(aria_labels) # ask Django to give us the widget dict # see Widget.get_context() on From e72d1caaaec5cade5aaf6826db8047a4f94207a2 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:19:02 -0700 Subject: [PATCH 36/73] Revert input_with_errors --- src/registrar/templates/includes/input_with_errors.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index a7df4c052..d1e53968e 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -66,7 +66,7 @@ {% if toggleable_input %} {% include "includes/toggleable_input.html" %} {% endif %} - + {# this is the input field, itself #} {% include widget.template_name %} 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 37/73] 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 38/73] 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 39/73] 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 40/73] 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 41/73] 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 42/73] 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 43/73] 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 44/73] 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 45/73] 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 46/73] 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 47/73] 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 ba7de7c67815343623a73be5b376e2c87d1e1477 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 30 Oct 2024 15:40:00 -0600 Subject: [PATCH 48/73] Implemented PR feedback --- src/registrar/assets/js/get-gov.js | 4 +- src/registrar/forms/portfolio.py | 39 +++++++---- .../templates/portfolio_members_add_new.html | 4 +- src/registrar/views/portfolios.py | 70 +------------------ 4 files changed, 31 insertions(+), 86 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 6f8ab2fa6..602d9cf79 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -337,7 +337,7 @@ function HookupRadioTogglerListener(radioButtonName, valueToElementMap) { allElementIds.forEach(function (elementId) { let element = document.getElementById(elementId); if (element) { - element.style.display = 'none'; + hideElement(element); } }); @@ -345,7 +345,7 @@ function HookupRadioTogglerListener(radioButtonName, valueToElementMap) { if (selectedValue && valueToElementMap[selectedValue]) { let elementToShow = document.getElementById(valueToElementMap[selectedValue]); if (elementToShow) { - elementToShow.style.display = 'block'; + showElement(elementToShow); } } } diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index e99c2db71..4d6ec81cb 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -180,7 +180,7 @@ class NewMemberForm(forms.ModelForm): widget=forms.RadioSelect, required=True, error_messages={ - "required": "Domain request permission is required", + "required": "Admin domain request permission is required", }, ) admin_org_members_permissions = forms.ChoiceField( @@ -189,7 +189,7 @@ class NewMemberForm(forms.ModelForm): widget=forms.RadioSelect, required=True, error_messages={ - "required": "Member permission is required", + "required": "Admin member permission is required", }, ) basic_org_domain_request_permissions = forms.ChoiceField( @@ -202,7 +202,7 @@ class NewMemberForm(forms.ModelForm): widget=forms.RadioSelect, required=True, error_messages={ - "required": "Member permission is required", + "required": "Basic member permission is required", }, ) @@ -234,6 +234,11 @@ def clean(self): if email_value: cleaned_data["email"] = email_value.lower() + + ########################################## + # TODO: future ticket + # (invite new member) + ########################################## # Check for an existing user (if there isn't any, send an invite) # if email_value: # try: @@ -241,21 +246,29 @@ def clean(self): # except User.DoesNotExist: # raise forms.ValidationError("User with this email does not exist.") - # Get the grade and sport from POST data - permission_level = cleaned_data.get("member_access_level") - # permission_level = self.data.get('new_member-permission_level') - if not permission_level: + member_access_level = cleaned_data.get("member_access_level") + + # Intercept the error messages so that we don't validate hidden inputs + if not member_access_level: + # If no member access level has been selected, delete error messages + # for all hidden inputs (which is everything except the e-mail input + # and member access selection) for field in self.fields: if field in self.errors and field != "email" and field != "member_access_level": del self.errors[field] return cleaned_data + + basic_dom_req_error = "basic_org_domain_request_permissions" + admin_dom_req_error = "admin_org_domain_request_permissions" + admin_member_error = "admin_org_members_permissions" - # Validate the sport based on the selected grade - if permission_level == "True": + if member_access_level == "admin" and basic_dom_req_error in self.errors: # remove the error messages pertaining to basic permission inputs - del self.errors["basic_org_domain_request_permissions"] - else: + del self.errors[basic_dom_req_error] + elif member_access_level == "basic": # remove the error messages pertaining to admin permission inputs - del self.errors["admin_org_domain_request_permissions"] - del self.errors["admin_org_members_permissions"] + if admin_dom_req_error in self.errors: + del self.errors[admin_dom_req_error] + if admin_member_error in self.errors: + del self.errors[admin_member_error] return cleaned_data diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 3a390a470..6d8e449f6 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -65,9 +65,9 @@

Member Access

{{ radio.choice_label }}

{% if radio.choice_label == "Admin Access" %} - Grants this member access to the organization-wide information on domains, domain requests, and members. Domain management can be assigned separately. + Grants this member access to the organization-wide information on domains, domain requests, and members. Domain management can be assigned separately. {% else %} - Grants this member access to the organization. They can be given extra permissions to view all organization domain requests and submit domain requests on behald of the organization. Basica access members can't view all members of an organization or manage them. Domain management can be assigned separacterly. + Grants this member access to the organization. They can be given extra permissions to view all organization domain requests and submit domain requests on behalf of the organization. Basic access members can’t view all members of an organization or manage them. Domain management can be assigned separately. {% endif %}

diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index bd9a10dd8..ebb90cda1 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -388,13 +388,6 @@ def post(self, request, *args, **kwargs): else: return self.form_invalid(form) - def form_valid(self, form): - """Handle the case when the form is valid.""" - # self.object = form.save(commit=False) - # self.object.creator = self.request.user - # self.object.save() - # messages.success(self.request, "The organization information for this portfolio has been updated.") - return super().form_valid(form) def form_invalid(self, form): """Handle the case when the form is invalid.""" @@ -405,7 +398,7 @@ def get_success_url(self): return reverse("members") ########################################## - # TODO: future ticket + # TODO: future ticket #2854 # (save/invite new member) ########################################## @@ -526,64 +519,3 @@ def get_success_url(self): # else: # messages.success(self.request, f"Added user {requested_email}.") # return redirect(self.get_success_url()) - - -# class NewMemberView(PortfolioMembersPermissionView, FormMixin): -# form = portfolioForms.NewMemberForm -# template_name = 'portfolio_members_add_new.html' # Assuming you have a template file for the form - -# # model = UserPortfolioPermission -# # template_name = "portfolio_members_add_new.html" -# # form_class = portfolioForms.NewMemberForm -# # context_object_name = "userPortfolioPermission" - -# def get_success_url(self): -# return reverse('success') # Redirect after successful submission - -# def get_context_data(self, **kwargs): -# """Add additional context data to the template.""" -# #TODO: Add permissions to context -# context = super().get_context_data(**kwargs) -# portfolio = self.request.session.get("portfolio") -# context["has_invite_members_permission"] = self.request.user.has_edit_members_portfolio_permission(portfolio) -# return context - -# def form_valid(self, form): -# # Get the cleaned data from the form -# cleaned_data = form.cleaned_data -# email = cleaned_data.get('email') -# # grade = cleaned_data.get('grade') -# # sport = cleaned_data.get('sport') - -# ########################################## -# # TODO: future ticket -# # (validate and save/invite new member here) -# ########################################## - -# # Lookup member by email -# # member = get_object_or_404(User, email=email) - -# # Check existing portfolio permissions -# # TODO: future ticket -- check for existing portfolio permissions, multipe portfolio flags, etc. -# # school = self.get_context_data()['school'] - -# # Update student school information -# # student.school = school -# # student.save() - -# # Create or update the SportEnrollment for this student -# # SportEnrollment.objects.create( -# # student=student, -# # grade=grade, -# # sport=sport -# # ) - -# return super().form_valid(form) - -# def form_invalid(self, form): -# # If the form is invalid, show errors -# return self.render_to_response(self.get_context_data(form=form)) - - -# def get(self, request): -# return render(request, "portfolio_members_add_new.html") From 0153627a9e29fd4f7d72a18d34ef6344e907f565 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 30 Oct 2024 15:47:09 -0600 Subject: [PATCH 49/73] linted --- src/registrar/forms/portfolio.py | 7 +++---- src/registrar/views/portfolios.py | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 4d6ec81cb..5309f7263 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -234,7 +234,6 @@ def clean(self): if email_value: cleaned_data["email"] = email_value.lower() - ########################################## # TODO: future ticket # (invite new member) @@ -257,7 +256,7 @@ def clean(self): if field in self.errors and field != "email" and field != "member_access_level": del self.errors[field] return cleaned_data - + basic_dom_req_error = "basic_org_domain_request_permissions" admin_dom_req_error = "admin_org_domain_request_permissions" admin_member_error = "admin_org_members_permissions" @@ -267,8 +266,8 @@ def clean(self): del self.errors[basic_dom_req_error] elif member_access_level == "basic": # remove the error messages pertaining to admin permission inputs - if admin_dom_req_error in self.errors: + if admin_dom_req_error in self.errors: del self.errors[admin_dom_req_error] - if admin_member_error in self.errors: + if admin_member_error in self.errors: del self.errors[admin_member_error] return cleaned_data diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 9e4d2c5a6..1dbab2913 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -423,7 +423,6 @@ def post(self, request, *args, **kwargs): else: return self.form_invalid(form) - def form_invalid(self, form): """Handle the case when the form is invalid.""" return self.render_to_response(self.get_context_data(form=form)) 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 50/73] 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 51/73] 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 57bb85ea86249a40f6cb65987b833285df28ba20 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 31 Oct 2024 09:30:34 -0600 Subject: [PATCH 52/73] updated screenreader for domains & domain-requests search buttons --- src/registrar/templates/includes/domain_requests_table.html | 4 ++-- src/registrar/templates/includes/domains_table.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index 5b7604222..5142131af 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -23,7 +23,7 @@

Domain requests

Reset -
From 8df16346e9676a96e7f933340a7bd4b3d1509e40 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 31 Oct 2024 15:16:54 -0600 Subject: [PATCH 55/73] Fixed error --- src/registrar/models/domain_information.py | 67 +++++++++++++++++++ .../templates/portfolio_members_add_new.html | 4 +- .../templates/portfolio_no_domains.html | 2 +- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 5f98197bd..7dadf26ac 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -425,3 +425,70 @@ def get_state_display_of_domain(self): return self.domain.get_state_display() else: return None + + @property + def converted_organization_name(self): + if self.portfolio: + return self.portfolio.organization_name + return self.organization_name + + # ----- Portfolio Properties ----- + @property + def converted_generic_org_type(self): + if self.portfolio: + return self.portfolio.organization_type + return self.generic_org_type + + @property + def converted_federal_agency(self): + if self.portfolio: + return self.portfolio.federal_agency + return self.federal_agency + + @property + def converted_federal_type(self): + if self.portfolio: + return self.portfolio.federal_type + return self.federal_type + + @property + def converted_senior_official(self): + if self.portfolio: + return self.portfolio.senior_official + return self.senior_official + + @property + def converted_address_line1(self): + if self.portfolio: + return self.portfolio.address_line1 + return self.address_line1 + + @property + def converted_address_line2(self): + if self.portfolio: + return self.portfolio.address_line2 + return self.address_line2 + + @property + def converted_city(self): + if self.portfolio: + return self.portfolio.city + return self.city + + @property + def converted_state_territory(self): + if self.portfolio: + return self.portfolio.state_territory + return self.state_territory + + @property + def converted_zipcode(self): + if self.portfolio: + return self.portfolio.zipcode + return self.zipcode + + @property + def converted_urbanization(self): + if self.portfolio: + return self.portfolio.urbanization + return self.urbanization diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 381a4548a..fe9cb9752 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -20,7 +20,7 @@
diff --git a/src/registrar/templates/portfolio_no_domains.html b/src/registrar/templates/portfolio_no_domains.html index bc42a0e39..75ff3a91f 100644 --- a/src/registrar/templates/portfolio_no_domains.html +++ b/src/registrar/templates/portfolio_no_domains.html @@ -1,4 +1,4 @@ -{% extends 'portfolio_no_domains.html' %} +{% extends 'portfolio_base.html' %} {% load static %} 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 56/73] 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 57/73] 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 58/73] 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 59/73] 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 60/73] 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 61/73] 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 62/73] 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 63/73] 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 64/73] 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) From b66f746aaa56375879c752f620c85b4bf7ceb341 Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Fri, 1 Nov 2024 15:35:20 -0500 Subject: [PATCH 65/73] refactor clone-staging.yaml --- .github/workflows/clone-staging.yaml | 62 +++++++++++----------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index df943f138..5989d3286 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -21,41 +21,27 @@ jobs: CF_USERNAME: CF_MS_USERNAME CF_PASSWORD: CF_MS_PASSWORD steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: 'pip' # caching pip dependencies - - - name: Install CG Tool - run: pip install git+https://github.com/cloud-gov/cg-manage-rds.git - - - name: Share DB Service - uses: cloud-gov/cg-cli-tools@main - with: - cf_username: ${{ secrets[env.CF_USERNAME] }} - cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-dotgov - cf_space: ${{ env.DESTINATION_ENVIRONMENT }} - cf_command: share-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} - - - name: Clone Database - uses: cloud-gov/cg-cli-tools@main - with: - cf_username: ${{ secrets.CF_MS_USERNAME }} - cf_password: ${{ secrets.CF_MS_PASSWORD }} - cf_org: cisa-dotgov - cf_space: ${{ env.SOURCE_ENVIRONMENT }} - command: cg-manage-rds clone getgov-${{ env.SOURCE_ENVIRONMENT }}-database getgov-${{ env.DESTINATION_ENVIRONMENT }}-database - - - name: Unshare DB Service - uses: cloud-gov/cg-cli-tools@main - with: - cf_username: ${{ secrets.CF_MS_USERNAME }} - cf_password: ${{ secrets.CF_MS_PASSWORD }} - cf_org: cisa-dotgov - cf_space: ${{ env.SOURCE_ENVIRONMENT }} - cf_command: unshare-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} + - name: Clone Database + run: | + # install cf cli + wget -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | sudo gpg --dearmor -o /usr/share/keyrings/cli.cloudfoundry.org.gpg + echo "deb [signed-by=/usr/share/keyrings/cli.cloudfoundry.org.gpg] https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list + + sudo apt-get update + sudo apt-get install cf8-cli + # install cg-manage-rds tool + pip install git+https://github.com/cloud-gov/cg-manage-rds.git + + # Authenticate and target CF org and space. + cf api api.fr.cloud.gov + cf auth ${{ secrets[env.CF_USERNAME] }} ${{ secrets[env.CF_PASSWORD] }} + cf target -o cisa-dotgov -s ${{ env.DESTINATION_ENVIRONMENT }} + + # share the target db with the source space + cf share-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} + + # clone from source to destination + cg-manage-rds clone getgov-${{ env.DESTINATION_ENVIRONMENT }}-database getgov-${{ env.SOURCE_ENVIRONMENT }}-database + + # unshare the service + cf unshare-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} From 0f39a731958627583668a5bb889783b525b6df97 Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Fri, 1 Nov 2024 16:03:03 -0500 Subject: [PATCH 66/73] Update clone-staging.yaml --- .github/workflows/clone-staging.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index 5989d3286..703c9bc44 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -23,12 +23,12 @@ jobs: steps: - name: Clone Database run: | - # install cf cli + # install cf cli and other tools wget -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | sudo gpg --dearmor -o /usr/share/keyrings/cli.cloudfoundry.org.gpg echo "deb [signed-by=/usr/share/keyrings/cli.cloudfoundry.org.gpg] https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list sudo apt-get update - sudo apt-get install cf8-cli + sudo apt-get install cf8-cli postgresql-client # install cg-manage-rds tool pip install git+https://github.com/cloud-gov/cg-manage-rds.git From 48eb586b922c84802b6db604abaa9cbd33293ea7 Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Mon, 4 Nov 2024 10:52:49 -0600 Subject: [PATCH 67/73] Update clone-staging.yaml --- .github/workflows/clone-staging.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index 703c9bc44..edfbffa05 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -18,8 +18,8 @@ jobs: clone-database: runs-on: ubuntu-latest env: - CF_USERNAME: CF_MS_USERNAME - CF_PASSWORD: CF_MS_PASSWORD + CF_USERNAME: ${{ secrets[env.CF_USERNAME] }} + CF_PASSWORD: ${{ secrets[env.CF_PASSWORD] }} steps: - name: Clone Database run: | @@ -34,14 +34,14 @@ jobs: # Authenticate and target CF org and space. cf api api.fr.cloud.gov - cf auth ${{ secrets[env.CF_USERNAME] }} ${{ secrets[env.CF_PASSWORD] }} - cf target -o cisa-dotgov -s ${{ env.DESTINATION_ENVIRONMENT }} + cf auth $CF_USERNAME $CF_PASSWORD + cf target -o cisa-dotgov -s $DESTINATION_ENVIRONMENT # share the target db with the source space - cf share-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} + cf share-service getgov-$DESTINATION_ENVIRONMENT-database -s $SOURCE_ENVIRONMENT # clone from source to destination - cg-manage-rds clone getgov-${{ env.DESTINATION_ENVIRONMENT }}-database getgov-${{ env.SOURCE_ENVIRONMENT }}-database + cg-manage-rds clone getgov-$DESTINATION_ENVIRONMENT-database getgov-$SOURCE_ENVIRONMENT-database # unshare the service - cf unshare-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} + cf unshare-service getgov-$DESTINATION_ENVIRONMENT-database -s $SOURCE_ENVIRONMENT From e511df87c51ebffd640d67b50db216f003ce2d3a Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 4 Nov 2024 11:46:18 -0600 Subject: [PATCH 68/73] update clone-staging.yaml --- .github/workflows/clone-staging.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index edfbffa05..a814f68f1 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -18,8 +18,8 @@ jobs: clone-database: runs-on: ubuntu-latest env: - CF_USERNAME: ${{ secrets[env.CF_USERNAME] }} - CF_PASSWORD: ${{ secrets[env.CF_PASSWORD] }} + CF_USERNAME: $CF_MS_USERNAME + CF_PASSWORD: $CF_MS_PASSWORD steps: - name: Clone Database run: | From 724b14ef38df4282c2e38c1d111cf9eedea0baf4 Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Mon, 4 Nov 2024 14:17:54 -0600 Subject: [PATCH 69/73] Update clone-staging.yaml --- .github/workflows/clone-staging.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index a814f68f1..56a60bf49 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -34,7 +34,7 @@ jobs: # Authenticate and target CF org and space. cf api api.fr.cloud.gov - cf auth $CF_USERNAME $CF_PASSWORD + cf auth "$CF_USERNAME" "$CF_PASSWORD" cf target -o cisa-dotgov -s $DESTINATION_ENVIRONMENT # share the target db with the source space From 975d8399485a66b6dc858c66d0eefadffee994ad Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Tue, 5 Nov 2024 10:35:46 -0600 Subject: [PATCH 70/73] Update clone-staging.yaml --- .github/workflows/clone-staging.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index 56a60bf49..eceed3604 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -18,8 +18,8 @@ jobs: clone-database: runs-on: ubuntu-latest env: - CF_USERNAME: $CF_MS_USERNAME - CF_PASSWORD: $CF_MS_PASSWORD + CF_USERNAME: ${{ secrets.CF_MS_USERNAME }} + CF_PASSWORD: ${{ secrets.CF_MS_PASSWORD }} steps: - name: Clone Database run: | From 9757e58f31931e0123620790ace3836f855e634b Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Tue, 5 Nov 2024 11:24:20 -0600 Subject: [PATCH 71/73] Update pg_dump version in cloning job --- .github/workflows/clone-staging.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index eceed3604..9ca590993 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -28,7 +28,7 @@ jobs: echo "deb [signed-by=/usr/share/keyrings/cli.cloudfoundry.org.gpg] https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list sudo apt-get update - sudo apt-get install cf8-cli postgresql-client + sudo apt-get install cf8-cli postgresql-client-common # install cg-manage-rds tool pip install git+https://github.com/cloud-gov/cg-manage-rds.git From 97435a45f7a16683c9686ada4916fbdde9c52360 Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Wed, 6 Nov 2024 11:43:01 -0600 Subject: [PATCH 72/73] Update clone-staging.yaml --- .github/workflows/clone-staging.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index 9ca590993..c9a80f4c8 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -16,7 +16,7 @@ env: jobs: clone-database: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 env: CF_USERNAME: ${{ secrets.CF_MS_USERNAME }} CF_PASSWORD: ${{ secrets.CF_MS_PASSWORD }} @@ -26,9 +26,7 @@ jobs: # install cf cli and other tools wget -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | sudo gpg --dearmor -o /usr/share/keyrings/cli.cloudfoundry.org.gpg echo "deb [signed-by=/usr/share/keyrings/cli.cloudfoundry.org.gpg] https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list - - sudo apt-get update - sudo apt-get install cf8-cli postgresql-client-common + # install cg-manage-rds tool pip install git+https://github.com/cloud-gov/cg-manage-rds.git From 5a709fd1335ccc1d477704ed953dc51f1cc0a0cd Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Wed, 6 Nov 2024 11:59:43 -0600 Subject: [PATCH 73/73] Update clone-staging.yaml --- .github/workflows/clone-staging.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index c9a80f4c8..885f99fec 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -27,6 +27,9 @@ jobs: wget -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | sudo gpg --dearmor -o /usr/share/keyrings/cli.cloudfoundry.org.gpg echo "deb [signed-by=/usr/share/keyrings/cli.cloudfoundry.org.gpg] https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list + sudo apt-get update + sudo apt-get install cf8-cli + # install cg-manage-rds tool pip install git+https://github.com/cloud-gov/cg-manage-rds.git