Merge branch 'main' into ms/2823-update-delete-domain-process

This commit is contained in:
Matt-Spence 2024-12-16 13:59:57 -05:00 committed by GitHub
commit 8016dd96c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 505 additions and 249 deletions

View file

@ -220,6 +220,14 @@ class DomainInformationAdminForm(forms.ModelForm):
fields = "__all__"
widgets = {
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
"portfolio": AutocompleteSelectWithPlaceholder(
DomainInformation._meta.get_field("portfolio"), admin.site, attrs={"data-placeholder": "---------"}
),
"sub_organization": AutocompleteSelectWithPlaceholder(
DomainInformation._meta.get_field("sub_organization"),
admin.site,
attrs={"data-placeholder": "---------", "ajax-url": "get-suborganization-list-json"},
),
}
@ -231,6 +239,14 @@ class DomainInformationInlineForm(forms.ModelForm):
fields = "__all__"
widgets = {
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
"portfolio": AutocompleteSelectWithPlaceholder(
DomainInformation._meta.get_field("portfolio"), admin.site, attrs={"data-placeholder": "---------"}
),
"sub_organization": AutocompleteSelectWithPlaceholder(
DomainInformation._meta.get_field("sub_organization"),
admin.site,
attrs={"data-placeholder": "---------", "ajax-url": "get-suborganization-list-json"},
),
}
@ -1523,6 +1539,70 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
orderable_fk_fields = [("domain", "name")]
# Define methods to display fields from the related portfolio
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
portfolio_senior_official.short_description = "Senior official" # type: ignore
def portfolio_organization_type(self, obj):
return (
DomainRequest.OrganizationChoices.get_org_label(obj.portfolio.organization_type)
if obj.portfolio and obj.portfolio.organization_type
else "-"
)
portfolio_organization_type.short_description = "Organization type" # type: ignore
def portfolio_federal_type(self, obj):
return (
BranchChoices.get_branch_label(obj.portfolio.federal_type)
if obj.portfolio and obj.portfolio.federal_type
else "-"
)
portfolio_federal_type.short_description = "Federal type" # type: ignore
def portfolio_organization_name(self, obj):
return obj.portfolio.organization_name if obj.portfolio else ""
portfolio_organization_name.short_description = "Organization name" # type: ignore
def portfolio_federal_agency(self, obj):
return obj.portfolio.federal_agency if obj.portfolio else ""
portfolio_federal_agency.short_description = "Federal agency" # type: ignore
def portfolio_state_territory(self, obj):
return obj.portfolio.state_territory if obj.portfolio else ""
portfolio_state_territory.short_description = "State, territory, or military post" # type: ignore
def portfolio_address_line1(self, obj):
return obj.portfolio.address_line1 if obj.portfolio else ""
portfolio_address_line1.short_description = "Address line 1" # type: ignore
def portfolio_address_line2(self, obj):
return obj.portfolio.address_line2 if obj.portfolio else ""
portfolio_address_line2.short_description = "Address line 2" # type: ignore
def portfolio_city(self, obj):
return obj.portfolio.city if obj.portfolio else ""
portfolio_city.short_description = "City" # type: ignore
def portfolio_zipcode(self, obj):
return obj.portfolio.zipcode if obj.portfolio else ""
portfolio_zipcode.short_description = "Zip code" # type: ignore
def portfolio_urbanization(self, obj):
return obj.portfolio.urbanization if obj.portfolio else ""
portfolio_urbanization.short_description = "Urbanization" # type: ignore
# Filters
list_filter = [GenericOrgFilter]
@ -1537,16 +1617,36 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
None,
{
"fields": [
"portfolio",
"sub_organization",
"creator",
"domain_request",
"notes",
]
},
),
(
"Requested by",
{
"fields": [
"portfolio",
"sub_organization",
"creator",
]
},
),
(".gov domain", {"fields": ["domain"]}),
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
(
"Contacts",
{
"fields": [
"senior_official",
"portfolio_senior_official",
"other_contacts",
"no_other_contacts_rationale",
"cisa_representative_first_name",
"cisa_representative_last_name",
"cisa_representative_email",
]
},
),
("Background info", {"fields": ["anything_else"]}),
(
"Type of organization",
@ -1595,10 +1695,58 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
],
},
),
# the below three sections are for portfolio fields
(
"Type of organization",
{
"fields": [
"portfolio_organization_type",
"portfolio_federal_type",
]
},
),
(
"Organization name and mailing address",
{
"fields": [
"portfolio_organization_name",
"portfolio_federal_agency",
]
},
),
(
"Show details",
{
"classes": ["collapse--dgfieldset"],
"description": "Extends organization name and mailing address",
"fields": [
"portfolio_state_territory",
"portfolio_address_line1",
"portfolio_address_line2",
"portfolio_city",
"portfolio_zipcode",
"portfolio_urbanization",
],
},
),
]
# Readonly fields for analysts and superusers
readonly_fields = ("other_contacts", "is_election_board")
readonly_fields = (
"portfolio_senior_official",
"portfolio_organization_type",
"portfolio_federal_type",
"portfolio_organization_name",
"portfolio_federal_agency",
"portfolio_state_territory",
"portfolio_address_line1",
"portfolio_address_line2",
"portfolio_city",
"portfolio_zipcode",
"portfolio_urbanization",
"other_contacts",
"is_election_board",
)
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
@ -2575,7 +2723,72 @@ class DomainInformationInline(admin.StackedInline):
template = "django/admin/includes/domain_info_inline_stacked.html"
model = models.DomainInformation
# Define methods to display fields from the related portfolio
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
portfolio_senior_official.short_description = "Senior official" # type: ignore
def portfolio_organization_type(self, obj):
return (
DomainRequest.OrganizationChoices.get_org_label(obj.portfolio.organization_type)
if obj.portfolio and obj.portfolio.organization_type
else "-"
)
portfolio_organization_type.short_description = "Organization type" # type: ignore
def portfolio_federal_type(self, obj):
return (
BranchChoices.get_branch_label(obj.portfolio.federal_type)
if obj.portfolio and obj.portfolio.federal_type
else "-"
)
portfolio_federal_type.short_description = "Federal type" # type: ignore
def portfolio_organization_name(self, obj):
return obj.portfolio.organization_name if obj.portfolio else ""
portfolio_organization_name.short_description = "Organization name" # type: ignore
def portfolio_federal_agency(self, obj):
return obj.portfolio.federal_agency if obj.portfolio else ""
portfolio_federal_agency.short_description = "Federal agency" # type: ignore
def portfolio_state_territory(self, obj):
return obj.portfolio.state_territory if obj.portfolio else ""
portfolio_state_territory.short_description = "State, territory, or military post" # type: ignore
def portfolio_address_line1(self, obj):
return obj.portfolio.address_line1 if obj.portfolio else ""
portfolio_address_line1.short_description = "Address line 1" # type: ignore
def portfolio_address_line2(self, obj):
return obj.portfolio.address_line2 if obj.portfolio else ""
portfolio_address_line2.short_description = "Address line 2" # type: ignore
def portfolio_city(self, obj):
return obj.portfolio.city if obj.portfolio else ""
portfolio_city.short_description = "City" # type: ignore
def portfolio_zipcode(self, obj):
return obj.portfolio.zipcode if obj.portfolio else ""
portfolio_zipcode.short_description = "Zip code" # type: ignore
def portfolio_urbanization(self, obj):
return obj.portfolio.urbanization if obj.portfolio else ""
portfolio_urbanization.short_description = "Urbanization" # type: ignore
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
readonly_fields = copy.deepcopy(DomainInformationAdmin.readonly_fields)
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)

View file

@ -1,3 +1,5 @@
import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js';
/**
* A function that appends target="_blank" to the domain_form buttons
*/
@ -28,3 +30,14 @@ export function initDomainFormTargetBlankButtons() {
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
}
}
/**
* A function for dynamic Domain fields
*/
export function initDynamicDomainFields(){
const domainPage = document.getElementById("domain_form");
if (domainPage) {
handlePortfolioSelection("#id_domain_info-0-portfolio",
"#id_domain_info-0-sub_organization");
}
}

View file

@ -1,4 +1,4 @@
import { handleSuborganizationFields } from './helpers-portfolio-dynamic-fields.js';
import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js';
/**
* A function for dynamic DomainInformation fields
@ -6,12 +6,7 @@ import { handleSuborganizationFields } from './helpers-portfolio-dynamic-fields.
export function initDynamicDomainInformationFields(){
const domainInformationPage = document.getElementById("domaininformation_form");
if (domainInformationPage) {
handleSuborganizationFields();
}
// DomainInformation is embedded inside domain so this should fire there too
const domainPage = document.getElementById("domain_form");
if (domainPage) {
handleSuborganizationFields(portfolioDropdownSelector="#id_domain_info-0-portfolio", suborgDropdownSelector="#id_domain_info-0-sub_organization");
console.log("handling domain information page");
handlePortfolioSelection();
}
}

View file

@ -1,57 +1,19 @@
import { hideElement, showElement } from './helpers-admin.js';
/**
* Helper function that handles business logic for the suborganization field.
* Can be used anywhere the suborganization dropdown exists
*/
export function handleSuborganizationFields(
portfolioDropdownSelector="#id_portfolio",
suborgDropdownSelector="#id_sub_organization",
requestedSuborgFieldSelector=".field-requested_suborganization",
suborgCitySelector=".field-suborganization_city",
suborgStateTerritorySelector=".field-suborganization_state_territory"
) {
// 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);
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);
}
/**
*
* This function handles the portfolio selection as well as display of
* portfolio-related fields in the DomainRequest Form.
*
* IMPORTANT NOTE: The logic in this method is paired dynamicPortfolioFields
* IMPORTANT NOTE: The business logic in this method is based on dynamicPortfolioFields
*/
export function handlePortfolioSelection() {
export function handlePortfolioSelection(
portfolioDropdownSelector="#id_portfolio",
suborgDropdownSelector="#id_sub_organization"
) {
// These dropdown are select2 fields so they must be interacted with via jquery
const portfolioDropdown = django.jQuery("#id_portfolio");
const suborganizationDropdown = django.jQuery("#id_sub_organization");
const portfolioDropdown = django.jQuery(portfolioDropdownSelector);
const suborganizationDropdown = django.jQuery(suborgDropdownSelector);
const suborganizationField = document.querySelector(".field-sub_organization");
const requestedSuborganizationField = document.querySelector(".field-requested_suborganization");
const suborganizationCity = document.querySelector(".field-suborganization_city");
@ -440,8 +402,8 @@ export function handlePortfolioSelection() {
showElement(portfolioSeniorOfficialField);
// Hide fields not applicable when a portfolio is selected
hideElement(otherEmployeesField);
hideElement(noOtherContactsRationaleField);
if (otherEmployeesField) hideElement(otherEmployeesField);
if (noOtherContactsRationaleField) hideElement(noOtherContactsRationaleField);
hideElement(cisaRepresentativeFirstNameField);
hideElement(cisaRepresentativeLastNameField);
hideElement(cisaRepresentativeEmailField);
@ -463,8 +425,8 @@ export function handlePortfolioSelection() {
// Show fields that are relevant when no portfolio is selected
showElement(seniorOfficialField);
hideElement(portfolioSeniorOfficialField);
showElement(otherEmployeesField);
showElement(noOtherContactsRationaleField);
if (otherEmployeesField) showElement(otherEmployeesField);
if (noOtherContactsRationaleField) showElement(noOtherContactsRationaleField);
showElement(cisaRepresentativeFirstNameField);
showElement(cisaRepresentativeLastNameField);
showElement(cisaRepresentativeEmailField);
@ -504,14 +466,14 @@ export function handlePortfolioSelection() {
if (portfolio_id && !suborganization_id) {
// Show suborganization request fields
showElement(requestedSuborganizationField);
showElement(suborganizationCity);
showElement(suborganizationStateTerritory);
if (requestedSuborganizationField) showElement(requestedSuborganizationField);
if (suborganizationCity) showElement(suborganizationCity);
if (suborganizationStateTerritory) showElement(suborganizationStateTerritory);
} else {
// Hide suborganization request fields if suborganization is selected
hideElement(requestedSuborganizationField);
hideElement(suborganizationCity);
hideElement(suborganizationStateTerritory);
if (requestedSuborganizationField) hideElement(requestedSuborganizationField);
if (suborganizationCity) hideElement(suborganizationCity);
if (suborganizationStateTerritory) hideElement(suborganizationStateTerritory);
}
}

View file

@ -14,6 +14,7 @@ import {
import { initDomainFormTargetBlankButtons } from './domain-form.js';
import { initDynamicPortfolioFields } from './portfolio-form.js';
import { initDynamicDomainInformationFields } from './domain-information-form.js';
import { initDynamicDomainFields } from './domain-form.js';
// General
initModals();
@ -33,6 +34,7 @@ initDynamicDomainRequestFields();
// Domain
initDomainFormTargetBlankButtons();
initDynamicDomainFields();
// Portfolio
initDynamicPortfolioFields();

View file

@ -2,203 +2,212 @@ import { hideElement, showElement } from './helpers-admin.js';
/**
* A function for dynamically changing some fields on the portfolio admin model
* IMPORTANT NOTE: The logic in this function is paired handlePortfolioSelection and should be refactored once we solidify our requirements.
* IMPORTANT NOTE: The business logic in this function is related to handlePortfolioSelection
*/
export function initDynamicPortfolioFields(){
// the federal agency change listener fires on page load, which we don't want.
var isInitialPageLoad = true
// 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");
if (!isPortfolioPage) {
return;
}
function handlePortfolioFields(){
let isPageLoading = true
// $ symbolically denotes that this is using jQuery
let $federalAgency = django.jQuery("#id_federal_agency");
let organizationType = document.getElementById("id_organization_type");
let readonlyOrganizationType = document.querySelector(".field-organization_type .readonly");
const $seniorOfficialDropdown = django.jQuery("#id_senior_official");
const seniorOfficialField = document.querySelector(".field-senior_official");
const seniorOfficialAddress = seniorOfficialField.querySelector(".dja-address-contact-list");
const seniorOfficialReadonly = seniorOfficialField.querySelector(".readonly");
const $federalAgencyDropdown = django.jQuery("#id_federal_agency");
const federalAgencyField = document.querySelector(".field-federal_agency");
const organizationTypeField = document.querySelector(".field-organization_type");
const organizationTypeReadonly = organizationTypeField.querySelector(".readonly");
const organizationTypeDropdown = document.getElementById("id_organization_type");
const organizationNameField = document.querySelector(".field-organization_name");
const federalTypeField = document.querySelector(".field-federal_type");
const urbanizationField = document.querySelector(".field-urbanization");
const stateTerritoryDropdown = document.getElementById("id_state_territory");
const seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
const seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
const federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
let organizationNameContainer = document.querySelector(".field-organization_name");
let federalType = document.querySelector(".field-federal_type");
if ($federalAgency && (organizationType || readonlyOrganizationType)) {
// Attach the change event listener
$federalAgency.on("change", function() {
handleFederalAgencyChange($federalAgency, organizationType, readonlyOrganizationType, organizationNameContainer, federalType);
/**
* Fetches federal type data based on a selected agency using an AJAX call.
*
* @param {string} agency
* @returns {Promise<Object|null>} - A promise that resolves to the portfolio data object if successful,
* or null if there was an error.
*/
function getFederalTypeFromAgency(agency) {
return fetch(`${federalPortfolioApi}?&agency_name=${agency}`)
.then(response => {
const statusCode = response.status;
return response.json().then(data => ({ statusCode, data }));
})
.then(({ statusCode, data }) => {
if (data.error) {
console.error("Error in AJAX call: " + data.error);
return;
}
return data.federal_type
})
.catch(error => {
console.error("Error fetching federal and portfolio types: ", error);
return null
});
}
// Handle dynamically hiding the urbanization field
let urbanizationField = document.querySelector(".field-urbanization");
let stateTerritory = document.getElementById("id_state_territory");
if (urbanizationField && stateTerritory) {
// Execute this function once on load
handleStateTerritoryChange(stateTerritory, urbanizationField);
// Attach the change event listener for state/territory
stateTerritory.addEventListener("change", function() {
handleStateTerritoryChange(stateTerritory, urbanizationField);
/**
* Fetches senior official contact data based on a selected agency using an AJAX call.
*
* @param {string} agency
* @returns {Promise<Object|null>} - A promise that resolves to the portfolio data object if successful,
* or null if there was an error.
*/
function getSeniorOfficialFromAgency(agency) {
return fetch(`${seniorOfficialApi}?agency_name=${agency}`)
.then(response => {
const statusCode = response.status;
return response.json().then(data => ({ statusCode, data }));
})
.then(({ statusCode, data }) => {
if (data.error) {
// Throw an error with status code and message
throw { statusCode, message: data.error };
} else {
return data;
}
})
.catch(error => {
console.error("Error fetching senior official: ", error);
throw error; // Re-throw for external handling
});
}
// Handle hiding the organization name field when the organization_type is federal.
// Run this first one page load, then secondly on a change event.
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
organizationType.addEventListener("change", function() {
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
});
});
function handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType) {
if (organizationType && organizationNameContainer) {
let selectedValue = organizationType.value;
/**
* Handles the side effects of change on the organization type field
*
* 1. If selection is federal, hide org name, show federal agency, show federal type if applicable
* 2. else show org name, hide federal agency, hide federal type if applicable
*/
function handleOrganizationTypeChange() {
if (organizationTypeDropdown && organizationNameField) {
let selectedValue = organizationTypeDropdown.value;
if (selectedValue === "federal") {
hideElement(organizationNameContainer);
showElement(federalAgencyContainer);
if (federalType) {
showElement(federalType);
hideElement(organizationNameField);
showElement(federalAgencyField);
if (federalTypeField) {
showElement(federalTypeField);
}
} else {
showElement(organizationNameContainer);
hideElement(federalAgencyContainer);
if (federalType) {
hideElement(federalType);
showElement(organizationNameField);
hideElement(federalAgencyField);
if (federalTypeField) {
hideElement(federalTypeField);
}
}
}
}
function handleFederalAgencyChange(federalAgency, organizationType, readonlyOrganizationType, organizationNameContainer, federalType) {
// Don't do anything on page load
if (isInitialPageLoad) {
isInitialPageLoad = false;
/**
* Handles the side effects of change on the federal agency field
*
* 1. handle org type dropdown or readonly
* 2. call handleOrganizationTypeChange
* 3. call getFederalTypeFromAgency and update federal type
* 4. call getSeniorOfficialFromAgency and update the SO fieldset
*/
function handleFederalAgencyChange() {
if (!isPageLoading) {
let selectedFederalAgency = $federalAgencyDropdown.find("option:selected").text();
if (!selectedFederalAgency) {
return;
}
// Set the org type to federal if an agency is selected
let selectedText = federalAgency.find("option:selected").text();
// There isn't a federal senior official associated with null records
if (!selectedText) {
return;
}
let organizationTypeValue = organizationType ? organizationType.value : readonlyOrganizationType.innerText.toLowerCase();
if (selectedText !== "Non-Federal Agency") {
// 1. Handle organization type
let organizationTypeValue = organizationTypeDropdown ? organizationTypeDropdown.value : organizationTypeReadonly.innerText.toLowerCase();
if (selectedFederalAgency !== "Non-Federal Agency") {
if (organizationTypeValue !== "federal") {
if (organizationType){
organizationType.value = "federal";
if (organizationTypeDropdown){
organizationTypeDropdown.value = "federal";
} else {
readonlyOrganizationType.innerText = "Federal"
organizationTypeReadonly.innerText = "Federal"
}
}
} else {
if (organizationTypeValue === "federal") {
if (organizationType){
organizationType.value = "";
if (organizationTypeDropdown){
organizationTypeDropdown.value = "";
} else {
readonlyOrganizationType.innerText = "-"
organizationTypeReadonly.innerText = "-"
}
}
}
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
// 2. Handle organization type change side effects
handleOrganizationTypeChange();
// Determine if any changes are necessary to the display of portfolio type or federal type
// based on changes to the Federal Agency
let federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
fetch(`${federalPortfolioApi}?&agency_name=${selectedText}`)
.then(response => {
const statusCode = response.status;
return response.json().then(data => ({ statusCode, data }));
})
.then(({ statusCode, data }) => {
if (data.error) {
console.error("Error in AJAX call: " + data.error);
return;
}
updateReadOnly(data.federal_type, '.field-federal_type');
})
.catch(error => console.error("Error fetching federal and portfolio types: ", error));
// Hide the contactList initially.
// If we can update the contact information, it'll be shown again.
hideElement(contactList.parentElement);
let seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
let $seniorOfficial = django.jQuery("#id_senior_official");
let readonlySeniorOfficial = document.querySelector(".field-senior_official .readonly");
let seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
fetch(`${seniorOfficialApi}?agency_name=${selectedText}`)
.then(response => {
const statusCode = response.status;
return response.json().then(data => ({ statusCode, data }));
})
.then(({ statusCode, data }) => {
if (data.error) {
// Clear the field if the SO doesn't exist.
if (statusCode === 404) {
if ($seniorOfficial && $seniorOfficial.length > 0) {
$seniorOfficial.val("").trigger("change");
}else {
// Show the "create one now" text if this field is none in readonly mode.
readonlySeniorOfficial.innerHTML = `<a href="${seniorOfficialAddUrl}">No senior official found. Create one now.</a>`;
}
console.warn("Record not found: " + data.error);
}else {
console.error("Error in AJAX call: " + data.error);
}
return;
}
// 3. Handle federal type
getFederalTypeFromAgency(selectedFederalAgency).then((federalType) => updateReadOnly(federalType, '.field-federal_type'));
// 4. Handle senior official
hideElement(seniorOfficialAddress.parentElement);
getSeniorOfficialFromAgency(selectedFederalAgency).then((senior_official) => {
// Update the "contact details" blurb beneath senior official
updateContactInfo(data);
showElement(contactList.parentElement);
updateSeniorOfficialContactInfo(senior_official);
showElement(seniorOfficialAddress.parentElement);
// Get the associated senior official with this federal agency
let seniorOfficialId = data.id;
let seniorOfficialName = [data.first_name, data.last_name].join(" ");
if ($seniorOfficial && $seniorOfficial.length > 0) {
let seniorOfficialId = senior_official.id;
let seniorOfficialName = [senior_official.first_name, senior_official.last_name].join(" ");
if ($seniorOfficialDropdown && $seniorOfficialDropdown.length > 0) {
// If the senior official is a dropdown field, edit that
updateSeniorOfficialDropdown($seniorOfficial, seniorOfficialId, seniorOfficialName);
updateSeniorOfficialDropdown(seniorOfficialId, seniorOfficialName);
} else {
if (readonlySeniorOfficial) {
if (seniorOfficialReadonly) {
let seniorOfficialLink = `<a href=/admin/registrar/seniorofficial/${seniorOfficialId}/change/>${seniorOfficialName}</a>`
readonlySeniorOfficial.innerHTML = seniorOfficialName ? seniorOfficialLink : "-";
seniorOfficialReadonly.innerHTML = seniorOfficialName ? seniorOfficialLink : "-";
}
}
})
.catch(error => console.error("Error fetching senior official: ", error));
.catch(error => {
if (error.statusCode === 404) {
// Handle "not found" senior official
if ($seniorOfficialDropdown && $seniorOfficialDropdown.length > 0) {
$seniorOfficialDropdown.val("").trigger("change");
} else {
seniorOfficialReadonly.innerHTML = `<a href="${seniorOfficialAddUrl}">No senior official found. Create one now.</a>`;
}
} else {
// Handle other errors
console.error("An error occurred:", error.message);
}
});
} else {
isPageLoading = false;
}
}
function updateSeniorOfficialDropdown(dropdown, seniorOfficialId, seniorOfficialName) {
/**
* Helper for updating federal type field
*/
function updateSeniorOfficialDropdown(seniorOfficialId, seniorOfficialName) {
if (!seniorOfficialId || !seniorOfficialName || !seniorOfficialName.trim()){
// Clear the field if the SO doesn't exist
dropdown.val("").trigger("change");
$seniorOfficialDropdown.val("").trigger("change");
return;
}
// Add the senior official to the dropdown.
// This format supports select2 - if we decide to convert this field in the future.
if (dropdown.find(`option[value='${seniorOfficialId}']`).length) {
if ($seniorOfficialDropdown.find(`option[value='${seniorOfficialId}']`).length) {
// Select the value that is associated with the current Senior Official.
dropdown.val(seniorOfficialId).trigger("change");
$seniorOfficialDropdown.val(seniorOfficialId).trigger("change");
} else {
// Create a DOM Option that matches the desired Senior Official. Then append it and select it.
let userOption = new Option(seniorOfficialName, seniorOfficialId, true, true);
dropdown.append(userOption).trigger("change");
$seniorOfficialDropdown.append(userOption).trigger("change");
}
}
function handleStateTerritoryChange(stateTerritory, urbanizationField) {
let selectedValue = stateTerritory.value;
/**
* Handle urbanization
*/
function handleStateTerritoryChange() {
let selectedValue = stateTerritoryDropdown.value;
if (selectedValue === "PR") {
showElement(urbanizationField)
} else {
@ -207,11 +216,7 @@ export function initDynamicPortfolioFields(){
}
/**
* Utility that selects a div from the DOM using selectorString,
* and updates a div within that div which has class of 'readonly'
* so that the text of the div is updated to updateText
* @param {*} updateText
* @param {*} selectorString
* Helper for updating senior official dropdown
*/
function updateReadOnly(updateText, selectorString) {
// find the div by selectorString
@ -226,34 +231,75 @@ export function initDynamicPortfolioFields(){
}
}
function updateContactInfo(data) {
if (!contactList) return;
const titleSpan = contactList.querySelector(".contact_info_title");
const emailSpan = contactList.querySelector(".contact_info_email");
const phoneSpan = contactList.querySelector(".contact_info_phone");
/**
* Helper for updating senior official contact info
*/
function updateSeniorOfficialContactInfo(senior_official) {
if (!seniorOfficialAddress) return;
const titleSpan = seniorOfficialAddress.querySelector(".contact_info_title");
const emailSpan = seniorOfficialAddress.querySelector(".contact_info_email");
const phoneSpan = seniorOfficialAddress.querySelector(".contact_info_phone");
if (titleSpan) {
titleSpan.textContent = data.title || "None";
titleSpan.textContent = senior_official.title || "None";
};
// Update the email field and the content for the clipboard
if (emailSpan) {
let copyButton = contactList.querySelector(".admin-icon-group");
emailSpan.textContent = data.email || "None";
if (data.email) {
const clipboardInput = contactList.querySelector(".admin-icon-group input");
let copyButton = seniorOfficialAddress.querySelector(".admin-icon-group");
emailSpan.textContent = senior_official.email || "None";
if (senior_official.email) {
const clipboardInput = seniorOfficialAddress.querySelector(".admin-icon-group input");
if (clipboardInput) {
clipboardInput.value = data.email;
clipboardInput.value = senior_official.email;
};
showElement(copyButton);
}else {
hideElement(copyButton);
}
}
if (phoneSpan) {
phoneSpan.textContent = data.phone || "None";
phoneSpan.textContent = senior_official.phone || "None";
};
}
/**
* Initializes necessary data and display configurations for the portfolio fields.
*/
function initializePortfolioSettings() {
if (urbanizationField && stateTerritoryDropdown) {
handleStateTerritoryChange();
}
handleOrganizationTypeChange();
}
/**
* Sets event listeners for key UI elements.
*/
function setEventListeners() {
if ($federalAgencyDropdown && (organizationTypeDropdown || organizationTypeReadonly)) {
$federalAgencyDropdown.on("change", function() {
handleFederalAgencyChange();
});
}
if (urbanizationField && stateTerritoryDropdown) {
stateTerritoryDropdown.addEventListener("change", function() {
handleStateTerritoryChange();
});
}
organizationTypeDropdown.addEventListener("change", function() {
handleOrganizationTypeChange();
});
}
// Run initial setup functions
initializePortfolioSettings();
setEventListeners();
}
export function initDynamicPortfolioFields() {
document.addEventListener('DOMContentLoaded', function() {
let isPortfolioPage = document.getElementById("portfolio_form");
if (isPortfolioPage) {
handlePortfolioFields();
}
});
}

View file

@ -1,6 +1,13 @@
{% extends 'admin/change_form.html' %}
{% load i18n static %}
{% block content %}
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get-portfolio-json' as url %}
<input id="portfolio_json_url" class="display-none" value="{{url}}" />
{{ block.super }}
{% endblock content %}
{% block field_sets %}
<div class="display-flex flex-row flex-justify submit-row">
<div class="flex-align-self-start button-list-mobile">

View file

@ -1,6 +1,13 @@
{% extends 'admin/change_form.html' %}
{% load i18n static %}
{% block content %}
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get-portfolio-json' as url %}
<input id="portfolio_json_url" class="display-none" value="{{url}}" />
{{ block.super }}
{% endblock content %}
{% block field_sets %}
{% for fieldset in adminform %}
{% comment %}

View file

@ -853,9 +853,9 @@ class TestDomainInformationAdmin(TestCase):
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
# Test for the copy link
# We expect 3 in the form + 2 from the js module copy-to-clipboard.js
# We expect 4 in the form + 2 from the js module copy-to-clipboard.js
# that gets pulled in the test in django.contrib.staticfiles.finders.FileSystemFinder
self.assertContains(response, "copy-to-clipboard", count=5)
self.assertContains(response, "copy-to-clipboard", count=6)
# cleanup this test
domain_info.delete()
@ -871,6 +871,17 @@ class TestDomainInformationAdmin(TestCase):
readonly_fields = self.admin.get_readonly_fields(request)
expected_fields = [
"portfolio_senior_official",
"portfolio_organization_type",
"portfolio_federal_type",
"portfolio_organization_name",
"portfolio_federal_agency",
"portfolio_state_territory",
"portfolio_address_line1",
"portfolio_address_line2",
"portfolio_city",
"portfolio_zipcode",
"portfolio_urbanization",
"other_contacts",
"is_election_board",
"federal_agency",