Merge pull request #3040 from cisagov/dk/2934-dja-domain-request

#2934: Django Admin - Dynamic Domain Request edit page
This commit is contained in:
dave-kennedy-ecs 2024-11-18 12:05:52 -05:00 committed by GitHub
commit 68dc4942a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 992 additions and 29 deletions

3
.gitignore vendored
View file

@ -171,6 +171,9 @@ node_modules
# Vim
*.swp
# VS Code
.vscode
# Compliance/trestle related
docs/compliance/.trestle/cache

View file

@ -1,12 +1,14 @@
from datetime import date
import logging
import copy
from typing import Optional
from django import forms
from django.db.models import Value, CharField, Q
from django.db.models.functions import Concat, Coalesce
from django.http import HttpResponseRedirect
from registrar.models.federal_agency import FederalAgency
from registrar.utility.admin_helpers import (
AutocompleteSelectWithPlaceholder,
get_action_needed_reason_default_email,
get_rejection_reason_default_email,
get_field_links_as_list,
@ -236,6 +238,14 @@ class DomainRequestAdminForm(forms.ModelForm):
"current_websites": NoAutocompleteFilteredSelectMultiple("current_websites", False),
"alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False),
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
"portfolio": AutocompleteSelectWithPlaceholder(
DomainRequest._meta.get_field("portfolio"), admin.site, attrs={"data-placeholder": "---------"}
),
"sub_organization": AutocompleteSelectWithPlaceholder(
DomainRequest._meta.get_field("sub_organization"),
admin.site,
attrs={"data-placeholder": "---------", "ajax-url": "get-suborganization-list-json"},
),
}
labels = {
"action_needed_reason_email": "Email",
@ -1816,6 +1826,70 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
custom_election_board.admin_order_field = "is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore
# 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
# This is just a placeholder. This field will be populated in the detail_table_fieldset view.
# This is not a field that exists on the model.
def status_history(self, obj):
@ -1847,30 +1921,38 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
None,
{
"fields": [
"portfolio",
"sub_organization",
"requested_suborganization",
"suborganization_city",
"suborganization_state_territory",
"status_history",
"status",
"rejection_reason",
"rejection_reason_email",
"action_needed_reason",
"action_needed_reason_email",
"investigator",
"creator",
"approved_domain",
"investigator",
"notes",
]
},
),
(
"Requested by",
{
"fields": [
"portfolio",
"sub_organization",
"requested_suborganization",
"suborganization_city",
"suborganization_state_territory",
"creator",
]
},
),
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
(
"Contacts",
{
"fields": [
"senior_official",
"portfolio_senior_official",
"other_contacts",
"no_other_contacts_rationale",
"cisa_representative_first_name",
@ -1927,10 +2009,55 @@ class DomainRequestAdmin(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 = (
"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",
"current_websites",
"alternative_domains",
@ -1979,10 +2106,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
def get_fieldsets(self, request, obj=None):
fieldsets = super().get_fieldsets(request, obj)
# Hide certain suborg fields behind the organization feature flag
# Hide certain portfolio and suborg fields behind the organization requests flag
# if it is not enabled
if not flag_is_active_for_user(request.user, "organization_feature"):
if not flag_is_active_for_user(request.user, "organization_requests"):
excluded_fields = [
"portfolio",
"sub_organization",
"requested_suborganization",
"suborganization_city",
"suborganization_state_territory",

View file

@ -86,6 +86,506 @@ function handleSuborganizationFields(
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
*/
function handlePortfolioSelection() {
// 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 suborganizationField = document.querySelector(".field-sub_organization");
const requestedSuborganizationField = document.querySelector(".field-requested_suborganization");
const suborganizationCity = document.querySelector(".field-suborganization_city");
const suborganizationStateTerritory = document.querySelector(".field-suborganization_state_territory");
const seniorOfficialField = document.querySelector(".field-senior_official");
const otherEmployeesField = document.querySelector(".field-other_contacts");
const noOtherContactsRationaleField = document.querySelector(".field-no_other_contacts_rationale");
const cisaRepresentativeFirstNameField = document.querySelector(".field-cisa_representative_first_name");
const cisaRepresentativeLastNameField = document.querySelector(".field-cisa_representative_last_name");
const cisaRepresentativeEmailField = document.querySelector(".field-cisa_representative_email");
const orgTypeFieldSet = document.querySelector(".field-is_election_board").parentElement;
const orgTypeFieldSetDetails = orgTypeFieldSet.nextElementSibling;
const orgNameFieldSet = document.querySelector(".field-organization_name").parentElement;
const orgNameFieldSetDetails = orgNameFieldSet.nextElementSibling;
const portfolioSeniorOfficialField = document.querySelector(".field-portfolio_senior_official");
const portfolioSeniorOfficial = portfolioSeniorOfficialField.querySelector(".readonly");
const portfolioSeniorOfficialAddress = portfolioSeniorOfficialField.querySelector(".dja-address-contact-list");
const portfolioOrgTypeFieldSet = document.querySelector(".field-portfolio_organization_type").parentElement;
const portfolioOrgType = document.querySelector(".field-portfolio_organization_type .readonly");
const portfolioFederalTypeField = document.querySelector(".field-portfolio_federal_type");
const portfolioFederalType = portfolioFederalTypeField.querySelector(".readonly");
const portfolioOrgNameField = document.querySelector(".field-portfolio_organization_name")
const portfolioOrgName = portfolioOrgNameField.querySelector(".readonly");
const portfolioOrgNameFieldSet = portfolioOrgNameField.parentElement;
const portfolioOrgNameFieldSetDetails = portfolioOrgNameFieldSet.nextElementSibling;
const portfolioFederalAgencyField = document.querySelector(".field-portfolio_federal_agency");
const portfolioFederalAgency = portfolioFederalAgencyField.querySelector(".readonly");
const portfolioStateTerritory = document.querySelector(".field-portfolio_state_territory .readonly");
const portfolioAddressLine1 = document.querySelector(".field-portfolio_address_line1 .readonly");
const portfolioAddressLine2 = document.querySelector(".field-portfolio_address_line2 .readonly");
const portfolioCity = document.querySelector(".field-portfolio_city .readonly");
const portfolioZipcode = document.querySelector(".field-portfolio_zipcode .readonly");
const portfolioUrbanizationField = document.querySelector(".field-portfolio_urbanization");
const portfolioUrbanization = portfolioUrbanizationField.querySelector(".readonly");
const portfolioJsonUrl = document.getElementById("portfolio_json_url")?.value || null;
let isPageLoading = true;
/**
* Fetches portfolio data by ID using an AJAX call.
*
* @param {number|string} portfolio_id - The ID of the portfolio to retrieve.
* @returns {Promise<Object|null>} - A promise that resolves to the portfolio data object if successful,
* or null if there was an error.
*
* This function performs an asynchronous fetch request to retrieve portfolio data.
* If the request is successful, it returns the portfolio data as an object.
* If an error occurs during the request or the data contains an error, it logs the error
* to the console and returns null.
*/
function getPortfolio(portfolio_id) {
return fetch(`${portfolioJsonUrl}?id=${portfolio_id}`)
.then(response => response.json())
.then(data => {
if (data.error) {
console.error("Error in AJAX call: " + data.error);
return null;
} else {
return data;
}
})
.catch(error => {
console.error("Error retrieving portfolio", error);
return null;
});
}
/**
* Updates various UI elements with the data from a given portfolio object.
*
* @param {Object} portfolio - The portfolio data object containing values to populate in the UI.
*
* This function updates multiple fields in the UI to reflect data in the `portfolio` object:
* - Clears and replaces selections in the `suborganizationDropdown` with values from `portfolio.suborganizations`.
* - Calls `updatePortfolioSeniorOfficial` to set the senior official information.
* - Sets the portfolio organization type, federal type, name, federal agency, and other address-related fields.
*
* The function expects that elements like `portfolioOrgType`, `portfolioFederalAgency`, etc.,
* are already defined and accessible in the global scope.
*/
function updatePortfolioFieldsData(portfolio) {
// replace selections in suborganizationDropdown with
// values in portfolio.suborganizations
suborganizationDropdown.empty();
// update portfolio senior official
updatePortfolioSeniorOfficial(portfolio.senior_official);
// update portfolio organization type
portfolioOrgType.innerText = portfolio.organization_type;
// update portfolio federal type
portfolioFederalType.innerText = portfolio.federal_type
// update portfolio organization name
portfolioOrgName.innerText = portfolio.organization_name;
// update portfolio federal agency
portfolioFederalAgency.innerText = portfolio.federal_agency ? portfolio.federal_agency.agency : '';
// update portfolio state
portfolioStateTerritory.innerText = portfolio.state_territory;
// update portfolio address line 1
portfolioAddressLine1.innerText = portfolio.address_line1;
// update portfolio address line 2
portfolioAddressLine2.innerText = portfolio.address_line2;
// update portfolio city
portfolioCity.innerText = portfolio.city;
// update portfolio zip code
portfolioZipcode.innerText = portfolio.zipcode
// update portfolio urbanization
portfolioUrbanization.innerText = portfolio.urbanization;
}
/**
* Updates the UI to display the senior official information from a given object.
*
* @param {Object} senior_official - The senior official's data object, containing details like
* first name, last name, and ID. If `senior_official` is null, displays a default message.
*
* This function:
* - Displays the senior official's name as a link (if available) in the `portfolioSeniorOfficial` element.
* - If a senior official exists, it sets `portfolioSeniorOfficialAddress` to show the official's contact info
* and displays it by calling `updateSeniorOfficialContactInfo`.
* - If no senior official is provided, it hides `portfolioSeniorOfficialAddress` and shows a "No senior official found." message.
*
* Dependencies:
* - Expects the `portfolioSeniorOfficial` and `portfolioSeniorOfficialAddress` elements to be available globally.
* - Uses `showElement` and `hideElement` for visibility control.
*/
function updatePortfolioSeniorOfficial(senior_official) {
if (senior_official) {
let seniorOfficialName = [senior_official.first_name, senior_official.last_name].join(' ');
let seniorOfficialLink = `<a href=/admin/registrar/seniorofficial/${senior_official.id}/change/ class='test'>${seniorOfficialName}</a>`
portfolioSeniorOfficial.innerHTML = seniorOfficialName ? seniorOfficialLink : "-";
updateSeniorOfficialContactInfo(portfolioSeniorOfficialAddress, senior_official);
showElement(portfolioSeniorOfficialAddress);
} else {
portfolioSeniorOfficial.innerText = "No senior official found.";
hideElement(portfolioSeniorOfficialAddress);
}
}
/**
* Populates and displays contact information for a senior official within a specified address field element.
*
* @param {HTMLElement} addressField - The DOM element containing contact info fields for the senior official.
* @param {Object} senior_official - The senior official's data object, containing properties like title, email, and phone.
*
* This function:
* - Sets the `title`, `email`, and `phone` fields in `addressField` to display the senior official's data.
* - Updates the `titleSpan` with the official's title, or "None" if unavailable.
* - Updates the `emailSpan` with the official's email, or "None" if unavailable.
* - If an email is provided, populates `hiddenInput` with the email for copying and shows the `copyButton`.
* - If no email is provided, hides the `copyButton`.
* - Updates the `phoneSpan` with the official's phone number, or "None" if unavailable.
*
* Dependencies:
* - Uses `showElement` and `hideElement` to control visibility of the `copyButton`.
* - Expects `addressField` to have specific classes (.contact_info_title, .contact_info_email, etc.) for query selectors to work.
*/
function updateSeniorOfficialContactInfo(addressField, senior_official) {
const titleSpan = addressField.querySelector(".contact_info_title");
const emailSpan = addressField.querySelector(".contact_info_email");
const phoneSpan = addressField.querySelector(".contact_info_phone");
const hiddenInput = addressField.querySelector("input");
const copyButton = addressField.querySelector(".admin-icon-group");
if (titleSpan) {
titleSpan.textContent = senior_official.title || "None";
};
if (emailSpan) {
emailSpan.textContent = senior_official.email || "None";
if (senior_official.email) {
hiddenInput.value = senior_official.email;
showElement(copyButton);
}else {
hideElement(copyButton);
}
}
if (phoneSpan) {
phoneSpan.textContent = senior_official.phone || "None";
};
}
/**
* Dynamically updates the visibility of certain portfolio fields based on specific conditions.
*
* This function adjusts the display of fields within the portfolio UI based on:
* - The presence of a senior official's contact information.
* - The selected state or territory, affecting the visibility of the urbanization field.
* - The organization type (Federal vs. non-Federal), toggling the visibility of related fields.
*
* Functionality:
* 1. **Senior Official Contact Info Display**:
* - If `portfolioSeniorOfficial` contains "No additional contact information found",
* hides `portfolioSeniorOfficialAddress`; otherwise, shows it.
*
* 2. **Urbanization Field Display**:
* - Displays `portfolioUrbanizationField` only when the `portfolioStateTerritory` value is "PR" (Puerto Rico).
*
* 3. **Federal Organization Type Display**:
* - If `portfolioOrgType` is "Federal", hides `portfolioOrgNameField` and shows both `portfolioFederalAgencyField`
* and `portfolioFederalTypeField`.
* - If not Federal, shows `portfolioOrgNameField` and hides `portfolioFederalAgencyField` and `portfolioFederalTypeField`.
* - Certain text fields (Organization Type, Organization Name, Federal Type, Federal Agency) updated to links
* to edit the portfolio
*
* Dependencies:
* - Expects specific elements to be defined globally (`portfolioSeniorOfficial`, `portfolioUrbanizationField`, etc.).
* - Uses `showElement` and `hideElement` functions to control element visibility.
*/
function updatePortfolioFieldsDataDynamicDisplay() {
// Handle visibility of senior official's contact information
if (portfolioSeniorOfficial.innerText.includes("No senior official found.")) {
hideElement(portfolioSeniorOfficialAddress);
} else {
showElement(portfolioSeniorOfficialAddress);
}
// Handle visibility of urbanization field based on state/territory value
let portfolioStateTerritoryValue = portfolioStateTerritory.innerText;
if (portfolioStateTerritoryValue === "PR") {
showElement(portfolioUrbanizationField);
} else {
hideElement(portfolioUrbanizationField);
}
// Handle visibility of fields based on organization type (Federal vs. others)
if (portfolioOrgType.innerText === "Federal") {
hideElement(portfolioOrgNameField);
showElement(portfolioFederalAgencyField);
showElement(portfolioFederalTypeField);
} else {
showElement(portfolioOrgNameField);
hideElement(portfolioFederalAgencyField);
hideElement(portfolioFederalTypeField);
}
// Modify the display of certain fields to convert them from text to links
// to edit the portfolio
let portfolio_id = portfolioDropdown.val();
let portfolioEditUrl = `/admin/registrar/portfolio/${portfolio_id}/change/`;
let portfolioOrgTypeValue = portfolioOrgType.innerText;
portfolioOrgType.innerHTML = `<a href=${portfolioEditUrl}>${portfolioOrgTypeValue}</a>`;
let portfolioOrgNameValue = portfolioOrgName.innerText;
portfolioOrgName.innerHTML = `<a href=${portfolioEditUrl}>${portfolioOrgNameValue}</a>`;
let portfolioFederalAgencyValue = portfolioFederalAgency.innerText;
portfolioFederalAgency.innerHTML = `<a href=${portfolioEditUrl}>${portfolioFederalAgencyValue}</a>`;
let portfolioFederalTypeValue = portfolioFederalType.innerText;
if (portfolioFederalTypeValue !== '-')
portfolioFederalType.innerHTML = `<a href=${portfolioEditUrl}>${portfolioFederalTypeValue}</a>`;
}
/**
* Asynchronously updates portfolio fields in the UI based on the selected portfolio.
*
* This function first checks if the page is loading or if a portfolio selection is available
* in the `portfolioDropdown`. If a portfolio is selected, it retrieves the portfolio data,
* then updates the UI fields to display relevant data. If no portfolio is selected, it simply
* refreshes the UI field display without new data. The `isPageLoading` flag prevents
* updates during page load.
*
* Workflow:
* 1. **Check Page Loading**:
* - If `isPageLoading` is `true`, set it to `false` and exit to prevent redundant updates.
* - If `isPageLoading` is `false`, proceed with portfolio field updates.
*
* 2. **Portfolio Selection**:
* - If a portfolio is selected (`portfolioDropdown.val()`), fetch the portfolio data.
* - Once data is fetched, run three update functions:
* - `updatePortfolioFieldsData`: Populates specific portfolio-related fields.
* - `updatePortfolioFieldsDisplay`: Handles the visibility of general portfolio fields.
* - `updatePortfolioFieldsDataDynamicDisplay`: Manages conditional display based on portfolio data.
* - If no portfolio is selected, only refreshes the field display using `updatePortfolioFieldsDisplay`.
*
* Dependencies:
* - Expects global elements (`portfolioDropdown`, etc.) and `isPageLoading` flag to be defined.
* - Assumes `getPortfolio`, `updatePortfolioFieldsData`, `updatePortfolioFieldsDisplay`, and `updatePortfolioFieldsDataDynamicDisplay` are available as functions.
*/
async function updatePortfolioFields() {
if (!isPageLoading) {
if (portfolioDropdown.val()) {
getPortfolio(portfolioDropdown.val()).then((portfolio) => {
updatePortfolioFieldsData(portfolio);
updatePortfolioFieldsDisplay();
updatePortfolioFieldsDataDynamicDisplay();
});
} else {
updatePortfolioFieldsDisplay();
}
} else {
isPageLoading = false;
}
}
/**
* Updates the Suborganization Dropdown with new data based on the provided portfolio ID.
*
* This function uses the Select2 jQuery plugin to update the dropdown by fetching suborganization
* data relevant to the selected portfolio. Upon invocation, it checks if Select2 is already initialized
* on `suborganizationDropdown` and destroys the existing instance to avoid duplication.
* It then reinitializes Select2 with customized options for an AJAX request, allowing the user to search
* and select suborganizations dynamically, with results filtered based on `portfolio_id`.
*
* Key workflow:
* 1. **Document Ready**: Ensures that the function runs only once the DOM is fully loaded.
* 2. **Check and Reinitialize Select2**:
* - If Select2 is already initialized, its destroyed to refresh with new options.
* - Select2 is reinitialized with AJAX settings for dynamic data fetching.
* 3. **AJAX Options**:
* - **Data Function**: Prepares the query by capturing the user's search term (`params.term`)
* and the provided `portfolio_id` to filter relevant suborganizations.
* - **Data Type**: Ensures responses are returned as JSON.
* - **Delay**: Introduces a 250ms delay to prevent excessive requests on fast typing.
* - **Cache**: Enables caching to improve performance.
* 4. **Theme and Placeholder**:
* - Sets the dropdown theme to admin-autocomplete for consistent styling.
* - Allows clearing of the dropdown and displays a placeholder as defined in the HTML.
*
* Dependencies:
* - Requires `suborganizationDropdown` element, the jQuery library, and the Select2 plugin.
* - `portfolio_id` is passed to filter results relevant to a specific portfolio.
*/
function updateSubOrganizationDropdown(portfolio_id) {
django.jQuery(document).ready(function() {
if (suborganizationDropdown.data('select2')) {
suborganizationDropdown.select2('destroy');
}
// Reinitialize Select2 with the updated URL
suborganizationDropdown.select2({
ajax: {
data: function (params) {
var query = {
search: params.term,
portfolio_id: portfolio_id
}
return query;
},
dataType: 'json',
delay: 250,
cache: true
},
theme: 'admin-autocomplete',
allowClear: true,
placeholder: suborganizationDropdown.attr('data-placeholder')
});
});
}
/**
* Updates the display of portfolio-related fields based on whether a portfolio is selected.
*
* This function controls the visibility of specific fields by showing or hiding them
* depending on the presence of a selected portfolio ID in the dropdown. When a portfolio
* is selected, certain fields are shown (like suborganizations and portfolio-related fields),
* while others are hidden (like senior official and other employee-related fields).
*
* Workflow:
* 1. **Retrieve Portfolio ID**:
* - Fetches the selected value from `portfolioDropdown` to check if a portfolio is selected.
*
* 2. **Display Fields for Selected Portfolio**:
* - If a `portfolio_id` exists, it updates the `suborganizationDropdown` for the specific portfolio.
* - Shows or hides various fields to display only relevant portfolio information:
* - Shows `suborganizationField`, `portfolioSeniorOfficialField`, and fields related to the portfolio organization.
* - Hides fields that are not applicable when a portfolio is selected, such as `seniorOfficialField` and `otherEmployeesField`.
*
* 3. **Display Fields for No Portfolio Selected**:
* - If no portfolio is selected (i.e., `portfolio_id` is falsy), it reverses the visibility:
* - Hides `suborganizationField` and other portfolio-specific fields.
* - Shows fields that are applicable when no portfolio is selected, such as the `seniorOfficialField`.
*
* Dependencies:
* - `portfolioDropdown` is assumed to be a dropdown element containing portfolio IDs.
* - `showElement` and `hideElement` utility functions are used to control element visibility.
* - Various global field elements (e.g., `suborganizationField`, `seniorOfficialField`, `portfolioOrgTypeFieldSet`) are used.
*/
function updatePortfolioFieldsDisplay() {
// Retrieve the selected portfolio ID
let portfolio_id = portfolioDropdown.val();
if (portfolio_id) {
// A portfolio is selected - update suborganization dropdown and show/hide relevant fields
// Update suborganization dropdown for the selected portfolio
updateSubOrganizationDropdown(portfolio_id);
// Show fields relevant to a selected portfolio
showElement(suborganizationField);
hideElement(seniorOfficialField);
showElement(portfolioSeniorOfficialField);
// Hide fields not applicable when a portfolio is selected
hideElement(otherEmployeesField);
hideElement(noOtherContactsRationaleField);
hideElement(cisaRepresentativeFirstNameField);
hideElement(cisaRepresentativeLastNameField);
hideElement(cisaRepresentativeEmailField);
hideElement(orgTypeFieldSet);
hideElement(orgTypeFieldSetDetails);
hideElement(orgNameFieldSet);
hideElement(orgNameFieldSetDetails);
// Show portfolio-specific fields
showElement(portfolioOrgTypeFieldSet);
showElement(portfolioOrgNameFieldSet);
showElement(portfolioOrgNameFieldSetDetails);
} else {
// No portfolio is selected - reverse visibility of fields
// Hide suborganization field as no portfolio is selected
hideElement(suborganizationField);
// Show fields that are relevant when no portfolio is selected
showElement(seniorOfficialField);
hideElement(portfolioSeniorOfficialField);
showElement(otherEmployeesField);
showElement(noOtherContactsRationaleField);
showElement(cisaRepresentativeFirstNameField);
showElement(cisaRepresentativeLastNameField);
showElement(cisaRepresentativeEmailField);
// Show organization type and name fields
showElement(orgTypeFieldSet);
showElement(orgTypeFieldSetDetails);
showElement(orgNameFieldSet);
showElement(orgNameFieldSetDetails);
// Hide portfolio-specific fields that arent applicable
hideElement(portfolioOrgTypeFieldSet);
hideElement(portfolioOrgNameFieldSet);
hideElement(portfolioOrgNameFieldSetDetails);
}
updateSuborganizationFieldsDisplay();
}
/**
* Updates the visibility of suborganization-related fields based on the selected value in the suborganization dropdown.
*
* If a suborganization is selected:
* - Hides the fields related to requesting a new suborganization (`requestedSuborganizationField`).
* - Hides the city (`suborganizationCity`) and state/territory (`suborganizationStateTerritory`) fields for the suborganization.
*
* If no suborganization is selected:
* - Shows the fields for requesting a new suborganization (`requestedSuborganizationField`).
* - Displays the city (`suborganizationCity`) and state/territory (`suborganizationStateTerritory`) fields.
*
* This function ensures the form dynamically reflects whether a specific suborganization is being selected or requested.
*/
function updateSuborganizationFieldsDisplay() {
let portfolio_id = portfolioDropdown.val();
let suborganization_id = suborganizationDropdown.val();
if (portfolio_id && !suborganization_id) {
// Show suborganization request fields
showElement(requestedSuborganizationField);
showElement(suborganizationCity);
showElement(suborganizationStateTerritory);
} else {
// Hide suborganization request fields if suborganization is selected
hideElement(requestedSuborganizationField);
hideElement(suborganizationCity);
hideElement(suborganizationStateTerritory);
}
}
/**
* Initializes necessary data and display configurations for the portfolio fields.
*/
function initializePortfolioSettings() {
// Update the visibility of portfolio-related fields based on current dropdown selection.
updatePortfolioFieldsDisplay();
// Dynamically adjust the display of certain fields based on the selected portfolio's characteristics.
updatePortfolioFieldsDataDynamicDisplay();
}
/**
* Sets event listeners for key UI elements.
*/
function setEventListeners() {
// When the `portfolioDropdown` selection changes, refresh the displayed portfolio fields.
portfolioDropdown.on("change", updatePortfolioFields);
// When the 'suborganizationDropdown' selection changes
suborganizationDropdown.on("change", updateSuborganizationFieldsDisplay);
}
// Run initial setup functions
initializePortfolioSettings();
setEventListeners();
}
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Initialization code.
@ -797,6 +1297,63 @@ document.addEventListener('DOMContentLoaded', function() {
customEmail.loadRejectedEmail()
});
/** An IIFE that hides and shows approved domain select2 row in domain request
* conditionally based on the Status field selection. If Approved, show. If not Approved,
* don't show.
*/
document.addEventListener('DOMContentLoaded', function() {
const domainRequestForm = document.getElementById("domainrequest_form");
if (!domainRequestForm) {
return;
}
const statusToCheck = "approved";
const statusSelect = document.getElementById("id_status");
const sessionVariableName = "showApprovedDomain";
let approvedDomainFormGroup = document.querySelector(".field-approved_domain");
function updateFormGroupVisibility(showFormGroups) {
if (showFormGroups) {
showElement(approvedDomainFormGroup);
}else {
hideElement(approvedDomainFormGroup);
}
}
// Handle showing/hiding the related fields on page load.
function initializeFormGroups() {
let isStatus = statusSelect.value == statusToCheck;
// Initial handling of these groups.
updateFormGroupVisibility(isStatus);
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
statusSelect.addEventListener('change', () => {
// Show the approved if the status is what we expect.
isStatus = statusSelect.value == statusToCheck;
updateFormGroupVisibility(isStatus);
addOrRemoveSessionBoolean(sessionVariableName, isStatus);
});
// Listen to Back/Forward button navigation and handle approvedDomainFormGroup display based on session storage
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
// accurately for this edge case, we use cache and test for the back/forward navigation.
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.type === "back_forward") {
let showTextAreaFormGroup = sessionStorage.getItem(sessionVariableName) !== null;
updateFormGroupVisibility(showTextAreaFormGroup);
}
});
});
observer.observe({ type: "navigation" });
}
initializeFormGroups();
});
/** An IIFE for copy summary button (appears in DomainRegistry models)
*/
@ -844,10 +1401,10 @@ document.addEventListener('DOMContentLoaded', function() {
if (contacts) {
contacts.forEach(contact => {
// Check if the <dl> element is not empty
const name = contact.querySelector('a#contact_info_name')?.innerText;
const title = contact.querySelector('span#contact_info_title')?.innerText;
const email = contact.querySelector('span#contact_info_email')?.innerText;
const phone = contact.querySelector('span#contact_info_phone')?.innerText;
const name = contact.querySelector('a.contact_info_name')?.innerText;
const title = contact.querySelector('span.contact_info_title')?.innerText;
const email = contact.querySelector('span.contact_info_email')?.innerText;
const phone = contact.querySelector('span.contact_info_phone')?.innerText;
const url = nameToUrlMap[name] || '#';
// Format the contact information
const listItem = document.createElement('li');
@ -898,9 +1455,9 @@ document.addEventListener('DOMContentLoaded', function() {
const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official');
const seniorOfficialElement = document.getElementById('id_senior_official');
const seniorOfficialName = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text;
const seniorOfficialTitle = extractTextById('contact_info_title', seniorOfficialDiv);
const seniorOfficialEmail = extractTextById('contact_info_email', seniorOfficialDiv);
const seniorOfficialPhone = extractTextById('contact_info_phone', seniorOfficialDiv);
const seniorOfficialTitle = seniorOfficialDiv.querySelector('.contact_info_title');
const seniorOfficialEmail = seniorOfficialDiv.querySelector('.contact_info_email');
const seniorOfficialPhone = seniorOfficialDiv.querySelector('.contact_info_phone');
let seniorOfficialInfo = `${seniorOfficialName}${seniorOfficialTitle}${seniorOfficialEmail}${seniorOfficialPhone}`;
const html_summary = `<strong>Recommendation:</strong></br>` +
@ -958,6 +1515,7 @@ document.addEventListener('DOMContentLoaded', function() {
/** An IIFE for dynamically changing some fields on the portfolio admin model
* IMPORTANT NOTE: The logic in this IIFE is paired handlePortfolioSelection
*/
(function dynamicPortfolioFields(){
@ -1184,9 +1742,9 @@ document.addEventListener('DOMContentLoaded', function() {
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");
const titleSpan = contactList.querySelector(".contact_info_title");
const emailSpan = contactList.querySelector(".contact_info_email");
const phoneSpan = contactList.querySelector(".contact_info_phone");
if (titleSpan) {
titleSpan.textContent = data.title || "None";
@ -1218,7 +1776,7 @@ document.addEventListener('DOMContentLoaded', function() {
(function dynamicDomainRequestFields(){
const domainRequestPage = document.getElementById("domainrequest_form");
if (domainRequestPage) {
handleSuborganizationFields();
handlePortfolioSelection();
}
})();

View file

@ -28,6 +28,8 @@ from registrar.views.domain_requests_json import get_domain_requests_json
from registrar.views.domains_json import get_domains_json
from registrar.views.utility.api_views import (
get_senior_official_from_federal_agency_json,
get_portfolio_json,
get_suborganization_list_json,
get_federal_and_portfolio_types_from_federal_agency_json,
get_action_needed_email_for_user_json,
get_rejection_email_for_user_json,
@ -201,6 +203,16 @@ urlpatterns = [
get_senior_official_from_federal_agency_json,
name="get-senior-official-from-federal-agency-json",
),
path(
"admin/api/get-portfolio-json/",
get_portfolio_json,
name="get-portfolio-json",
),
path(
"admin/api/get-suborganization-list-json/",
get_suborganization_list_json,
name="get-suborganization-list-json",
),
path(
"admin/api/get-federal-and-portfolio-types-from-federal-agency-json/",
get_federal_and_portfolio_types_from_federal_agency_json,

View file

@ -4,7 +4,25 @@
Template for an input field with a clipboard
{% endcomment %}
{% if not invisible_input_field %}
{% if empty_field %}
<div class="admin-icon-group">
<input aria-hidden="true" class="display-none" value="">
<button
class="usa-button--dja usa-button usa-button__small-text usa-button--unstyled padding-left-1 usa-button--icon copy-to-clipboard"
type="button"
>
<div class="no-outline-on-click">
<svg
class="usa-icon"
>
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
<!-- the span is targeted in JS, do not remove -->
<span>Copy</span>
</div>
</button>
</div>
{% elif not invisible_input_field %}
<div class="admin-icon-group">
{{ field }}
<button

View file

@ -2,6 +2,13 @@
{% load custom_filters %}
{% 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 %}
{# Create an invisible <a> tag so that we can use a click event to toggle the modal. #}
<a id="invisible-ineligible-modal-toggler" class="display-none" href="#toggle-set-ineligible" aria-controls="toggle-set-ineligible" data-open-modal></a>

View file

@ -6,7 +6,7 @@
{% if show_formatted_name %}
{% if user.get_formatted_name %}
<a id="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
<a class="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
{% else %}
None
{% endif %}
@ -16,7 +16,7 @@
{% if user|has_contact_info %}
{# Title #}
{% if user.title %}
<span id="contact_info_title">{{ user.title }}</span>
<span class="contact_info_title">{{ user.title }}</span>
{% else %}
None
{% endif %}
@ -24,7 +24,7 @@
{# Email #}
{% if user.email %}
<span id="contact_info_email">{{ user.email }}</span>
<span class="contact_info_email">{{ user.email }}</span>
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
<br>
{% else %}
@ -33,17 +33,24 @@
{# Phone #}
{% if user.phone %}
<span id="contact_info_phone">{{ user.phone }}</span>
<span class="contact_info_phone">{{ user.phone }}</span>
<br>
{% else %}
None<br>
{% endif %}
{% elif fields_always_present %}
<span class="contact_info_title"></span>
</br>
<span class="contact_info_email"></span>
{% include "admin/input_with_clipboard.html" with field=user empty_field=True %}
<br>
<span class="contact_info_phone"></span>
<br>
{% elif not hide_no_contact_info_message %}
No additional contact information found.<br>
{% endif %}
{% if user_verification_type and not skip_additional_contact_info %}
<span id="contact_info_phone">{{ user_verification_type }}</span>
<span class="contact_info_phone">{{ user_verification_type }}</span>
{% endif %}
</address>

View file

@ -66,6 +66,14 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
No changelog to display.
</div>
{% endif %}
{% elif field.field.name == "portfolio_senior_official" %}
<div class="readonly">
{% if original_object.portfolio.senior_official %}
<a href="{% url 'admin:registrar_seniorofficial_change' original_object.portfolio.senior_official.id %}">{{ field.contents }}</a>
{% else %}
No senior official found.<br>
{% endif %}
</div>
{% elif field.field.name == "other_contacts" %}
{% if all_contacts.count > 2 %}
<div class="readonly">
@ -332,6 +340,16 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
<label aria-label="Senior official contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.senior_official no_title_top_padding=field.is_readonly %}
</div>
{% elif field.field.name == "portfolio_senior_official" %}
<div class="flex-container">
<label aria-label="Senior official contact details"></label>
{% comment %}fields_always_present=True will shortcut the contact_detail_list template when
1. Senior official field should be hidden on domain request because no portfoloio is selected, which is desirable
2. A portfolio is selected but there is no senior official on the portfolio, where the shortcut is not desirable
To solve 2, we use an else No additional contact information found on field.field.name == "portfolio_senior_official"
and we hide the placeholders from detail_table_fieldset in JS{% endcomment %}
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.portfolio.senior_official no_title_top_padding=field.is_readonly fields_always_present=True %}
</div>
{% elif field.field.name == "other_contacts" and original_object.other_contacts.all %}
{% with all_contacts=original_object.other_contacts.all %}
{% if all_contacts.count > 2 %}

View file

@ -1526,7 +1526,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
# Test for the copy link
self.assertContains(response, "copy-to-clipboard", count=4)
self.assertContains(response, "copy-to-clipboard", count=5)
# Test that Creator counts display properly
self.assertNotContains(response, "Approved domains")
@ -1626,6 +1626,17 @@ class TestDomainRequestAdmin(MockEppLib):
readonly_fields = self.admin.get_readonly_fields(request, domain_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",
"current_websites",
"alternative_domains",
@ -1691,6 +1702,17 @@ class TestDomainRequestAdmin(MockEppLib):
readonly_fields = self.admin.get_readonly_fields(request)
self.maxDiff = None
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",
"current_websites",
"alternative_domains",
@ -1723,6 +1745,17 @@ class TestDomainRequestAdmin(MockEppLib):
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",
"current_websites",
"alternative_domains",

View file

@ -2,7 +2,8 @@ from django.urls import reverse
from django.test import TestCase, Client
from registrar.models import FederalAgency, SeniorOfficial, User, DomainRequest
from django.contrib.auth import get_user_model
from registrar.tests.common import create_superuser, create_user, completed_domain_request
from registrar.models.portfolio import Portfolio
from registrar.tests.common import create_superuser, create_test_user, create_user, completed_domain_request
from api.tests.common import less_console_noise_decorator
from registrar.utility.constants import BranchChoices
@ -74,6 +75,79 @@ class GetSeniorOfficialJsonTest(TestCase):
self.assertEqual(data["error"], "Senior Official not found")
class GetPortfolioJsonTest(TestCase):
def setUp(self):
self.client = Client()
self.user = create_test_user()
self.superuser = create_superuser()
self.analyst_user = create_user()
self.agency = FederalAgency.objects.create(agency="Test Agency")
self.senior_official = SeniorOfficial.objects.create(
first_name="John", last_name="Doe", title="Director", federal_agency=self.agency
)
self.portfolio = Portfolio.objects.create(
creator=self.user,
federal_agency=self.agency,
senior_official=self.senior_official,
organization_name="Org name",
organization_type=Portfolio.OrganizationChoices.FEDERAL,
)
self.api_url = reverse("get-portfolio-json")
def tearDown(self):
Portfolio.objects.all().delete()
User.objects.all().delete()
SeniorOfficial.objects.all().delete()
FederalAgency.objects.all().delete()
@less_console_noise_decorator
def test_get_portfolio_authenticated_superuser(self):
"""Test that a superuser can get the portfolio information."""
self.client.force_login(self.superuser)
response = self.client.get(self.api_url, {"id": self.portfolio.id})
self.assertEqual(response.status_code, 200)
portfolio = response.json()
self.assertEqual(portfolio["id"], self.portfolio.id)
self.assertEqual(portfolio["creator"], self.user.id)
self.assertEqual(portfolio["organization_name"], self.portfolio.organization_name)
self.assertEqual(portfolio["organization_type"], "Federal")
self.assertEqual(portfolio["notes"], None)
self.assertEqual(portfolio["federal_agency"]["id"], self.agency.id)
self.assertEqual(portfolio["federal_agency"]["agency"], self.agency.agency)
self.assertEqual(portfolio["senior_official"]["id"], self.senior_official.id)
self.assertEqual(portfolio["senior_official"]["first_name"], self.senior_official.first_name)
self.assertEqual(portfolio["senior_official"]["last_name"], self.senior_official.last_name)
self.assertEqual(portfolio["senior_official"]["title"], self.senior_official.title)
self.assertEqual(portfolio["senior_official"]["phone"], None)
self.assertEqual(portfolio["senior_official"]["email"], None)
self.assertEqual(portfolio["federal_type"], "-")
@less_console_noise_decorator
def test_get_portfolio_json_authenticated_analyst(self):
"""Test that an analyst user can fetch the portfolio's information."""
self.client.force_login(self.analyst_user)
response = self.client.get(self.api_url, {"id": self.portfolio.id})
self.assertEqual(response.status_code, 200)
portfolio = response.json()
self.assertEqual(portfolio["id"], self.portfolio.id)
@less_console_noise_decorator
def test_get_portfolio_json_unauthenticated(self):
"""Test that an unauthenticated user receives a 403 with an error message."""
self.client.force_login(self.user)
response = self.client.get(self.api_url, {"id": self.portfolio.id})
self.assertEqual(response.status_code, 302)
@less_console_noise_decorator
def test_get_portfolio_json_not_found(self):
"""Test that a request for a non-existent portfolio returns a 404 with an error message."""
self.client.force_login(self.superuser)
response = self.client.get(self.api_url, {"id": -1})
self.assertEqual(response.status_code, 404)
class GetFederalPortfolioTypeJsonTest(TestCase):
def setUp(self):
self.client = Client()

View file

@ -4,6 +4,7 @@ from django.utils.html import format_html
from django.urls import reverse
from django.utils.html import escape
from registrar.models.utility.generic_helper import value_of_attribute
from django.contrib.admin.widgets import AutocompleteSelect
def get_action_needed_reason_default_email(domain_request, action_needed_reason):
@ -94,3 +95,26 @@ def get_field_links_as_list(
else:
links = "".join(links)
return format_html(f'<ul class="add-list-reset">{links}</ul>') if links else msg_for_none
class AutocompleteSelectWithPlaceholder(AutocompleteSelect):
"""Override of the default autoselect element. This is because by default,
the autocomplete element clears data-placeholder"""
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
if "data-placeholder" in base_attrs:
attrs["data-placeholder"] = base_attrs["data-placeholder"]
return attrs
def __init__(self, field, admin_site, attrs=None, choices=(), using=None):
"""Set a custom ajax url for the select2 if passed through attrs"""
if attrs:
self.custom_ajax_url = attrs.pop("ajax-url", None)
super().__init__(field, admin_site, attrs, choices, using)
def get_url(self):
"""Override the get_url method to use the custom ajax url"""
if self.custom_ajax_url:
return reverse(self.custom_ajax_url)
return reverse(self.url_name % self.admin_site.name)

View file

@ -39,6 +39,86 @@ def get_senior_official_from_federal_agency_json(request):
return JsonResponse({"error": "Senior Official not found"}, status=404)
@login_required
@staff_member_required
def get_portfolio_json(request):
"""Returns portfolio information as a JSON"""
# This API is only accessible to admins and analysts
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
portfolio_id = request.GET.get("id")
try:
portfolio = Portfolio.objects.get(id=portfolio_id)
except Portfolio.DoesNotExist:
return JsonResponse({"error": "Portfolio not found"}, status=404)
# Convert the portfolio to a dictionary
portfolio_dict = model_to_dict(portfolio)
portfolio_dict["id"] = portfolio.id
# map portfolio federal type
portfolio_dict["federal_type"] = (
BranchChoices.get_branch_label(portfolio.federal_type) if portfolio.federal_type else "-"
)
# map portfolio organization type
portfolio_dict["organization_type"] = (
DomainRequest.OrganizationChoices.get_org_label(portfolio.organization_type)
if portfolio.organization_type
else "-"
)
# Add senior official information if it exists
if portfolio.senior_official:
senior_official = model_to_dict(
portfolio.senior_official, fields=["id", "first_name", "last_name", "title", "phone", "email"]
)
# The phone number field isn't json serializable, so we
# convert this to a string first if it exists.
if "phone" in senior_official and senior_official.get("phone"):
senior_official["phone"] = str(senior_official["phone"])
portfolio_dict["senior_official"] = senior_official
else:
portfolio_dict["senior_official"] = None
# Add federal agency information if it exists
if portfolio.federal_agency:
federal_agency = model_to_dict(portfolio.federal_agency, fields=["agency", "id"])
portfolio_dict["federal_agency"] = federal_agency
else:
portfolio_dict["federal_agency"] = "-"
return JsonResponse(portfolio_dict)
@login_required
@staff_member_required
def get_suborganization_list_json(request):
"""Returns suborganization list information for a portfolio as a JSON"""
# This API is only accessible to admins and analysts
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
portfolio_id = request.GET.get("portfolio_id")
try:
portfolio = Portfolio.objects.get(id=portfolio_id)
except Portfolio.DoesNotExist:
return JsonResponse({"error": "Portfolio not found"}, status=404)
# Add suborganizations related to this portfolio
suborganizations = portfolio.portfolio_suborganizations.all().values("id", "name")
results = [{"id": sub["id"], "text": sub["name"]} for sub in suborganizations]
return JsonResponse({"results": results, "pagination": {"more": False}})
@login_required
@staff_member_required
def get_federal_and_portfolio_types_from_federal_agency_json(request):