diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 1b6e2de6a..e89147b11 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1367,6 +1367,8 @@ class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
autocomplete_fields = ["user", "domain"]
+ change_form_template = "django/admin/user_domain_role_change_form.html"
+
# Fixes a bug where non-superusers are redirected to the main page
def delete_view(self, request, object_id, extra_context=None):
"""Custom delete_view implementation that specifies redirect behaviour"""
@@ -1500,7 +1502,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
autocomplete_fields = ["domain"]
- change_form_template = "django/admin/email_clipboard_change_form.html"
+ change_form_template = "django/admin/domain_invitation_change_form.html"
# Select domain invitations to change -> Domain invitations
def changelist_view(self, request, extra_context=None):
diff --git a/src/registrar/assets/js/uswds-edited.js b/src/registrar/assets/js/uswds-edited.js
index f59417b41..9d4dd2e51 100644
--- a/src/registrar/assets/js/uswds-edited.js
+++ b/src/registrar/assets/js/uswds-edited.js
@@ -29,6 +29,7 @@
* - tooltip dynamic content updated to include nested element (for better sizing control)
* - modal exposed to window to be accessible in other js files
* - fixed bug in createHeaderButton which added newlines to header button tooltips
+ * - modified combobox to handle error class
*/
if ("document" in window.self) {
@@ -1213,6 +1214,11 @@ const enhanceComboBox = _comboBoxEl => {
input.setAttribute("class", INPUT_CLASS);
input.setAttribute("type", "text");
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 => {
const value = Sanitizer.escapeHTML`${attr[key]}`;
input.setAttribute(key, value);
diff --git a/src/registrar/assets/src/js/getgov/combobox.js b/src/registrar/assets/src/js/getgov/combobox.js
deleted file mode 100644
index 36b7aa0ad..000000000
--- a/src/registrar/assets/src/js/getgov/combobox.js
+++ /dev/null
@@ -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;
- });
- }
- }
-}
diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js
index 6ff402aa4..a077da929 100644
--- a/src/registrar/assets/src/js/getgov/main.js
+++ b/src/registrar/assets/src/js/getgov/main.js
@@ -3,7 +3,6 @@ import { initDomainValidators } from './domain-validators.js';
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
import { initializeUrbanizationToggle } from './urbanization.js';
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
-import { loadInitialValuesForComboBoxes } from './combobox.js';
import { handleRequestingEntityFieldset } from './requesting-entity.js';
import { initDomainsTable } from './table-domains.js';
import { initDomainRequestsTable } from './table-domain-requests.js';
@@ -31,8 +30,6 @@ initializeUrbanizationToggle();
userProfileListener();
finishUserSetupListener();
-loadInitialValuesForComboBoxes();
-
handleRequestingEntityFieldset();
initDomainsTable();
diff --git a/src/registrar/assets/src/js/getgov/requesting-entity.js b/src/registrar/assets/src/js/getgov/requesting-entity.js
index 3bcdcd35c..833eab2f8 100644
--- a/src/registrar/assets/src/js/getgov/requesting-entity.js
+++ b/src/registrar/assets/src/js/getgov/requesting-entity.js
@@ -9,15 +9,15 @@ export function handleRequestingEntityFieldset() {
const formPrefix = "portfolio_requesting_entity";
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 selectParent = select?.parentElement;
+ const input = document.getElementById(`id_${formPrefix}-sub_organization`);
+ const inputGrandParent = input?.parentElement?.parentElement;
+ const select = input?.previousElementSibling;
const suborgContainer = document.getElementById("suborganization-container");
const suborgDetailsContainer = document.getElementById("suborganization-container__details");
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.
// 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
// 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) {
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
- if (select.options.length == 2) { // --Select-- and other are the only options
- hideElement(selectParent); // Hide the select drop down and indicate requesting new suborg
+ if (select.options.length == 1) { // other is the only option
+ hideElement(inputGrandParent); // Hide the combo box and indicate requesting new suborg
hideElement(suborgAddtlInstruction); // Hide additional instruction related to the list
requestingNewSuborganization.value = "True";
} else {
@@ -37,11 +37,6 @@ export function handleRequestingEntityFieldset() {
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") {
select.value = "other";
}
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 699efe63b..05eb90db3 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -4,6 +4,7 @@ import logging
from django import forms
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
from django.forms import formset_factory
+from registrar.forms.utility.combobox import ComboboxWidget
from registrar.models import DomainRequest, FederalAgency
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from registrar.models.suborganization import Suborganization
@@ -161,9 +162,10 @@ class DomainSuborganizationForm(forms.ModelForm):
"""Form for updating the suborganization"""
sub_organization = forms.ModelChoiceField(
+ label="Suborganization name",
queryset=Suborganization.objects.none(),
required=False,
- widget=forms.Select(),
+ widget=ComboboxWidget,
)
class Meta:
@@ -178,20 +180,6 @@ class DomainSuborganizationForm(forms.ModelForm):
portfolio = self.instance.portfolio if self.instance else None
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):
def clean(self):
@@ -456,6 +444,13 @@ class DomainSecurityEmailForm(forms.Form):
class DomainOrgNameAddressForm(forms.ModelForm):
"""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(
label="Zip code",
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:
model = DomainInformation
fields = [
@@ -486,25 +491,12 @@ class DomainOrgNameAddressForm(forms.ModelForm):
"organization_name": {"required": "Enter the name of your organization."},
"address_line1": {"required": "Enter the street address of your organization."},
"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 = {
- # 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,
"address_line1": forms.TextInput,
"address_line2": forms.TextInput,
"city": forms.TextInput,
- "state_territory": forms.Select(
- attrs={
- "required": True,
- },
- choices=DomainInformation.StateTerritoryChoices.choices,
- ),
"urbanization": forms.TextInput,
}
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index ccdbb17ba..7c9dcb180 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -7,6 +7,7 @@ from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator
from django.utils.safestring import mark_safe
+from registrar.forms.utility.combobox import ComboboxWidget
from registrar.forms.utility.wizard_form_helper import (
RegistrarForm,
RegistrarFormSet,
@@ -43,7 +44,7 @@ class RequestingEntityForm(RegistrarForm):
label="Suborganization name",
required=False,
queryset=Suborganization.objects.none(),
- empty_label="--Select--",
+ widget=ComboboxWidget,
)
requested_suborganization = forms.CharField(
label="Requested suborganization",
@@ -56,22 +57,44 @@ class RequestingEntityForm(RegistrarForm):
suborganization_state_territory = forms.ChoiceField(
label="State, territory, or military post",
required=False,
- choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
+ choices=DomainRequest.StateTerritoryChoices.choices,
+ widget=ComboboxWidget,
)
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)
if self.domain_request.portfolio:
- self.fields["sub_organization"].queryset = Suborganization.objects.filter(
- portfolio=self.domain_request.portfolio
- )
+ # Fetch the queryset for the 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):
"""On suborganization clean, set the suborganization value to None if the user is requesting
a custom suborganization (as it doesn't exist yet)"""
-
# If it's a new suborganization, return None (equivalent to selecting nothing)
if self.cleaned_data.get("is_requesting_new_suborganization"):
return None
@@ -94,41 +117,60 @@ class RequestingEntityForm(RegistrarForm):
return name
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."""
- # Remove the custom other field before cleaning
- data = self.data.copy() if self.data else None
+ # Ensure self.data is not None before proceeding
+ 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.
- # 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 and "other" in suborganization:
- data["portfolio_requesting_entity-sub_organization"] = ""
+ # Retrieve sub_organization and store in _original_suborganization
+ suborganization = data.get("portfolio_requesting_entity-sub_organization")
+ self._original_suborganization = suborganization
+ # If the original value was "other", clear it for validation
+ if self._original_suborganization == "other":
+ data["portfolio_requesting_entity-sub_organization"] = ""
- # Set the modified data back to the form
- self.data = data
+ # Set the modified data back to the form
+ 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
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):
- """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."""
+ """Custom clean implementation to handle our desired logic flow for suborganization."""
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_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.
+ # Get the cleaned data
+ suborganization = cleaned_data.get("sub_organization")
+ is_requesting_new_suborganization = cleaned_data.get("is_requesting_new_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
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.")
if not cleaned_data.get("suborganization_city"):
@@ -141,6 +183,12 @@ class RequestingEntityForm(RegistrarForm):
elif not suborganization:
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
@@ -274,7 +322,7 @@ class OrganizationContactForm(RegistrarForm):
# uncomment to see if modelChoiceField can be an arg later
required=False,
queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
- empty_label="--Select--",
+ widget=ComboboxWidget,
)
organization_name = forms.CharField(
label="Organization name",
@@ -294,10 +342,11 @@ class OrganizationContactForm(RegistrarForm):
)
state_territory = forms.ChoiceField(
label="State, territory, or military post",
- choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
+ choices=DomainRequest.StateTerritoryChoices.choices,
error_messages={
"required": ("Select the state, territory, or military post where your organization is located.")
},
+ widget=ComboboxWidget,
)
zipcode = forms.CharField(
label="Zip code",
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 0a8c4d623..e57b56c4f 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -6,6 +6,7 @@ from django.core.validators import RegexValidator
from django.core.validators import MaxLengthValidator
from django.utils.safestring import mark_safe
+from registrar.forms.utility.combobox import ComboboxWidget
from registrar.models import (
PortfolioInvitation,
UserPortfolioPermission,
@@ -33,6 +34,15 @@ class PortfolioOrgAddressForm(forms.ModelForm):
"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:
model = Portfolio
@@ -47,25 +57,12 @@ class PortfolioOrgAddressForm(forms.ModelForm):
error_messages = {
"address_line1": {"required": "Enter the street address of your organization."},
"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."},
}
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_line2": forms.TextInput,
"city": forms.TextInput,
- "state_territory": forms.Select(
- attrs={
- "required": True,
- },
- choices=DomainInformation.StateTerritoryChoices.choices,
- ),
# "urbanization": forms.TextInput,
}
diff --git a/src/registrar/forms/utility/combobox.py b/src/registrar/forms/utility/combobox.py
new file mode 100644
index 000000000..277aec4f3
--- /dev/null
+++ b/src/registrar/forms/utility/combobox.py
@@ -0,0 +1,5 @@
+from django.forms import Select
+
+
+class ComboboxWidget(Select):
+ template_name = "django/forms/widgets/combobox.html"
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 6bd8278a1..cb481db7a 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -4,9 +4,9 @@ import ipaddress
import re
from datetime import date, timedelta
from typing import Optional
+from django.db import transaction
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
-
-from django.db import models
+from django.db import models, IntegrityError
from django.utils import timezone
from typing import Any
from registrar.models.host import Host
@@ -1329,14 +1329,14 @@ class Domain(TimeStampedModel, DomainHelper):
def get_default_administrative_contact(self):
"""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.domain = self
return contact
def get_default_technical_contact(self):
"""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.domain = self
return contact
@@ -1678,9 +1678,11 @@ class Domain(TimeStampedModel, DomainHelper):
for domainContact in contact_data:
req = commands.InfoContact(id=domainContact.contact)
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
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
in_db = self._get_or_create_public_contact(mapped_object)
@@ -1871,8 +1873,9 @@ class Domain(TimeStampedModel, DomainHelper):
missingSecurity = True
missingTech = True
- if len(cleaned.get("_contacts")) < 3:
- for contact in cleaned.get("_contacts"):
+ contacts = cleaned.get("_contacts", [])
+ if len(contacts) < 3:
+ for contact in contacts:
if contact.type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
missingAdmin = False
if contact.type == PublicContact.ContactTypeChoices.SECURITY:
@@ -1891,6 +1894,11 @@ class Domain(TimeStampedModel, DomainHelper):
technical_contact = self.get_default_technical_contact()
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):
"""Contact registry for info about a domain."""
try:
@@ -2104,8 +2112,21 @@ class Domain(TimeStampedModel, DomainHelper):
# Save to DB if it doesn't exist already.
if db_contact.count() == 0:
# Doesn't run custom save logic, just saves to DB
- public_contact.save(skip_epp_save=True)
- logger.info(f"Created a new PublicContact: {public_contact}")
+ try:
+ 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
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:
existing_contact.delete()
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
# If it already exists, we can assume that the DB instance was updated during set, so we should just use that.
diff --git a/src/registrar/templates/django/admin/domain_invitation_change_form.html b/src/registrar/templates/django/admin/domain_invitation_change_form.html
new file mode 100644
index 000000000..6ce6ed0d1
--- /dev/null
+++ b/src/registrar/templates/django/admin/domain_invitation_change_form.html
@@ -0,0 +1,14 @@
+{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% load custom_filters %}
+{% load i18n static %}
+
+{% block content_subtitle %}
+
+
+
+ If you add someone to a domain here, it will trigger emails to the invitee and all managers of the domain when you click "save." If you don't want to trigger those emails, use the User domain roles permissions table instead.
+
+
+
+ {{ block.super }}
+{% endblock %}
diff --git a/src/registrar/templates/django/admin/user_domain_role_change_form.html b/src/registrar/templates/django/admin/user_domain_role_change_form.html
new file mode 100644
index 000000000..d8c298bc1
--- /dev/null
+++ b/src/registrar/templates/django/admin/user_domain_role_change_form.html
@@ -0,0 +1,14 @@
+{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% load custom_filters %}
+{% load i18n static %}
+
+{% block content_subtitle %}
+
+ {{ block.super }}
+{% endblock %}
diff --git a/src/registrar/templates/django/forms/widgets/combobox.html b/src/registrar/templates/django/forms/widgets/combobox.html
index 7ff31945b..02cd4e35e 100644
--- a/src/registrar/templates/django/forms/widgets/combobox.html
+++ b/src/registrar/templates/django/forms/widgets/combobox.html
@@ -11,6 +11,7 @@ for now we just carry the attribute to both the parent element and the select.
{{ name }}="{{ value }}"
{% endif %}
{% 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 %}
diff --git a/src/registrar/templates/django/forms/widgets/select.html b/src/registrar/templates/django/forms/widgets/select.html
index cc62eb91d..db6deafe2 100644
--- a/src/registrar/templates/django/forms/widgets/select.html
+++ b/src/registrar/templates/django/forms/widgets/select.html
@@ -3,6 +3,9 @@
{# hint: spacing in the class string matters #}
class="usa-select{% if classes %} {{ classes }}{% endif %}"
{% 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 %}
{% if group %}