mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-31 15:06:32 +02:00
Merge branch 'main' into nl/2871-bundle-screenreader
This commit is contained in:
commit
6723b95573
13 changed files with 246 additions and 213 deletions
|
@ -29,6 +29,7 @@
|
||||||
* - tooltip dynamic content updated to include nested element (for better sizing control)
|
* - tooltip dynamic content updated to include nested element (for better sizing control)
|
||||||
* - modal exposed to window to be accessible in other js files
|
* - modal exposed to window to be accessible in other js files
|
||||||
* - fixed bug in createHeaderButton which added newlines to header button tooltips
|
* - fixed bug in createHeaderButton which added newlines to header button tooltips
|
||||||
|
* - modified combobox to handle error class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ("document" in window.self) {
|
if ("document" in window.self) {
|
||||||
|
@ -1213,6 +1214,11 @@ const enhanceComboBox = _comboBoxEl => {
|
||||||
input.setAttribute("class", INPUT_CLASS);
|
input.setAttribute("class", INPUT_CLASS);
|
||||||
input.setAttribute("type", "text");
|
input.setAttribute("type", "text");
|
||||||
input.setAttribute("role", "combobox");
|
input.setAttribute("role", "combobox");
|
||||||
|
// DOTGOV - handle error class for combobox
|
||||||
|
// Check if 'usa-input--error' exists in selectEl and add it to input if true
|
||||||
|
if (selectEl.classList.contains('usa-input--error')) {
|
||||||
|
input.classList.add('usa-input--error');
|
||||||
|
}
|
||||||
additionalAttributes.forEach(attr => Object.keys(attr).forEach(key => {
|
additionalAttributes.forEach(attr => Object.keys(attr).forEach(key => {
|
||||||
const value = Sanitizer.escapeHTML`${attr[key]}`;
|
const value = Sanitizer.escapeHTML`${attr[key]}`;
|
||||||
input.setAttribute(key, value);
|
input.setAttribute(key, value);
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { hideElement, showElement } from './helpers.js';
|
|
||||||
|
|
||||||
export function loadInitialValuesForComboBoxes() {
|
|
||||||
var overrideDefaultClearButton = true;
|
|
||||||
var isTyping = false;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
|
||||||
handleAllComboBoxElements();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleAllComboBoxElements() {
|
|
||||||
const comboBoxElements = document.querySelectorAll(".usa-combo-box");
|
|
||||||
comboBoxElements.forEach(comboBox => {
|
|
||||||
const input = comboBox.querySelector("input");
|
|
||||||
const select = comboBox.querySelector("select");
|
|
||||||
if (!input || !select) {
|
|
||||||
console.warn("No combobox element found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Set the initial value of the combobox
|
|
||||||
let initialValue = select.getAttribute("data-default-value");
|
|
||||||
let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input");
|
|
||||||
if (!clearInputButton) {
|
|
||||||
console.warn("No clear element found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override the default clear button behavior such that it no longer clears the input,
|
|
||||||
// it just resets to the data-initial-value.
|
|
||||||
// Due to the nature of how uswds works, this is slightly hacky.
|
|
||||||
// Use a MutationObserver to watch for changes in the dropdown list
|
|
||||||
const dropdownList = comboBox.querySelector(`#${input.id}--list`);
|
|
||||||
const observer = new MutationObserver(function(mutations) {
|
|
||||||
mutations.forEach(function(mutation) {
|
|
||||||
if (mutation.type === "childList") {
|
|
||||||
addBlankOption(clearInputButton, dropdownList, initialValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure the observer to watch for changes in the dropdown list
|
|
||||||
const config = { childList: true, subtree: true };
|
|
||||||
observer.observe(dropdownList, config);
|
|
||||||
|
|
||||||
// Input event listener to detect typing
|
|
||||||
input.addEventListener("input", () => {
|
|
||||||
isTyping = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Blur event listener to reset typing state
|
|
||||||
input.addEventListener("blur", () => {
|
|
||||||
isTyping = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hide the reset button when there is nothing to reset.
|
|
||||||
// Do this once on init, then everytime a change occurs.
|
|
||||||
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
|
||||||
select.addEventListener("change", () => {
|
|
||||||
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change the default input behaviour - have it reset to the data default instead
|
|
||||||
clearInputButton.addEventListener("click", (e) => {
|
|
||||||
if (overrideDefaultClearButton && initialValue) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
input.click();
|
|
||||||
// Find the dropdown option with the desired value
|
|
||||||
const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option");
|
|
||||||
if (dropdownOptions) {
|
|
||||||
dropdownOptions.forEach(option => {
|
|
||||||
if (option.getAttribute("data-value") === initialValue) {
|
|
||||||
// Simulate a click event on the dropdown option
|
|
||||||
option.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateClearButtonVisibility(select, initialValue, clearInputButton) {
|
|
||||||
if (select.value === initialValue) {
|
|
||||||
hideElement(clearInputButton);
|
|
||||||
}else {
|
|
||||||
showElement(clearInputButton)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addBlankOption(clearInputButton, dropdownList, initialValue) {
|
|
||||||
if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) {
|
|
||||||
const blankOption = document.createElement("li");
|
|
||||||
blankOption.setAttribute("role", "option");
|
|
||||||
blankOption.setAttribute("data-value", "");
|
|
||||||
blankOption.classList.add("usa-combo-box__list-option");
|
|
||||||
if (!initialValue){
|
|
||||||
blankOption.classList.add("usa-combo-box__list-option--selected")
|
|
||||||
}
|
|
||||||
blankOption.textContent = "⎯";
|
|
||||||
|
|
||||||
dropdownList.insertBefore(blankOption, dropdownList.firstChild);
|
|
||||||
blankOption.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
overrideDefaultClearButton = false;
|
|
||||||
// Trigger the default clear behavior
|
|
||||||
clearInputButton.click();
|
|
||||||
overrideDefaultClearButton = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ import { initDomainValidators } from './domain-validators.js';
|
||||||
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
||||||
import { initializeUrbanizationToggle } from './urbanization.js';
|
import { initializeUrbanizationToggle } from './urbanization.js';
|
||||||
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
||||||
import { loadInitialValuesForComboBoxes } from './combobox.js';
|
|
||||||
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
||||||
import { initDomainsTable } from './table-domains.js';
|
import { initDomainsTable } from './table-domains.js';
|
||||||
import { initDomainRequestsTable } from './table-domain-requests.js';
|
import { initDomainRequestsTable } from './table-domain-requests.js';
|
||||||
|
@ -31,8 +30,6 @@ initializeUrbanizationToggle();
|
||||||
userProfileListener();
|
userProfileListener();
|
||||||
finishUserSetupListener();
|
finishUserSetupListener();
|
||||||
|
|
||||||
loadInitialValuesForComboBoxes();
|
|
||||||
|
|
||||||
handleRequestingEntityFieldset();
|
handleRequestingEntityFieldset();
|
||||||
|
|
||||||
initDomainsTable();
|
initDomainsTable();
|
||||||
|
|
|
@ -9,15 +9,15 @@ export function handleRequestingEntityFieldset() {
|
||||||
const formPrefix = "portfolio_requesting_entity";
|
const formPrefix = "portfolio_requesting_entity";
|
||||||
const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`);
|
const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`);
|
||||||
const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`);
|
const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`);
|
||||||
const select = document.getElementById(`id_${formPrefix}-sub_organization`);
|
const input = document.getElementById(`id_${formPrefix}-sub_organization`);
|
||||||
const selectParent = select?.parentElement;
|
const inputGrandParent = input?.parentElement?.parentElement;
|
||||||
|
const select = input?.previousElementSibling;
|
||||||
const suborgContainer = document.getElementById("suborganization-container");
|
const suborgContainer = document.getElementById("suborganization-container");
|
||||||
const suborgDetailsContainer = document.getElementById("suborganization-container__details");
|
const suborgDetailsContainer = document.getElementById("suborganization-container__details");
|
||||||
const suborgAddtlInstruction = document.getElementById("suborganization-addtl-instruction");
|
const suborgAddtlInstruction = document.getElementById("suborganization-addtl-instruction");
|
||||||
const subOrgCreateNewOption = document.getElementById("option-to-add-suborg")?.value;
|
|
||||||
// Make sure all crucial page elements exist before proceeding.
|
// Make sure all crucial page elements exist before proceeding.
|
||||||
// This more or less ensures that we are on the Requesting Entity page, and not elsewhere.
|
// This more or less ensures that we are on the Requesting Entity page, and not elsewhere.
|
||||||
if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return;
|
if (!radios || !input || !select || !inputGrandParent || !suborgContainer || !suborgDetailsContainer) return;
|
||||||
|
|
||||||
// requestingSuborganization: This just broadly determines if they're requesting a suborg at all
|
// 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.
|
// requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not.
|
||||||
|
@ -27,8 +27,8 @@ export function handleRequestingEntityFieldset() {
|
||||||
function toggleSuborganization(radio=null) {
|
function toggleSuborganization(radio=null) {
|
||||||
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
||||||
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
||||||
if (select.options.length == 2) { // --Select-- and other are the only options
|
if (select.options.length == 1) { // other is the only option
|
||||||
hideElement(selectParent); // Hide the select drop down and indicate requesting new suborg
|
hideElement(inputGrandParent); // Hide the combo box and indicate requesting new suborg
|
||||||
hideElement(suborgAddtlInstruction); // Hide additional instruction related to the list
|
hideElement(suborgAddtlInstruction); // Hide additional instruction related to the list
|
||||||
requestingNewSuborganization.value = "True";
|
requestingNewSuborganization.value = "True";
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,11 +37,6 @@ export function handleRequestingEntityFieldset() {
|
||||||
requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer);
|
requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add fake "other" option to sub_organization select
|
|
||||||
if (select && !Array.from(select.options).some(option => option.value === "other")) {
|
|
||||||
select.add(new Option(subOrgCreateNewOption, "other"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestingNewSuborganization.value === "True") {
|
if (requestingNewSuborganization.value === "True") {
|
||||||
select.value = "other";
|
select.value = "other";
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.models import DomainRequest, FederalAgency
|
from registrar.models import DomainRequest, FederalAgency
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
from registrar.models.suborganization import Suborganization
|
from registrar.models.suborganization import Suborganization
|
||||||
|
@ -161,9 +162,10 @@ class DomainSuborganizationForm(forms.ModelForm):
|
||||||
"""Form for updating the suborganization"""
|
"""Form for updating the suborganization"""
|
||||||
|
|
||||||
sub_organization = forms.ModelChoiceField(
|
sub_organization = forms.ModelChoiceField(
|
||||||
|
label="Suborganization name",
|
||||||
queryset=Suborganization.objects.none(),
|
queryset=Suborganization.objects.none(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(),
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -178,20 +180,6 @@ class DomainSuborganizationForm(forms.ModelForm):
|
||||||
portfolio = self.instance.portfolio if self.instance else None
|
portfolio = self.instance.portfolio if self.instance else None
|
||||||
self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=portfolio)
|
self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=portfolio)
|
||||||
|
|
||||||
# Set initial value
|
|
||||||
if self.instance and self.instance.sub_organization:
|
|
||||||
self.fields["sub_organization"].initial = self.instance.sub_organization
|
|
||||||
|
|
||||||
# Set custom form label
|
|
||||||
self.fields["sub_organization"].label = "Suborganization name"
|
|
||||||
|
|
||||||
# Use the combobox rather than the regular select widget
|
|
||||||
self.fields["sub_organization"].widget.template_name = "django/forms/widgets/combobox.html"
|
|
||||||
|
|
||||||
# Set data-default-value attribute
|
|
||||||
if self.instance and self.instance.sub_organization:
|
|
||||||
self.fields["sub_organization"].widget.attrs["data-default-value"] = self.instance.sub_organization.pk
|
|
||||||
|
|
||||||
|
|
||||||
class BaseNameserverFormset(forms.BaseFormSet):
|
class BaseNameserverFormset(forms.BaseFormSet):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -456,6 +444,13 @@ class DomainSecurityEmailForm(forms.Form):
|
||||||
class DomainOrgNameAddressForm(forms.ModelForm):
|
class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
"""Form for updating the organization name and mailing address."""
|
"""Form for updating the organization name and mailing address."""
|
||||||
|
|
||||||
|
# for federal agencies we also want to know the top-level agency.
|
||||||
|
federal_agency = forms.ModelChoiceField(
|
||||||
|
label="Federal agency",
|
||||||
|
required=False,
|
||||||
|
queryset=FederalAgency.objects.all(),
|
||||||
|
widget=ComboboxWidget,
|
||||||
|
)
|
||||||
zipcode = forms.CharField(
|
zipcode = forms.CharField(
|
||||||
label="Zip code",
|
label="Zip code",
|
||||||
validators=[
|
validators=[
|
||||||
|
@ -469,6 +464,16 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
state_territory = forms.ChoiceField(
|
||||||
|
label="State, territory, or military post",
|
||||||
|
required=True,
|
||||||
|
choices=DomainInformation.StateTerritoryChoices.choices,
|
||||||
|
error_messages={
|
||||||
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
|
},
|
||||||
|
widget=ComboboxWidget(attrs={"required": True}),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DomainInformation
|
model = DomainInformation
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -486,25 +491,12 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
"organization_name": {"required": "Enter the name of your organization."},
|
"organization_name": {"required": "Enter the name of your organization."},
|
||||||
"address_line1": {"required": "Enter the street address of your organization."},
|
"address_line1": {"required": "Enter the street address of your organization."},
|
||||||
"city": {"required": "Enter the city where your organization is located."},
|
"city": {"required": "Enter the city where your organization is located."},
|
||||||
"state_territory": {
|
|
||||||
"required": "Select the state, territory, or military post where your organization is located."
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
# We need to set the required attributed for State/territory
|
|
||||||
# because for this fields we are creating an individual
|
|
||||||
# instance of the Select. For the other fields we use the for loop to set
|
|
||||||
# the class's required attribute to true.
|
|
||||||
"organization_name": forms.TextInput,
|
"organization_name": forms.TextInput,
|
||||||
"address_line1": forms.TextInput,
|
"address_line1": forms.TextInput,
|
||||||
"address_line2": forms.TextInput,
|
"address_line2": forms.TextInput,
|
||||||
"city": forms.TextInput,
|
"city": forms.TextInput,
|
||||||
"state_territory": forms.Select(
|
|
||||||
attrs={
|
|
||||||
"required": True,
|
|
||||||
},
|
|
||||||
choices=DomainInformation.StateTerritoryChoices.choices,
|
|
||||||
),
|
|
||||||
"urbanization": forms.TextInput,
|
"urbanization": forms.TextInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django import forms
|
||||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
from django.core.validators import RegexValidator, MaxLengthValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.forms.utility.wizard_form_helper import (
|
from registrar.forms.utility.wizard_form_helper import (
|
||||||
RegistrarForm,
|
RegistrarForm,
|
||||||
RegistrarFormSet,
|
RegistrarFormSet,
|
||||||
|
@ -43,7 +44,7 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
label="Suborganization name",
|
label="Suborganization name",
|
||||||
required=False,
|
required=False,
|
||||||
queryset=Suborganization.objects.none(),
|
queryset=Suborganization.objects.none(),
|
||||||
empty_label="--Select--",
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
requested_suborganization = forms.CharField(
|
requested_suborganization = forms.CharField(
|
||||||
label="Requested suborganization",
|
label="Requested suborganization",
|
||||||
|
@ -56,22 +57,44 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
suborganization_state_territory = forms.ChoiceField(
|
suborganization_state_territory = forms.ChoiceField(
|
||||||
label="State, territory, or military post",
|
label="State, territory, or military post",
|
||||||
required=False,
|
required=False,
|
||||||
choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
|
choices=DomainRequest.StateTerritoryChoices.choices,
|
||||||
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Override of init to add the suborganization queryset"""
|
"""Override of init to add the suborganization queryset and 'other' option"""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.domain_request.portfolio:
|
if self.domain_request.portfolio:
|
||||||
self.fields["sub_organization"].queryset = Suborganization.objects.filter(
|
# Fetch the queryset for the portfolio
|
||||||
portfolio=self.domain_request.portfolio
|
queryset = Suborganization.objects.filter(portfolio=self.domain_request.portfolio)
|
||||||
)
|
# set the queryset appropriately so that post can validate against queryset
|
||||||
|
self.fields["sub_organization"].queryset = queryset
|
||||||
|
|
||||||
|
# Modify the choices to include "other" so that form can display options properly
|
||||||
|
self.fields["sub_organization"].choices = [(obj.id, str(obj)) for obj in queryset] + [
|
||||||
|
("other", "Other (enter your suborganization manually)")
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_database(cls, obj: DomainRequest | Contact | None):
|
||||||
|
"""Returns a dict of form field values gotten from `obj`.
|
||||||
|
Overrides RegistrarForm method in order to set sub_organization to 'other'
|
||||||
|
on GETs of the RequestingEntityForm."""
|
||||||
|
if obj is None:
|
||||||
|
return {}
|
||||||
|
# get the domain request as a dict, per usual method
|
||||||
|
domain_request_dict = {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
||||||
|
|
||||||
|
# set sub_organization to 'other' if is_requesting_new_suborganization is True
|
||||||
|
if isinstance(obj, DomainRequest) and obj.is_requesting_new_suborganization():
|
||||||
|
domain_request_dict["sub_organization"] = "other"
|
||||||
|
|
||||||
|
return domain_request_dict
|
||||||
|
|
||||||
def clean_sub_organization(self):
|
def clean_sub_organization(self):
|
||||||
"""On suborganization clean, set the suborganization value to None if the user is requesting
|
"""On suborganization clean, set the suborganization value to None if the user is requesting
|
||||||
a custom suborganization (as it doesn't exist yet)"""
|
a custom suborganization (as it doesn't exist yet)"""
|
||||||
|
|
||||||
# If it's a new 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"):
|
if self.cleaned_data.get("is_requesting_new_suborganization"):
|
||||||
return None
|
return None
|
||||||
|
@ -94,41 +117,60 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def full_clean(self):
|
def full_clean(self):
|
||||||
"""Validation logic to remove the custom suborganization value before clean is triggered.
|
"""Validation logic to temporarily remove the custom suborganization value before clean is triggered.
|
||||||
Without this override, the form will throw an 'invalid option' error."""
|
Without this override, the form will throw an 'invalid option' error."""
|
||||||
# Remove the custom other field before cleaning
|
# Ensure self.data is not None before proceeding
|
||||||
data = self.data.copy() if self.data else None
|
if self.data:
|
||||||
|
# handle case where form has been submitted
|
||||||
|
# Create a copy of the data for manipulation
|
||||||
|
data = self.data.copy()
|
||||||
|
|
||||||
# Remove the 'other' value from suborganization if it exists.
|
# Retrieve sub_organization and store in _original_suborganization
|
||||||
# This is a special value that tracks if the user is requesting a new suborg.
|
suborganization = data.get("portfolio_requesting_entity-sub_organization")
|
||||||
suborganization = self.data.get("portfolio_requesting_entity-sub_organization")
|
self._original_suborganization = suborganization
|
||||||
if suborganization and "other" in suborganization:
|
# If the original value was "other", clear it for validation
|
||||||
data["portfolio_requesting_entity-sub_organization"] = ""
|
if self._original_suborganization == "other":
|
||||||
|
data["portfolio_requesting_entity-sub_organization"] = ""
|
||||||
|
|
||||||
# Set the modified data back to the form
|
# Set the modified data back to the form
|
||||||
self.data = data
|
self.data = data
|
||||||
|
else:
|
||||||
|
# handle case of a GET
|
||||||
|
suborganization = None
|
||||||
|
if self.initial and "sub_organization" in self.initial:
|
||||||
|
suborganization = self.initial["sub_organization"]
|
||||||
|
|
||||||
|
# Check if is_requesting_new_suborganization is True
|
||||||
|
is_requesting_new_suborganization = False
|
||||||
|
if self.initial and "is_requesting_new_suborganization" in self.initial:
|
||||||
|
# Call the method if it exists
|
||||||
|
is_requesting_new_suborganization = self.initial["is_requesting_new_suborganization"]()
|
||||||
|
|
||||||
|
# Determine if "other" should be set
|
||||||
|
if is_requesting_new_suborganization and suborganization is None:
|
||||||
|
self._original_suborganization = "other"
|
||||||
|
else:
|
||||||
|
self._original_suborganization = suborganization
|
||||||
|
|
||||||
# Call the parent's full_clean method
|
# Call the parent's full_clean method
|
||||||
super().full_clean()
|
super().full_clean()
|
||||||
|
|
||||||
|
# Restore "other" if there are errors
|
||||||
|
if self.errors:
|
||||||
|
self.data["portfolio_requesting_entity-sub_organization"] = self._original_suborganization
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Custom clean implementation to handle our desired logic flow for suborganization.
|
"""Custom clean implementation to handle our desired logic flow for suborganization."""
|
||||||
Given that these fields often rely on eachother, we need to do this in the parent function."""
|
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
# Do some custom error validation if the requesting entity is a suborg.
|
# Get the cleaned data
|
||||||
# Otherwise, just validate as normal.
|
suborganization = cleaned_data.get("sub_organization")
|
||||||
suborganization = self.cleaned_data.get("sub_organization")
|
is_requesting_new_suborganization = cleaned_data.get("is_requesting_new_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(
|
requesting_entity_is_suborganization = self.data.get(
|
||||||
"portfolio_requesting_entity-requesting_entity_is_suborganization"
|
"portfolio_requesting_entity-requesting_entity_is_suborganization"
|
||||||
)
|
)
|
||||||
if requesting_entity_is_suborganization == "True":
|
if requesting_entity_is_suborganization == "True":
|
||||||
if is_requesting_new_suborganization:
|
if is_requesting_new_suborganization:
|
||||||
# Validate custom suborganization fields
|
|
||||||
if not cleaned_data.get("requested_suborganization") and "requested_suborganization" not in self.errors:
|
if not cleaned_data.get("requested_suborganization") and "requested_suborganization" not in self.errors:
|
||||||
self.add_error("requested_suborganization", "Enter the name of your suborganization.")
|
self.add_error("requested_suborganization", "Enter the name of your suborganization.")
|
||||||
if not cleaned_data.get("suborganization_city"):
|
if not cleaned_data.get("suborganization_city"):
|
||||||
|
@ -141,6 +183,12 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
elif not suborganization:
|
elif not suborganization:
|
||||||
self.add_error("sub_organization", "Suborganization is required.")
|
self.add_error("sub_organization", "Suborganization is required.")
|
||||||
|
|
||||||
|
# If there are errors, restore the "other" value for rendering
|
||||||
|
if self.errors and getattr(self, "_original_suborganization", None) == "other":
|
||||||
|
self.cleaned_data["sub_organization"] = self._original_suborganization
|
||||||
|
elif not self.data and getattr(self, "_original_suborganization", None) == "other":
|
||||||
|
self.cleaned_data["sub_organization"] = self._original_suborganization
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -274,7 +322,7 @@ class OrganizationContactForm(RegistrarForm):
|
||||||
# uncomment to see if modelChoiceField can be an arg later
|
# uncomment to see if modelChoiceField can be an arg later
|
||||||
required=False,
|
required=False,
|
||||||
queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
|
queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
|
||||||
empty_label="--Select--",
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
organization_name = forms.CharField(
|
organization_name = forms.CharField(
|
||||||
label="Organization name",
|
label="Organization name",
|
||||||
|
@ -294,10 +342,11 @@ class OrganizationContactForm(RegistrarForm):
|
||||||
)
|
)
|
||||||
state_territory = forms.ChoiceField(
|
state_territory = forms.ChoiceField(
|
||||||
label="State, territory, or military post",
|
label="State, territory, or military post",
|
||||||
choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
|
choices=DomainRequest.StateTerritoryChoices.choices,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": ("Select the state, territory, or military post where your organization is located.")
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
},
|
},
|
||||||
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
zipcode = forms.CharField(
|
zipcode = forms.CharField(
|
||||||
label="Zip code",
|
label="Zip code",
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.core.validators import RegexValidator
|
||||||
from django.core.validators import MaxLengthValidator
|
from django.core.validators import MaxLengthValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
PortfolioInvitation,
|
PortfolioInvitation,
|
||||||
UserPortfolioPermission,
|
UserPortfolioPermission,
|
||||||
|
@ -33,6 +34,15 @@ class PortfolioOrgAddressForm(forms.ModelForm):
|
||||||
"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789.",
|
"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
state_territory = forms.ChoiceField(
|
||||||
|
label="State, territory, or military post",
|
||||||
|
required=True,
|
||||||
|
choices=DomainInformation.StateTerritoryChoices.choices,
|
||||||
|
error_messages={
|
||||||
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
|
},
|
||||||
|
widget=ComboboxWidget(attrs={"required": True}),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Portfolio
|
model = Portfolio
|
||||||
|
@ -47,25 +57,12 @@ class PortfolioOrgAddressForm(forms.ModelForm):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
"address_line1": {"required": "Enter the street address of your organization."},
|
"address_line1": {"required": "Enter the street address of your organization."},
|
||||||
"city": {"required": "Enter the city where your organization is located."},
|
"city": {"required": "Enter the city where your organization is located."},
|
||||||
"state_territory": {
|
|
||||||
"required": "Select the state, territory, or military post where your organization is located."
|
|
||||||
},
|
|
||||||
"zipcode": {"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."},
|
"zipcode": {"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."},
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
# We need to set the required attributed for State/territory
|
|
||||||
# because for this fields we are creating an individual
|
|
||||||
# instance of the Select. For the other fields we use the for loop to set
|
|
||||||
# the class's required attribute to true.
|
|
||||||
"address_line1": forms.TextInput,
|
"address_line1": forms.TextInput,
|
||||||
"address_line2": forms.TextInput,
|
"address_line2": forms.TextInput,
|
||||||
"city": forms.TextInput,
|
"city": forms.TextInput,
|
||||||
"state_territory": forms.Select(
|
|
||||||
attrs={
|
|
||||||
"required": True,
|
|
||||||
},
|
|
||||||
choices=DomainInformation.StateTerritoryChoices.choices,
|
|
||||||
),
|
|
||||||
# "urbanization": forms.TextInput,
|
# "urbanization": forms.TextInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
src/registrar/forms/utility/combobox.py
Normal file
5
src/registrar/forms/utility/combobox.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.forms import Select
|
||||||
|
|
||||||
|
|
||||||
|
class ComboboxWidget(Select):
|
||||||
|
template_name = "django/forms/widgets/combobox.html"
|
|
@ -4,9 +4,9 @@ import ipaddress
|
||||||
import re
|
import re
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from django.db import transaction
|
||||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||||
|
from django.db import models, IntegrityError
|
||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from registrar.models.host import Host
|
from registrar.models.host import Host
|
||||||
|
@ -1329,14 +1329,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
def get_default_administrative_contact(self):
|
def get_default_administrative_contact(self):
|
||||||
"""Gets the default administrative contact."""
|
"""Gets the default administrative contact."""
|
||||||
logger.info("get_default_security_contact() -> Adding administrative security contact")
|
logger.info("get_default_administrative_contact() -> Adding default administrative contact")
|
||||||
contact = PublicContact.get_default_administrative()
|
contact = PublicContact.get_default_administrative()
|
||||||
contact.domain = self
|
contact.domain = self
|
||||||
return contact
|
return contact
|
||||||
|
|
||||||
def get_default_technical_contact(self):
|
def get_default_technical_contact(self):
|
||||||
"""Gets the default technical contact."""
|
"""Gets the default technical contact."""
|
||||||
logger.info("get_default_security_contact() -> Adding technical security contact")
|
logger.info("get_default_security_contact() -> Adding default technical contact")
|
||||||
contact = PublicContact.get_default_technical()
|
contact = PublicContact.get_default_technical()
|
||||||
contact.domain = self
|
contact.domain = self
|
||||||
return contact
|
return contact
|
||||||
|
@ -1678,9 +1678,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
for domainContact in contact_data:
|
for domainContact in contact_data:
|
||||||
req = commands.InfoContact(id=domainContact.contact)
|
req = commands.InfoContact(id=domainContact.contact)
|
||||||
data = registry.send(req, cleaned=True).res_data[0]
|
data = registry.send(req, cleaned=True).res_data[0]
|
||||||
|
logger.info(f"_fetch_contacts => this is the data: {data}")
|
||||||
|
|
||||||
# Map the object we recieved from EPP to a PublicContact
|
# Map the object we recieved from EPP to a PublicContact
|
||||||
mapped_object = self.map_epp_contact_to_public_contact(data, domainContact.contact, domainContact.type)
|
mapped_object = self.map_epp_contact_to_public_contact(data, domainContact.contact, domainContact.type)
|
||||||
|
logger.info(f"_fetch_contacts => mapped_object: {mapped_object}")
|
||||||
|
|
||||||
# Find/create it in the DB
|
# Find/create it in the DB
|
||||||
in_db = self._get_or_create_public_contact(mapped_object)
|
in_db = self._get_or_create_public_contact(mapped_object)
|
||||||
|
@ -1871,8 +1873,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
missingSecurity = True
|
missingSecurity = True
|
||||||
missingTech = True
|
missingTech = True
|
||||||
|
|
||||||
if len(cleaned.get("_contacts")) < 3:
|
contacts = cleaned.get("_contacts", [])
|
||||||
for contact in cleaned.get("_contacts"):
|
if len(contacts) < 3:
|
||||||
|
for contact in contacts:
|
||||||
if contact.type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
if contact.type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
||||||
missingAdmin = False
|
missingAdmin = False
|
||||||
if contact.type == PublicContact.ContactTypeChoices.SECURITY:
|
if contact.type == PublicContact.ContactTypeChoices.SECURITY:
|
||||||
|
@ -1891,6 +1894,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
technical_contact = self.get_default_technical_contact()
|
technical_contact = self.get_default_technical_contact()
|
||||||
technical_contact.save()
|
technical_contact.save()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"_add_missing_contacts_if_unknown => Adding contacts. Values are "
|
||||||
|
f"missingAdmin: {missingAdmin}, missingSecurity: {missingSecurity}, missingTech: {missingTech}"
|
||||||
|
)
|
||||||
|
|
||||||
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
|
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
|
||||||
"""Contact registry for info about a domain."""
|
"""Contact registry for info about a domain."""
|
||||||
try:
|
try:
|
||||||
|
@ -2104,8 +2112,21 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
# Save to DB if it doesn't exist already.
|
# Save to DB if it doesn't exist already.
|
||||||
if db_contact.count() == 0:
|
if db_contact.count() == 0:
|
||||||
# Doesn't run custom save logic, just saves to DB
|
# Doesn't run custom save logic, just saves to DB
|
||||||
public_contact.save(skip_epp_save=True)
|
try:
|
||||||
logger.info(f"Created a new PublicContact: {public_contact}")
|
with transaction.atomic():
|
||||||
|
public_contact.save(skip_epp_save=True)
|
||||||
|
logger.info(f"Created a new PublicContact: {public_contact}")
|
||||||
|
except IntegrityError as err:
|
||||||
|
logger.error(
|
||||||
|
f"_get_or_create_public_contact() => tried to create a duplicate public contact: {err}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return PublicContact.objects.get(
|
||||||
|
registry_id=public_contact.registry_id,
|
||||||
|
contact_type=public_contact.contact_type,
|
||||||
|
domain=self,
|
||||||
|
)
|
||||||
|
|
||||||
# Append the item we just created
|
# Append the item we just created
|
||||||
return public_contact
|
return public_contact
|
||||||
|
|
||||||
|
@ -2115,7 +2136,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
if existing_contact.email != public_contact.email or existing_contact.registry_id != public_contact.registry_id:
|
if existing_contact.email != public_contact.email or existing_contact.registry_id != public_contact.registry_id:
|
||||||
existing_contact.delete()
|
existing_contact.delete()
|
||||||
public_contact.save()
|
public_contact.save()
|
||||||
logger.warning("Requested PublicContact is out of sync " "with DB.")
|
logger.warning("Requested PublicContact is out of sync with DB.")
|
||||||
return public_contact
|
return public_contact
|
||||||
|
|
||||||
# If it already exists, we can assume that the DB instance was updated during set, so we should just use that.
|
# If it already exists, we can assume that the DB instance was updated during set, so we should just use that.
|
||||||
|
|
|
@ -11,6 +11,7 @@ for now we just carry the attribute to both the parent element and the select.
|
||||||
{{ name }}="{{ value }}"
|
{{ name }}="{{ value }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
data-default-value="{% for group_name, group_choices, group_index in widget.optgroups %}{% for option in group_choices %}{% if option.selected %}{{ option.value }}{% endif %}{% endfor %}{% endfor %}"
|
||||||
>
|
>
|
||||||
{% include "django/forms/widgets/select.html" %}
|
{% include "django/forms/widgets/select.html" with is_combobox=True %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
{# hint: spacing in the class string matters #}
|
{# hint: spacing in the class string matters #}
|
||||||
class="usa-select{% if classes %} {{ classes }}{% endif %}"
|
class="usa-select{% if classes %} {{ classes }}{% endif %}"
|
||||||
{% include "django/forms/widgets/attrs.html" %}
|
{% include "django/forms/widgets/attrs.html" %}
|
||||||
|
{% if is_combobox %}
|
||||||
|
data-default-value="{% for group_name, group_choices, group_index in widget.optgroups %}{% for option in group_choices %}{% if option.selected %}{{ option.value }}{% endif %}{% endfor %}{% endfor %}"
|
||||||
|
{% endif %}
|
||||||
>
|
>
|
||||||
{% for group, options, index in widget.optgroups %}
|
{% for group, options, index in widget.optgroups %}
|
||||||
{% if group %}<optgroup label="{{ group }}">{% endif %}
|
{% if group %}<optgroup label="{{ group }}">{% endif %}
|
||||||
|
|
|
@ -349,6 +349,70 @@ class TestDomainCache(MockEppLib):
|
||||||
class TestDomainCreation(MockEppLib):
|
class TestDomainCreation(MockEppLib):
|
||||||
"""Rule: An approved domain request must result in a domain"""
|
"""Rule: An approved domain request must result in a domain"""
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_or_create_public_contact_race_condition(self):
|
||||||
|
"""
|
||||||
|
Scenario: Two processes try to create the same security contact simultaneously
|
||||||
|
Given a domain in UNKNOWN state
|
||||||
|
When a race condition occurs during contact creation
|
||||||
|
Then no IntegrityError is raised
|
||||||
|
And only one security contact exists in database
|
||||||
|
And the correct public contact is returned
|
||||||
|
|
||||||
|
CONTEXT: We ran into an intermittent but somewhat rare issue where IntegrityError
|
||||||
|
was raised when creating PublicContact.
|
||||||
|
Per our logs, this seemed to appear during periods of high app activity.
|
||||||
|
"""
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov")
|
||||||
|
|
||||||
|
self.first_call = True
|
||||||
|
|
||||||
|
def mock_filter(*args, **kwargs):
|
||||||
|
"""Simulates a race condition by creating a
|
||||||
|
duplicate contact between the first filter and save.
|
||||||
|
"""
|
||||||
|
# Return an empty queryset for the first call. Otherwise just proceed as normal.
|
||||||
|
if self.first_call:
|
||||||
|
self.first_call = False
|
||||||
|
duplicate = PublicContact(
|
||||||
|
domain=domain,
|
||||||
|
contact_type=PublicContact.ContactTypeChoices.SECURITY,
|
||||||
|
registry_id="defaultSec",
|
||||||
|
email="dotgov@cisa.dhs.gov",
|
||||||
|
name="Registry Customer Service",
|
||||||
|
)
|
||||||
|
duplicate.save(skip_epp_save=True)
|
||||||
|
return PublicContact.objects.none()
|
||||||
|
|
||||||
|
return PublicContact.objects.filter(*args, **kwargs)
|
||||||
|
|
||||||
|
with patch.object(PublicContact.objects, "filter", side_effect=mock_filter):
|
||||||
|
try:
|
||||||
|
public_contact = PublicContact(
|
||||||
|
domain=domain,
|
||||||
|
contact_type=PublicContact.ContactTypeChoices.SECURITY,
|
||||||
|
registry_id="defaultSec",
|
||||||
|
email="dotgov@cisa.dhs.gov",
|
||||||
|
name="Registry Customer Service",
|
||||||
|
)
|
||||||
|
returned_public_contact = domain._get_or_create_public_contact(public_contact)
|
||||||
|
except IntegrityError:
|
||||||
|
self.fail(
|
||||||
|
"IntegrityError was raised during contact creation due to a race condition. "
|
||||||
|
"This indicates that concurrent contact creation is not working in some cases. "
|
||||||
|
"The error occurs when two processes try to create the same contact simultaneously. "
|
||||||
|
"Expected behavior: gracefully handle duplicate creation and return existing contact."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify that only one contact exists and its correctness
|
||||||
|
security_contacts = PublicContact.objects.filter(
|
||||||
|
domain=domain, contact_type=PublicContact.ContactTypeChoices.SECURITY
|
||||||
|
)
|
||||||
|
self.assertEqual(security_contacts.count(), 1)
|
||||||
|
self.assertEqual(returned_public_contact, security_contacts.get())
|
||||||
|
self.assertEqual(returned_public_contact.registry_id, "defaultSec")
|
||||||
|
self.assertEqual(returned_public_contact.email, "dotgov@cisa.dhs.gov")
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_approved_domain_request_creates_domain_locally(self):
|
def test_approved_domain_request_creates_domain_locally(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2879,7 +2879,7 @@ class TestRequestingEntity(WebTest):
|
||||||
|
|
||||||
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||||
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
||||||
form["portfolio_requesting_entity-sub_organization"] = ""
|
form["portfolio_requesting_entity-sub_organization"] = "other"
|
||||||
|
|
||||||
form["portfolio_requesting_entity-requested_suborganization"] = "moon"
|
form["portfolio_requesting_entity-requested_suborganization"] = "moon"
|
||||||
form["portfolio_requesting_entity-suborganization_city"] = "kepler"
|
form["portfolio_requesting_entity-suborganization_city"] = "kepler"
|
||||||
|
@ -2942,18 +2942,34 @@ class TestRequestingEntity(WebTest):
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# For 2 the tests below, it is required to submit a form without submitting a value
|
||||||
|
# for the select/combobox. WebTest will not do this; by default, WebTest will submit
|
||||||
|
# the first choice in a select. So, need to manipulate the form to remove the
|
||||||
|
# particular select/combobox that will not be submitted, and then post the form.
|
||||||
|
form_action = f"/request/{domain_request.pk}/portfolio_requesting_entity/"
|
||||||
|
|
||||||
# Test missing suborganization selection
|
# Test missing suborganization selection
|
||||||
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||||
form["portfolio_requesting_entity-sub_organization"] = ""
|
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = False
|
||||||
|
# remove sub_organization from the form submission
|
||||||
response = form.submit()
|
form_data = form.submit_fields()
|
||||||
|
form_data = [(key, value) for key, value in form_data if key != "portfolio_requesting_entity-sub_organization"]
|
||||||
|
response = self.app.post(form_action, dict(form_data))
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
self.assertContains(response, "Suborganization is required.", status_code=200)
|
self.assertContains(response, "Suborganization is required.", status_code=200)
|
||||||
|
|
||||||
# Test missing custom suborganization details
|
# Test missing custom suborganization details
|
||||||
|
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||||
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
||||||
response = form.submit()
|
form["portfolio_requesting_entity-sub_organization"] = "other"
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
# remove suborganization_state_territory from the form submission
|
||||||
|
form_data = form.submit_fields()
|
||||||
|
form_data = [
|
||||||
|
(key, value)
|
||||||
|
for key, value in form_data
|
||||||
|
if key != "portfolio_requesting_entity-suborganization_state_territory"
|
||||||
|
]
|
||||||
|
response = self.app.post(form_action, dict(form_data))
|
||||||
self.assertContains(response, "Enter the name of your suborganization.", status_code=200)
|
self.assertContains(response, "Enter the name of your suborganization.", status_code=200)
|
||||||
self.assertContains(response, "Enter the city where your suborganization is located.", status_code=200)
|
self.assertContains(response, "Enter the city where your suborganization is located.", status_code=200)
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue