diff --git a/src/registrar/admin.py b/src/registrar/admin.py index fecd17146..4465b7098 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -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) diff --git a/src/registrar/assets/src/js/getgov-admin/domain-form.js b/src/registrar/assets/src/js/getgov-admin/domain-form.js index 474e2822c..5335bd0d3 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-form.js @@ -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"); + } +} diff --git a/src/registrar/assets/src/js/getgov-admin/domain-information-form.js b/src/registrar/assets/src/js/getgov-admin/domain-information-form.js index 8139c752f..6c79bd32a 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-information-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-information-form.js @@ -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(); } } diff --git a/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js b/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js index 39f30b87f..0e5946c23 100644 --- a/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js +++ b/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js @@ -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); } } diff --git a/src/registrar/assets/src/js/getgov-admin/main.js b/src/registrar/assets/src/js/getgov-admin/main.js index ec9aeeedf..64be572b2 100644 --- a/src/registrar/assets/src/js/getgov-admin/main.js +++ b/src/registrar/assets/src/js/getgov-admin/main.js @@ -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(); diff --git a/src/registrar/assets/src/js/getgov-admin/portfolio-form.js b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js index f001bf39b..74729c2b2 100644 --- a/src/registrar/assets/src/js/getgov-admin/portfolio-form.js +++ b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js @@ -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(){ +function handlePortfolioFields(){ - // the federal agency change listener fires on page load, which we don't want. - var isInitialPageLoad = true + let isPageLoading = true + // $ symbolically denotes that this is using jQuery + 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; - // 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; - } - - // $ 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"); - - 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} - 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} - 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; - return; - } + /** + * 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) { - // 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") { - if (organizationTypeValue !== "federal") { - if (organizationType){ - organizationType.value = "federal"; - }else { - readonlyOrganizationType.innerText = "Federal" - } - } - }else { - if (organizationTypeValue === "federal") { - if (organizationType){ - organizationType.value = ""; - }else { - readonlyOrganizationType.innerText = "-" - } - } - } - - handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType); - - // 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); + let selectedFederalAgency = $federalAgencyDropdown.find("option:selected").text(); + if (!selectedFederalAgency) { 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 = `No senior official found. Create one now.`; + // 1. Handle organization type + let organizationTypeValue = organizationTypeDropdown ? organizationTypeDropdown.value : organizationTypeReadonly.innerText.toLowerCase(); + if (selectedFederalAgency !== "Non-Federal Agency") { + if (organizationTypeValue !== "federal") { + if (organizationTypeDropdown){ + organizationTypeDropdown.value = "federal"; + } else { + organizationTypeReadonly.innerText = "Federal" + } + } + } else { + if (organizationTypeValue === "federal") { + if (organizationTypeDropdown){ + organizationTypeDropdown.value = ""; + } else { + organizationTypeReadonly.innerText = "-" } - console.warn("Record not found: " + data.error); - }else { - console.error("Error in AJAX call: " + data.error); } - return; } - // Update the "contact details" blurb beneath senior official - updateContactInfo(data); - showElement(contactList.parentElement); + // 2. Handle organization type change side effects + handleOrganizationTypeChange(); + + // 3. Handle federal type + getFederalTypeFromAgency(selectedFederalAgency).then((federalType) => updateReadOnly(federalType, '.field-federal_type')); - // 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) { - // If the senior official is a dropdown field, edit that - updateSeniorOfficialDropdown($seniorOfficial, seniorOfficialId, seniorOfficialName); - }else { - if (readonlySeniorOfficial) { - let seniorOfficialLink = `${seniorOfficialName}` - readonlySeniorOfficial.innerHTML = seniorOfficialName ? seniorOfficialLink : "-"; + // 4. Handle senior official + hideElement(seniorOfficialAddress.parentElement); + getSeniorOfficialFromAgency(selectedFederalAgency).then((senior_official) => { + // Update the "contact details" blurb beneath senior official + updateSeniorOfficialContactInfo(senior_official); + showElement(seniorOfficialAddress.parentElement); + // Get the associated senior official with this federal agency + 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(seniorOfficialId, seniorOfficialName); + } else { + if (seniorOfficialReadonly) { + let seniorOfficialLink = `${seniorOfficialName}` + 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 = `No senior official found. Create one now.`; + } + } 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(); + } + }); } diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index 662328660..7aa0034b9 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -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 %} + + {{ block.super }} +{% endblock content %} + {% block field_sets %}
diff --git a/src/registrar/templates/django/admin/domain_information_change_form.html b/src/registrar/templates/django/admin/domain_information_change_form.html index c5b0d54b8..487fd97e1 100644 --- a/src/registrar/templates/django/admin/domain_information_change_form.html +++ b/src/registrar/templates/django/admin/domain_information_change_form.html @@ -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 %} + + {{ block.super }} +{% endblock content %} + {% block field_sets %} {% for fieldset in adminform %} {% comment %} diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 8307163c6..a259e5bef 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -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",