mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-01 15:34:53 +02:00
Merge pull request #3040 from cisagov/dk/2934-dja-domain-request
#2934: Django Admin - Dynamic Domain Request edit page
This commit is contained in:
commit
68dc4942a8
12 changed files with 992 additions and 29 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -171,6 +171,9 @@ node_modules
|
|||
# Vim
|
||||
*.swp
|
||||
|
||||
# VS Code
|
||||
.vscode
|
||||
|
||||
# Compliance/trestle related
|
||||
docs/compliance/.trestle/cache
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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, it’s 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 aren’t 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();
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue