mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 18:56:15 +02:00
Merge remote-tracking branch 'origin/main' into nl/3032-updates-to-analyst-org-domain-request
This commit is contained in:
commit
20acec9b6b
15 changed files with 840 additions and 324 deletions
|
@ -20,7 +20,7 @@ applications:
|
||||||
# Tell Django where it is being hosted
|
# Tell Django where it is being hosted
|
||||||
DJANGO_BASE_URL: https://getgov-ms.app.cloud.gov
|
DJANGO_BASE_URL: https://getgov-ms.app.cloud.gov
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: DEBUG
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
|
|
|
@ -62,9 +62,11 @@ class RegistryError(Exception):
|
||||||
- 2501 - 2502 Something malicious or abusive may have occurred
|
- 2501 - 2502 Something malicious or abusive may have occurred
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, code=None, **kwargs):
|
def __init__(self, *args, code=None, note="", **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.code = code
|
self.code = code
|
||||||
|
# note is a string that can be used to provide additional context
|
||||||
|
self.note = note
|
||||||
|
|
||||||
def should_retry(self):
|
def should_retry(self):
|
||||||
return self.code == ErrorCode.COMMAND_FAILED
|
return self.code == ErrorCode.COMMAND_FAILED
|
||||||
|
|
|
@ -220,6 +220,14 @@ class DomainInformationAdminForm(forms.ModelForm):
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
widgets = {
|
widgets = {
|
||||||
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
||||||
|
"portfolio": AutocompleteSelectWithPlaceholder(
|
||||||
|
DomainInformation._meta.get_field("portfolio"), admin.site, attrs={"data-placeholder": "---------"}
|
||||||
|
),
|
||||||
|
"sub_organization": AutocompleteSelectWithPlaceholder(
|
||||||
|
DomainInformation._meta.get_field("sub_organization"),
|
||||||
|
admin.site,
|
||||||
|
attrs={"data-placeholder": "---------", "ajax-url": "get-suborganization-list-json"},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -231,6 +239,14 @@ class DomainInformationInlineForm(forms.ModelForm):
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
widgets = {
|
widgets = {
|
||||||
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
||||||
|
"portfolio": AutocompleteSelectWithPlaceholder(
|
||||||
|
DomainInformation._meta.get_field("portfolio"), admin.site, attrs={"data-placeholder": "---------"}
|
||||||
|
),
|
||||||
|
"sub_organization": AutocompleteSelectWithPlaceholder(
|
||||||
|
DomainInformation._meta.get_field("sub_organization"),
|
||||||
|
admin.site,
|
||||||
|
attrs={"data-placeholder": "---------", "ajax-url": "get-suborganization-list-json"},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1523,6 +1539,70 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
orderable_fk_fields = [("domain", "name")]
|
orderable_fk_fields = [("domain", "name")]
|
||||||
|
|
||||||
|
# Define methods to display fields from the related portfolio
|
||||||
|
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
|
||||||
|
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
|
||||||
|
|
||||||
|
portfolio_senior_official.short_description = "Senior official" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_organization_type(self, obj):
|
||||||
|
return (
|
||||||
|
DomainRequest.OrganizationChoices.get_org_label(obj.portfolio.organization_type)
|
||||||
|
if obj.portfolio and obj.portfolio.organization_type
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio_organization_type.short_description = "Organization type" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_federal_type(self, obj):
|
||||||
|
return (
|
||||||
|
BranchChoices.get_branch_label(obj.portfolio.federal_type)
|
||||||
|
if obj.portfolio and obj.portfolio.federal_type
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio_federal_type.short_description = "Federal type" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_organization_name(self, obj):
|
||||||
|
return obj.portfolio.organization_name if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_organization_name.short_description = "Organization name" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_federal_agency(self, obj):
|
||||||
|
return obj.portfolio.federal_agency if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_federal_agency.short_description = "Federal agency" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_state_territory(self, obj):
|
||||||
|
return obj.portfolio.state_territory if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_state_territory.short_description = "State, territory, or military post" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_address_line1(self, obj):
|
||||||
|
return obj.portfolio.address_line1 if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_address_line1.short_description = "Address line 1" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_address_line2(self, obj):
|
||||||
|
return obj.portfolio.address_line2 if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_address_line2.short_description = "Address line 2" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_city(self, obj):
|
||||||
|
return obj.portfolio.city if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_city.short_description = "City" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_zipcode(self, obj):
|
||||||
|
return obj.portfolio.zipcode if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_zipcode.short_description = "Zip code" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_urbanization(self, obj):
|
||||||
|
return obj.portfolio.urbanization if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_urbanization.short_description = "Urbanization" # type: ignore
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = [GenericOrgFilter]
|
list_filter = [GenericOrgFilter]
|
||||||
|
|
||||||
|
@ -1537,16 +1617,36 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
"portfolio",
|
|
||||||
"sub_organization",
|
|
||||||
"creator",
|
|
||||||
"domain_request",
|
"domain_request",
|
||||||
"notes",
|
"notes",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Requested by",
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
|
"creator",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
(".gov domain", {"fields": ["domain"]}),
|
(".gov domain", {"fields": ["domain"]}),
|
||||||
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
|
(
|
||||||
|
"Contacts",
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
"senior_official",
|
||||||
|
"portfolio_senior_official",
|
||||||
|
"other_contacts",
|
||||||
|
"no_other_contacts_rationale",
|
||||||
|
"cisa_representative_first_name",
|
||||||
|
"cisa_representative_last_name",
|
||||||
|
"cisa_representative_email",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
("Background info", {"fields": ["anything_else"]}),
|
("Background info", {"fields": ["anything_else"]}),
|
||||||
(
|
(
|
||||||
"Type of organization",
|
"Type of organization",
|
||||||
|
@ -1595,10 +1695,58 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
# the below three sections are for portfolio fields
|
||||||
|
(
|
||||||
|
"Type of organization",
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
"portfolio_organization_type",
|
||||||
|
"portfolio_federal_type",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Organization name and mailing address",
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
"portfolio_organization_name",
|
||||||
|
"portfolio_federal_agency",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Show details",
|
||||||
|
{
|
||||||
|
"classes": ["collapse--dgfieldset"],
|
||||||
|
"description": "Extends organization name and mailing address",
|
||||||
|
"fields": [
|
||||||
|
"portfolio_state_territory",
|
||||||
|
"portfolio_address_line1",
|
||||||
|
"portfolio_address_line2",
|
||||||
|
"portfolio_city",
|
||||||
|
"portfolio_zipcode",
|
||||||
|
"portfolio_urbanization",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Readonly fields for analysts and superusers
|
# Readonly fields for analysts and superusers
|
||||||
readonly_fields = ("other_contacts", "is_election_board")
|
readonly_fields = (
|
||||||
|
"portfolio_senior_official",
|
||||||
|
"portfolio_organization_type",
|
||||||
|
"portfolio_federal_type",
|
||||||
|
"portfolio_organization_name",
|
||||||
|
"portfolio_federal_agency",
|
||||||
|
"portfolio_state_territory",
|
||||||
|
"portfolio_address_line1",
|
||||||
|
"portfolio_address_line2",
|
||||||
|
"portfolio_city",
|
||||||
|
"portfolio_zipcode",
|
||||||
|
"portfolio_urbanization",
|
||||||
|
"other_contacts",
|
||||||
|
"is_election_board",
|
||||||
|
)
|
||||||
|
|
||||||
# Read only that we'll leverage for CISA Analysts
|
# Read only that we'll leverage for CISA Analysts
|
||||||
analyst_readonly_fields = [
|
analyst_readonly_fields = [
|
||||||
|
@ -2649,7 +2797,72 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
template = "django/admin/includes/domain_info_inline_stacked.html"
|
template = "django/admin/includes/domain_info_inline_stacked.html"
|
||||||
model = models.DomainInformation
|
model = models.DomainInformation
|
||||||
|
|
||||||
|
# Define methods to display fields from the related portfolio
|
||||||
|
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
|
||||||
|
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
|
||||||
|
|
||||||
|
portfolio_senior_official.short_description = "Senior official" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_organization_type(self, obj):
|
||||||
|
return (
|
||||||
|
DomainRequest.OrganizationChoices.get_org_label(obj.portfolio.organization_type)
|
||||||
|
if obj.portfolio and obj.portfolio.organization_type
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio_organization_type.short_description = "Organization type" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_federal_type(self, obj):
|
||||||
|
return (
|
||||||
|
BranchChoices.get_branch_label(obj.portfolio.federal_type)
|
||||||
|
if obj.portfolio and obj.portfolio.federal_type
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio_federal_type.short_description = "Federal type" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_organization_name(self, obj):
|
||||||
|
return obj.portfolio.organization_name if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_organization_name.short_description = "Organization name" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_federal_agency(self, obj):
|
||||||
|
return obj.portfolio.federal_agency if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_federal_agency.short_description = "Federal agency" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_state_territory(self, obj):
|
||||||
|
return obj.portfolio.state_territory if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_state_territory.short_description = "State, territory, or military post" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_address_line1(self, obj):
|
||||||
|
return obj.portfolio.address_line1 if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_address_line1.short_description = "Address line 1" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_address_line2(self, obj):
|
||||||
|
return obj.portfolio.address_line2 if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_address_line2.short_description = "Address line 2" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_city(self, obj):
|
||||||
|
return obj.portfolio.city if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_city.short_description = "City" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_zipcode(self, obj):
|
||||||
|
return obj.portfolio.zipcode if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_zipcode.short_description = "Zip code" # type: ignore
|
||||||
|
|
||||||
|
def portfolio_urbanization(self, obj):
|
||||||
|
return obj.portfolio.urbanization if obj.portfolio else ""
|
||||||
|
|
||||||
|
portfolio_urbanization.short_description = "Urbanization" # type: ignore
|
||||||
|
|
||||||
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
|
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
|
||||||
|
readonly_fields = copy.deepcopy(DomainInformationAdmin.readonly_fields)
|
||||||
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
|
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
|
||||||
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
|
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
|
||||||
|
|
||||||
|
@ -3195,7 +3408,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
except RegistryError as err:
|
except RegistryError as err:
|
||||||
# Using variables to get past the linter
|
# Using variables to get past the linter
|
||||||
message1 = f"Cannot delete Domain when in state {obj.state}"
|
message1 = f"Cannot delete Domain when in state {obj.state}"
|
||||||
message2 = "This subdomain is being used as a hostname on another domain"
|
message2 = f"This subdomain is being used as a hostname on another domain: {err.note}"
|
||||||
# Human-readable mappings of ErrorCodes. Can be expanded.
|
# Human-readable mappings of ErrorCodes. Can be expanded.
|
||||||
error_messages = {
|
error_messages = {
|
||||||
# noqa on these items as black wants to reformat to an invalid length
|
# noqa on these items as black wants to reformat to an invalid length
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that appends target="_blank" to the domain_form buttons
|
* A function that appends target="_blank" to the domain_form buttons
|
||||||
*/
|
*/
|
||||||
|
@ -28,3 +30,14 @@ export function initDomainFormTargetBlankButtons() {
|
||||||
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
|
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function for dynamic Domain fields
|
||||||
|
*/
|
||||||
|
export function initDynamicDomainFields(){
|
||||||
|
const domainPage = document.getElementById("domain_form");
|
||||||
|
if (domainPage) {
|
||||||
|
handlePortfolioSelection("#id_domain_info-0-portfolio",
|
||||||
|
"#id_domain_info-0-sub_organization");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { handleSuborganizationFields } from './helpers-portfolio-dynamic-fields.js';
|
import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function for dynamic DomainInformation fields
|
* A function for dynamic DomainInformation fields
|
||||||
|
@ -6,12 +6,7 @@ import { handleSuborganizationFields } from './helpers-portfolio-dynamic-fields.
|
||||||
export function initDynamicDomainInformationFields(){
|
export function initDynamicDomainInformationFields(){
|
||||||
const domainInformationPage = document.getElementById("domaininformation_form");
|
const domainInformationPage = document.getElementById("domaininformation_form");
|
||||||
if (domainInformationPage) {
|
if (domainInformationPage) {
|
||||||
handleSuborganizationFields();
|
console.log("handling domain information page");
|
||||||
}
|
handlePortfolioSelection();
|
||||||
|
|
||||||
// DomainInformation is embedded inside domain so this should fire there too
|
|
||||||
const domainPage = document.getElementById("domain_form");
|
|
||||||
if (domainPage) {
|
|
||||||
handleSuborganizationFields(portfolioDropdownSelector="#id_domain_info-0-portfolio", suborgDropdownSelector="#id_domain_info-0-sub_organization");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,19 @@
|
||||||
import { hideElement, showElement } from './helpers-admin.js';
|
import { hideElement, showElement } from './helpers-admin.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function that handles business logic for the suborganization field.
|
|
||||||
* Can be used anywhere the suborganization dropdown exists
|
|
||||||
*/
|
|
||||||
export function handleSuborganizationFields(
|
|
||||||
portfolioDropdownSelector="#id_portfolio",
|
|
||||||
suborgDropdownSelector="#id_sub_organization",
|
|
||||||
requestedSuborgFieldSelector=".field-requested_suborganization",
|
|
||||||
suborgCitySelector=".field-suborganization_city",
|
|
||||||
suborgStateTerritorySelector=".field-suborganization_state_territory"
|
|
||||||
) {
|
|
||||||
// These dropdown are select2 fields so they must be interacted with via jquery
|
|
||||||
const portfolioDropdown = django.jQuery(portfolioDropdownSelector)
|
|
||||||
const suborganizationDropdown = django.jQuery(suborgDropdownSelector)
|
|
||||||
const requestedSuborgField = document.querySelector(requestedSuborgFieldSelector);
|
|
||||||
const suborgCity = document.querySelector(suborgCitySelector);
|
|
||||||
const suborgStateTerritory = document.querySelector(suborgStateTerritorySelector);
|
|
||||||
if (!suborganizationDropdown || !requestedSuborgField || !suborgCity || !suborgStateTerritory) {
|
|
||||||
console.error("Requested suborg fields not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSuborganizationFields() {
|
|
||||||
if (portfolioDropdown.val() && !suborganizationDropdown.val()) {
|
|
||||||
showElement(requestedSuborgField);
|
|
||||||
showElement(suborgCity);
|
|
||||||
showElement(suborgStateTerritory);
|
|
||||||
}else {
|
|
||||||
hideElement(requestedSuborgField);
|
|
||||||
hideElement(suborgCity);
|
|
||||||
hideElement(suborgStateTerritory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the function once on page startup, then attach an event listener
|
|
||||||
toggleSuborganizationFields();
|
|
||||||
suborganizationDropdown.on("change", toggleSuborganizationFields);
|
|
||||||
portfolioDropdown.on("change", toggleSuborganizationFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* This function handles the portfolio selection as well as display of
|
* This function handles the portfolio selection as well as display of
|
||||||
* portfolio-related fields in the DomainRequest Form.
|
* portfolio-related fields in the DomainRequest Form.
|
||||||
*
|
*
|
||||||
* IMPORTANT NOTE: The logic in this method is paired dynamicPortfolioFields
|
* IMPORTANT NOTE: The business logic in this method is based on dynamicPortfolioFields
|
||||||
*/
|
*/
|
||||||
export function handlePortfolioSelection() {
|
export function handlePortfolioSelection(
|
||||||
|
portfolioDropdownSelector="#id_portfolio",
|
||||||
|
suborgDropdownSelector="#id_sub_organization"
|
||||||
|
) {
|
||||||
// These dropdown are select2 fields so they must be interacted with via jquery
|
// These dropdown are select2 fields so they must be interacted with via jquery
|
||||||
const portfolioDropdown = django.jQuery("#id_portfolio");
|
const portfolioDropdown = django.jQuery(portfolioDropdownSelector);
|
||||||
const suborganizationDropdown = django.jQuery("#id_sub_organization");
|
const suborganizationDropdown = django.jQuery(suborgDropdownSelector);
|
||||||
const suborganizationField = document.querySelector(".field-sub_organization");
|
const suborganizationField = document.querySelector(".field-sub_organization");
|
||||||
const requestedSuborganizationField = document.querySelector(".field-requested_suborganization");
|
const requestedSuborganizationField = document.querySelector(".field-requested_suborganization");
|
||||||
const suborganizationCity = document.querySelector(".field-suborganization_city");
|
const suborganizationCity = document.querySelector(".field-suborganization_city");
|
||||||
|
@ -440,8 +402,8 @@ export function handlePortfolioSelection() {
|
||||||
showElement(portfolioSeniorOfficialField);
|
showElement(portfolioSeniorOfficialField);
|
||||||
|
|
||||||
// Hide fields not applicable when a portfolio is selected
|
// Hide fields not applicable when a portfolio is selected
|
||||||
hideElement(otherEmployeesField);
|
if (otherEmployeesField) hideElement(otherEmployeesField);
|
||||||
hideElement(noOtherContactsRationaleField);
|
if (noOtherContactsRationaleField) hideElement(noOtherContactsRationaleField);
|
||||||
hideElement(cisaRepresentativeFirstNameField);
|
hideElement(cisaRepresentativeFirstNameField);
|
||||||
hideElement(cisaRepresentativeLastNameField);
|
hideElement(cisaRepresentativeLastNameField);
|
||||||
hideElement(cisaRepresentativeEmailField);
|
hideElement(cisaRepresentativeEmailField);
|
||||||
|
@ -463,8 +425,8 @@ export function handlePortfolioSelection() {
|
||||||
// Show fields that are relevant when no portfolio is selected
|
// Show fields that are relevant when no portfolio is selected
|
||||||
showElement(seniorOfficialField);
|
showElement(seniorOfficialField);
|
||||||
hideElement(portfolioSeniorOfficialField);
|
hideElement(portfolioSeniorOfficialField);
|
||||||
showElement(otherEmployeesField);
|
if (otherEmployeesField) showElement(otherEmployeesField);
|
||||||
showElement(noOtherContactsRationaleField);
|
if (noOtherContactsRationaleField) showElement(noOtherContactsRationaleField);
|
||||||
showElement(cisaRepresentativeFirstNameField);
|
showElement(cisaRepresentativeFirstNameField);
|
||||||
showElement(cisaRepresentativeLastNameField);
|
showElement(cisaRepresentativeLastNameField);
|
||||||
showElement(cisaRepresentativeEmailField);
|
showElement(cisaRepresentativeEmailField);
|
||||||
|
@ -504,14 +466,14 @@ export function handlePortfolioSelection() {
|
||||||
|
|
||||||
if (portfolio_id && !suborganization_id) {
|
if (portfolio_id && !suborganization_id) {
|
||||||
// Show suborganization request fields
|
// Show suborganization request fields
|
||||||
showElement(requestedSuborganizationField);
|
if (requestedSuborganizationField) showElement(requestedSuborganizationField);
|
||||||
showElement(suborganizationCity);
|
if (suborganizationCity) showElement(suborganizationCity);
|
||||||
showElement(suborganizationStateTerritory);
|
if (suborganizationStateTerritory) showElement(suborganizationStateTerritory);
|
||||||
} else {
|
} else {
|
||||||
// Hide suborganization request fields if suborganization is selected
|
// Hide suborganization request fields if suborganization is selected
|
||||||
hideElement(requestedSuborganizationField);
|
if (requestedSuborganizationField) hideElement(requestedSuborganizationField);
|
||||||
hideElement(suborganizationCity);
|
if (suborganizationCity) hideElement(suborganizationCity);
|
||||||
hideElement(suborganizationStateTerritory);
|
if (suborganizationStateTerritory) hideElement(suborganizationStateTerritory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
import { initDomainFormTargetBlankButtons } from './domain-form.js';
|
import { initDomainFormTargetBlankButtons } from './domain-form.js';
|
||||||
import { initDynamicPortfolioFields } from './portfolio-form.js';
|
import { initDynamicPortfolioFields } from './portfolio-form.js';
|
||||||
import { initDynamicDomainInformationFields } from './domain-information-form.js';
|
import { initDynamicDomainInformationFields } from './domain-information-form.js';
|
||||||
|
import { initDynamicDomainFields } from './domain-form.js';
|
||||||
|
|
||||||
// General
|
// General
|
||||||
initModals();
|
initModals();
|
||||||
|
@ -33,6 +34,7 @@ initDynamicDomainRequestFields();
|
||||||
|
|
||||||
// Domain
|
// Domain
|
||||||
initDomainFormTargetBlankButtons();
|
initDomainFormTargetBlankButtons();
|
||||||
|
initDynamicDomainFields();
|
||||||
|
|
||||||
// Portfolio
|
// Portfolio
|
||||||
initDynamicPortfolioFields();
|
initDynamicPortfolioFields();
|
||||||
|
|
|
@ -2,203 +2,212 @@ import { hideElement, showElement } from './helpers-admin.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function for dynamically changing some fields on the portfolio admin model
|
* A function for dynamically changing some fields on the portfolio admin model
|
||||||
* IMPORTANT NOTE: The logic in this function is paired handlePortfolioSelection and should be refactored once we solidify our requirements.
|
* IMPORTANT NOTE: The business logic in this function is related to handlePortfolioSelection
|
||||||
*/
|
*/
|
||||||
export function initDynamicPortfolioFields(){
|
function handlePortfolioFields(){
|
||||||
|
|
||||||
// the federal agency change listener fires on page load, which we don't want.
|
let isPageLoading = true
|
||||||
var isInitialPageLoad = true
|
// $ symbolically denotes that this is using jQuery
|
||||||
|
const $seniorOfficialDropdown = django.jQuery("#id_senior_official");
|
||||||
|
const seniorOfficialField = document.querySelector(".field-senior_official");
|
||||||
|
const seniorOfficialAddress = seniorOfficialField.querySelector(".dja-address-contact-list");
|
||||||
|
const seniorOfficialReadonly = seniorOfficialField.querySelector(".readonly");
|
||||||
|
const $federalAgencyDropdown = django.jQuery("#id_federal_agency");
|
||||||
|
const federalAgencyField = document.querySelector(".field-federal_agency");
|
||||||
|
const organizationTypeField = document.querySelector(".field-organization_type");
|
||||||
|
const organizationTypeReadonly = organizationTypeField.querySelector(".readonly");
|
||||||
|
const organizationTypeDropdown = document.getElementById("id_organization_type");
|
||||||
|
const organizationNameField = document.querySelector(".field-organization_name");
|
||||||
|
const federalTypeField = document.querySelector(".field-federal_type");
|
||||||
|
const urbanizationField = document.querySelector(".field-urbanization");
|
||||||
|
const stateTerritoryDropdown = document.getElementById("id_state_territory");
|
||||||
|
const seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
|
||||||
|
const seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
|
||||||
|
const federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
|
||||||
|
|
||||||
// This is the additional information that exists beneath the SO element.
|
/**
|
||||||
var contactList = document.querySelector(".field-senior_official .dja-address-contact-list");
|
* Fetches federal type data based on a selected agency using an AJAX call.
|
||||||
const federalAgencyContainer = document.querySelector(".field-federal_agency");
|
*
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
* @param {string} agency
|
||||||
|
* @returns {Promise<Object|null>} - A promise that resolves to the portfolio data object if successful,
|
||||||
let isPortfolioPage = document.getElementById("portfolio_form");
|
* or null if there was an error.
|
||||||
if (!isPortfolioPage) {
|
*/
|
||||||
return;
|
function getFederalTypeFromAgency(agency) {
|
||||||
}
|
return fetch(`${federalPortfolioApi}?&agency_name=${agency}`)
|
||||||
|
.then(response => {
|
||||||
// $ symbolically denotes that this is using jQuery
|
const statusCode = response.status;
|
||||||
let $federalAgency = django.jQuery("#id_federal_agency");
|
return response.json().then(data => ({ statusCode, data }));
|
||||||
let organizationType = document.getElementById("id_organization_type");
|
})
|
||||||
let readonlyOrganizationType = document.querySelector(".field-organization_type .readonly");
|
.then(({ statusCode, data }) => {
|
||||||
|
if (data.error) {
|
||||||
let organizationNameContainer = document.querySelector(".field-organization_name");
|
console.error("Error in AJAX call: " + data.error);
|
||||||
let federalType = document.querySelector(".field-federal_type");
|
return;
|
||||||
|
}
|
||||||
if ($federalAgency && (organizationType || readonlyOrganizationType)) {
|
return data.federal_type
|
||||||
// Attach the change event listener
|
})
|
||||||
$federalAgency.on("change", function() {
|
.catch(error => {
|
||||||
handleFederalAgencyChange($federalAgency, organizationType, readonlyOrganizationType, organizationNameContainer, federalType);
|
console.error("Error fetching federal and portfolio types: ", error);
|
||||||
|
return null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle dynamically hiding the urbanization field
|
|
||||||
let urbanizationField = document.querySelector(".field-urbanization");
|
|
||||||
let stateTerritory = document.getElementById("id_state_territory");
|
|
||||||
if (urbanizationField && stateTerritory) {
|
|
||||||
// Execute this function once on load
|
|
||||||
handleStateTerritoryChange(stateTerritory, urbanizationField);
|
|
||||||
|
|
||||||
// Attach the change event listener for state/territory
|
/**
|
||||||
stateTerritory.addEventListener("change", function() {
|
* Fetches senior official contact data based on a selected agency using an AJAX call.
|
||||||
handleStateTerritoryChange(stateTerritory, urbanizationField);
|
*
|
||||||
|
* @param {string} agency
|
||||||
|
* @returns {Promise<Object|null>} - A promise that resolves to the portfolio data object if successful,
|
||||||
|
* or null if there was an error.
|
||||||
|
*/
|
||||||
|
function getSeniorOfficialFromAgency(agency) {
|
||||||
|
return fetch(`${seniorOfficialApi}?agency_name=${agency}`)
|
||||||
|
.then(response => {
|
||||||
|
const statusCode = response.status;
|
||||||
|
return response.json().then(data => ({ statusCode, data }));
|
||||||
|
})
|
||||||
|
.then(({ statusCode, data }) => {
|
||||||
|
if (data.error) {
|
||||||
|
// Throw an error with status code and message
|
||||||
|
throw { statusCode, message: data.error };
|
||||||
|
} else {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching senior official: ", error);
|
||||||
|
throw error; // Re-throw for external handling
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle hiding the organization name field when the organization_type is federal.
|
/**
|
||||||
// Run this first one page load, then secondly on a change event.
|
* Handles the side effects of change on the organization type field
|
||||||
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
|
*
|
||||||
organizationType.addEventListener("change", function() {
|
* 1. If selection is federal, hide org name, show federal agency, show federal type if applicable
|
||||||
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
|
* 2. else show org name, hide federal agency, hide federal type if applicable
|
||||||
});
|
*/
|
||||||
});
|
function handleOrganizationTypeChange() {
|
||||||
|
if (organizationTypeDropdown && organizationNameField) {
|
||||||
function handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType) {
|
let selectedValue = organizationTypeDropdown.value;
|
||||||
if (organizationType && organizationNameContainer) {
|
|
||||||
let selectedValue = organizationType.value;
|
|
||||||
if (selectedValue === "federal") {
|
if (selectedValue === "federal") {
|
||||||
hideElement(organizationNameContainer);
|
hideElement(organizationNameField);
|
||||||
showElement(federalAgencyContainer);
|
showElement(federalAgencyField);
|
||||||
if (federalType) {
|
if (federalTypeField) {
|
||||||
showElement(federalType);
|
showElement(federalTypeField);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showElement(organizationNameContainer);
|
showElement(organizationNameField);
|
||||||
hideElement(federalAgencyContainer);
|
hideElement(federalAgencyField);
|
||||||
if (federalType) {
|
if (federalTypeField) {
|
||||||
hideElement(federalType);
|
hideElement(federalTypeField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFederalAgencyChange(federalAgency, organizationType, readonlyOrganizationType, organizationNameContainer, federalType) {
|
/**
|
||||||
// Don't do anything on page load
|
* Handles the side effects of change on the federal agency field
|
||||||
if (isInitialPageLoad) {
|
*
|
||||||
isInitialPageLoad = false;
|
* 1. handle org type dropdown or readonly
|
||||||
return;
|
* 2. call handleOrganizationTypeChange
|
||||||
}
|
* 3. call getFederalTypeFromAgency and update federal type
|
||||||
|
* 4. call getSeniorOfficialFromAgency and update the SO fieldset
|
||||||
|
*/
|
||||||
|
function handleFederalAgencyChange() {
|
||||||
|
if (!isPageLoading) {
|
||||||
|
|
||||||
// Set the org type to federal if an agency is selected
|
let selectedFederalAgency = $federalAgencyDropdown.find("option:selected").text();
|
||||||
let selectedText = federalAgency.find("option:selected").text();
|
if (!selectedFederalAgency) {
|
||||||
|
|
||||||
// There isn't a federal senior official associated with null records
|
|
||||||
if (!selectedText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let organizationTypeValue = organizationType ? organizationType.value : readonlyOrganizationType.innerText.toLowerCase();
|
|
||||||
if (selectedText !== "Non-Federal Agency") {
|
|
||||||
if (organizationTypeValue !== "federal") {
|
|
||||||
if (organizationType){
|
|
||||||
organizationType.value = "federal";
|
|
||||||
}else {
|
|
||||||
readonlyOrganizationType.innerText = "Federal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
if (organizationTypeValue === "federal") {
|
|
||||||
if (organizationType){
|
|
||||||
organizationType.value = "";
|
|
||||||
}else {
|
|
||||||
readonlyOrganizationType.innerText = "-"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
|
|
||||||
|
|
||||||
// Determine if any changes are necessary to the display of portfolio type or federal type
|
|
||||||
// based on changes to the Federal Agency
|
|
||||||
let federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
|
|
||||||
fetch(`${federalPortfolioApi}?&agency_name=${selectedText}`)
|
|
||||||
.then(response => {
|
|
||||||
const statusCode = response.status;
|
|
||||||
return response.json().then(data => ({ statusCode, data }));
|
|
||||||
})
|
|
||||||
.then(({ statusCode, data }) => {
|
|
||||||
if (data.error) {
|
|
||||||
console.error("Error in AJAX call: " + data.error);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateReadOnly(data.federal_type, '.field-federal_type');
|
|
||||||
})
|
|
||||||
.catch(error => console.error("Error fetching federal and portfolio types: ", error));
|
|
||||||
|
|
||||||
// Hide the contactList initially.
|
// 1. Handle organization type
|
||||||
// If we can update the contact information, it'll be shown again.
|
let organizationTypeValue = organizationTypeDropdown ? organizationTypeDropdown.value : organizationTypeReadonly.innerText.toLowerCase();
|
||||||
hideElement(contactList.parentElement);
|
if (selectedFederalAgency !== "Non-Federal Agency") {
|
||||||
|
if (organizationTypeValue !== "federal") {
|
||||||
let seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
|
if (organizationTypeDropdown){
|
||||||
let $seniorOfficial = django.jQuery("#id_senior_official");
|
organizationTypeDropdown.value = "federal";
|
||||||
let readonlySeniorOfficial = document.querySelector(".field-senior_official .readonly");
|
} else {
|
||||||
let seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
|
organizationTypeReadonly.innerText = "Federal"
|
||||||
fetch(`${seniorOfficialApi}?agency_name=${selectedText}`)
|
}
|
||||||
.then(response => {
|
}
|
||||||
const statusCode = response.status;
|
} else {
|
||||||
return response.json().then(data => ({ statusCode, data }));
|
if (organizationTypeValue === "federal") {
|
||||||
})
|
if (organizationTypeDropdown){
|
||||||
.then(({ statusCode, data }) => {
|
organizationTypeDropdown.value = "";
|
||||||
if (data.error) {
|
} else {
|
||||||
// Clear the field if the SO doesn't exist.
|
organizationTypeReadonly.innerText = "-"
|
||||||
if (statusCode === 404) {
|
|
||||||
if ($seniorOfficial && $seniorOfficial.length > 0) {
|
|
||||||
$seniorOfficial.val("").trigger("change");
|
|
||||||
}else {
|
|
||||||
// Show the "create one now" text if this field is none in readonly mode.
|
|
||||||
readonlySeniorOfficial.innerHTML = `<a href="${seniorOfficialAddUrl}">No senior official found. Create one now.</a>`;
|
|
||||||
}
|
}
|
||||||
console.warn("Record not found: " + data.error);
|
|
||||||
}else {
|
|
||||||
console.error("Error in AJAX call: " + data.error);
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the "contact details" blurb beneath senior official
|
// 2. Handle organization type change side effects
|
||||||
updateContactInfo(data);
|
handleOrganizationTypeChange();
|
||||||
showElement(contactList.parentElement);
|
|
||||||
|
// 3. Handle federal type
|
||||||
|
getFederalTypeFromAgency(selectedFederalAgency).then((federalType) => updateReadOnly(federalType, '.field-federal_type'));
|
||||||
|
|
||||||
// Get the associated senior official with this federal agency
|
// 4. Handle senior official
|
||||||
let seniorOfficialId = data.id;
|
hideElement(seniorOfficialAddress.parentElement);
|
||||||
let seniorOfficialName = [data.first_name, data.last_name].join(" ");
|
getSeniorOfficialFromAgency(selectedFederalAgency).then((senior_official) => {
|
||||||
if ($seniorOfficial && $seniorOfficial.length > 0) {
|
// Update the "contact details" blurb beneath senior official
|
||||||
// If the senior official is a dropdown field, edit that
|
updateSeniorOfficialContactInfo(senior_official);
|
||||||
updateSeniorOfficialDropdown($seniorOfficial, seniorOfficialId, seniorOfficialName);
|
showElement(seniorOfficialAddress.parentElement);
|
||||||
}else {
|
// Get the associated senior official with this federal agency
|
||||||
if (readonlySeniorOfficial) {
|
let seniorOfficialId = senior_official.id;
|
||||||
let seniorOfficialLink = `<a href=/admin/registrar/seniorofficial/${seniorOfficialId}/change/>${seniorOfficialName}</a>`
|
let seniorOfficialName = [senior_official.first_name, senior_official.last_name].join(" ");
|
||||||
readonlySeniorOfficial.innerHTML = seniorOfficialName ? seniorOfficialLink : "-";
|
if ($seniorOfficialDropdown && $seniorOfficialDropdown.length > 0) {
|
||||||
|
// If the senior official is a dropdown field, edit that
|
||||||
|
updateSeniorOfficialDropdown(seniorOfficialId, seniorOfficialName);
|
||||||
|
} else {
|
||||||
|
if (seniorOfficialReadonly) {
|
||||||
|
let seniorOfficialLink = `<a href=/admin/registrar/seniorofficial/${seniorOfficialId}/change/>${seniorOfficialName}</a>`
|
||||||
|
seniorOfficialReadonly.innerHTML = seniorOfficialName ? seniorOfficialLink : "-";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
.catch(error => {
|
||||||
.catch(error => console.error("Error fetching senior official: ", error));
|
if (error.statusCode === 404) {
|
||||||
|
// Handle "not found" senior official
|
||||||
|
if ($seniorOfficialDropdown && $seniorOfficialDropdown.length > 0) {
|
||||||
|
$seniorOfficialDropdown.val("").trigger("change");
|
||||||
|
} else {
|
||||||
|
seniorOfficialReadonly.innerHTML = `<a href="${seniorOfficialAddUrl}">No senior official found. Create one now.</a>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle other errors
|
||||||
|
console.error("An error occurred:", error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isPageLoading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSeniorOfficialDropdown(dropdown, seniorOfficialId, seniorOfficialName) {
|
/**
|
||||||
|
* Helper for updating federal type field
|
||||||
|
*/
|
||||||
|
function updateSeniorOfficialDropdown(seniorOfficialId, seniorOfficialName) {
|
||||||
if (!seniorOfficialId || !seniorOfficialName || !seniorOfficialName.trim()){
|
if (!seniorOfficialId || !seniorOfficialName || !seniorOfficialName.trim()){
|
||||||
// Clear the field if the SO doesn't exist
|
// Clear the field if the SO doesn't exist
|
||||||
dropdown.val("").trigger("change");
|
$seniorOfficialDropdown.val("").trigger("change");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the senior official to the dropdown.
|
// Add the senior official to the dropdown.
|
||||||
// This format supports select2 - if we decide to convert this field in the future.
|
// This format supports select2 - if we decide to convert this field in the future.
|
||||||
if (dropdown.find(`option[value='${seniorOfficialId}']`).length) {
|
if ($seniorOfficialDropdown.find(`option[value='${seniorOfficialId}']`).length) {
|
||||||
// Select the value that is associated with the current Senior Official.
|
// Select the value that is associated with the current Senior Official.
|
||||||
dropdown.val(seniorOfficialId).trigger("change");
|
$seniorOfficialDropdown.val(seniorOfficialId).trigger("change");
|
||||||
} else {
|
} else {
|
||||||
// Create a DOM Option that matches the desired Senior Official. Then append it and select it.
|
// Create a DOM Option that matches the desired Senior Official. Then append it and select it.
|
||||||
let userOption = new Option(seniorOfficialName, seniorOfficialId, true, true);
|
let userOption = new Option(seniorOfficialName, seniorOfficialId, true, true);
|
||||||
dropdown.append(userOption).trigger("change");
|
$seniorOfficialDropdown.append(userOption).trigger("change");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStateTerritoryChange(stateTerritory, urbanizationField) {
|
/**
|
||||||
let selectedValue = stateTerritory.value;
|
* Handle urbanization
|
||||||
|
*/
|
||||||
|
function handleStateTerritoryChange() {
|
||||||
|
let selectedValue = stateTerritoryDropdown.value;
|
||||||
if (selectedValue === "PR") {
|
if (selectedValue === "PR") {
|
||||||
showElement(urbanizationField)
|
showElement(urbanizationField)
|
||||||
} else {
|
} else {
|
||||||
|
@ -207,11 +216,7 @@ export function initDynamicPortfolioFields(){
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility that selects a div from the DOM using selectorString,
|
* Helper for updating senior official dropdown
|
||||||
* and updates a div within that div which has class of 'readonly'
|
|
||||||
* so that the text of the div is updated to updateText
|
|
||||||
* @param {*} updateText
|
|
||||||
* @param {*} selectorString
|
|
||||||
*/
|
*/
|
||||||
function updateReadOnly(updateText, selectorString) {
|
function updateReadOnly(updateText, selectorString) {
|
||||||
// find the div by selectorString
|
// find the div by selectorString
|
||||||
|
@ -226,34 +231,75 @@ export function initDynamicPortfolioFields(){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateContactInfo(data) {
|
/**
|
||||||
if (!contactList) return;
|
* Helper for updating senior official contact info
|
||||||
|
*/
|
||||||
const titleSpan = contactList.querySelector(".contact_info_title");
|
function updateSeniorOfficialContactInfo(senior_official) {
|
||||||
const emailSpan = contactList.querySelector(".contact_info_email");
|
if (!seniorOfficialAddress) return;
|
||||||
const phoneSpan = contactList.querySelector(".contact_info_phone");
|
const titleSpan = seniorOfficialAddress.querySelector(".contact_info_title");
|
||||||
|
const emailSpan = seniorOfficialAddress.querySelector(".contact_info_email");
|
||||||
|
const phoneSpan = seniorOfficialAddress.querySelector(".contact_info_phone");
|
||||||
if (titleSpan) {
|
if (titleSpan) {
|
||||||
titleSpan.textContent = data.title || "None";
|
titleSpan.textContent = senior_official.title || "None";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the email field and the content for the clipboard
|
// Update the email field and the content for the clipboard
|
||||||
if (emailSpan) {
|
if (emailSpan) {
|
||||||
let copyButton = contactList.querySelector(".admin-icon-group");
|
let copyButton = seniorOfficialAddress.querySelector(".admin-icon-group");
|
||||||
emailSpan.textContent = data.email || "None";
|
emailSpan.textContent = senior_official.email || "None";
|
||||||
if (data.email) {
|
if (senior_official.email) {
|
||||||
const clipboardInput = contactList.querySelector(".admin-icon-group input");
|
const clipboardInput = seniorOfficialAddress.querySelector(".admin-icon-group input");
|
||||||
if (clipboardInput) {
|
if (clipboardInput) {
|
||||||
clipboardInput.value = data.email;
|
clipboardInput.value = senior_official.email;
|
||||||
};
|
};
|
||||||
showElement(copyButton);
|
showElement(copyButton);
|
||||||
}else {
|
}else {
|
||||||
hideElement(copyButton);
|
hideElement(copyButton);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (phoneSpan) {
|
if (phoneSpan) {
|
||||||
phoneSpan.textContent = data.phone || "None";
|
phoneSpan.textContent = senior_official.phone || "None";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes necessary data and display configurations for the portfolio fields.
|
||||||
|
*/
|
||||||
|
function initializePortfolioSettings() {
|
||||||
|
if (urbanizationField && stateTerritoryDropdown) {
|
||||||
|
handleStateTerritoryChange();
|
||||||
|
}
|
||||||
|
handleOrganizationTypeChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets event listeners for key UI elements.
|
||||||
|
*/
|
||||||
|
function setEventListeners() {
|
||||||
|
if ($federalAgencyDropdown && (organizationTypeDropdown || organizationTypeReadonly)) {
|
||||||
|
$federalAgencyDropdown.on("change", function() {
|
||||||
|
handleFederalAgencyChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (urbanizationField && stateTerritoryDropdown) {
|
||||||
|
stateTerritoryDropdown.addEventListener("change", function() {
|
||||||
|
handleStateTerritoryChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
organizationTypeDropdown.addEventListener("change", function() {
|
||||||
|
handleOrganizationTypeChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run initial setup functions
|
||||||
|
initializePortfolioSettings();
|
||||||
|
setEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initDynamicPortfolioFields() {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let isPortfolioPage = document.getElementById("portfolio_form");
|
||||||
|
if (isPortfolioPage) {
|
||||||
|
handlePortfolioFields();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,6 +230,12 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""Called during delete. Example: `del domain.registrant`."""
|
"""Called during delete. Example: `del domain.registrant`."""
|
||||||
super().__delete__(obj)
|
super().__delete__(obj)
|
||||||
|
|
||||||
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
|
# If the domain is deleted we don't want the expiration date to be set
|
||||||
|
if self.state == self.State.DELETED and self.expiration_date:
|
||||||
|
self.expiration_date = None
|
||||||
|
super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls, domain: str) -> bool:
|
def available(cls, domain: str) -> bool:
|
||||||
"""Check if a domain is available.
|
"""Check if a domain is available.
|
||||||
|
@ -253,7 +259,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
return not cls.available(domain)
|
return not cls.available(domain)
|
||||||
|
|
||||||
@Cache
|
@Cache
|
||||||
def contacts(self) -> dict[str, str]:
|
def registry_contacts(self) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Get a dictionary of registry IDs for the contacts for this domain.
|
Get a dictionary of registry IDs for the contacts for this domain.
|
||||||
|
|
||||||
|
@ -706,7 +712,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@nameservers.setter # type: ignore
|
@nameservers.setter # type: ignore
|
||||||
def nameservers(self, hosts: list[tuple[str, list]]):
|
def nameservers(self, hosts: list[tuple[str, list]]): # noqa
|
||||||
"""Host should be a tuple of type str, str,... where the elements are
|
"""Host should be a tuple of type str, str,... where the elements are
|
||||||
Fully qualified host name, addresses associated with the host
|
Fully qualified host name, addresses associated with the host
|
||||||
example: [(ns1.okay.gov, [127.0.0.1, others ips])]"""
|
example: [(ns1.okay.gov, [127.0.0.1, others ips])]"""
|
||||||
|
@ -743,7 +749,12 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount
|
successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount
|
||||||
|
|
||||||
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
try:
|
||||||
|
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
||||||
|
except Exception as e:
|
||||||
|
# we don't need this part to succeed in order to continue.
|
||||||
|
logger.error("Failed to delete nameserver hosts: %s", e)
|
||||||
|
|
||||||
if successTotalNameservers < 2:
|
if successTotalNameservers < 2:
|
||||||
try:
|
try:
|
||||||
self.dns_needed()
|
self.dns_needed()
|
||||||
|
@ -1029,6 +1040,47 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
def _delete_domain(self):
|
def _delete_domain(self):
|
||||||
"""This domain should be deleted from the registry
|
"""This domain should be deleted from the registry
|
||||||
may raises RegistryError, should be caught or handled correctly by caller"""
|
may raises RegistryError, should be caught or handled correctly by caller"""
|
||||||
|
|
||||||
|
logger.info("Deleting subdomains for %s", self.name)
|
||||||
|
# check if any subdomains are in use by another domain
|
||||||
|
hosts = Host.objects.filter(name__regex=r".+{}".format(self.name))
|
||||||
|
for host in hosts:
|
||||||
|
if host.domain != self:
|
||||||
|
logger.error("Unable to delete host: %s is in use by another domain: %s", host.name, host.domain)
|
||||||
|
raise RegistryError(
|
||||||
|
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION,
|
||||||
|
note=f"Host {host.name} is in use by {host.domain}",
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
deleted_values,
|
||||||
|
updated_values,
|
||||||
|
new_values,
|
||||||
|
oldNameservers,
|
||||||
|
) = self.getNameserverChanges(hosts=[])
|
||||||
|
|
||||||
|
_ = self._update_host_values(updated_values, oldNameservers) # returns nothing, just need to be run and errors
|
||||||
|
addToDomainList, _ = self.createNewHostList(new_values)
|
||||||
|
deleteHostList, _ = self.createDeleteHostList(deleted_values)
|
||||||
|
responseCode = self.addAndRemoveHostsFromDomain(hostsToAdd=addToDomainList, hostsToDelete=deleteHostList)
|
||||||
|
|
||||||
|
# if unable to update domain raise error and stop
|
||||||
|
if responseCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY:
|
||||||
|
raise NameserverError(code=nsErrorCodes.BAD_DATA)
|
||||||
|
|
||||||
|
# addAndRemoveHostsFromDomain removes the hosts from the domain object,
|
||||||
|
# but we still need to delete the object themselves
|
||||||
|
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
||||||
|
|
||||||
|
logger.debug("Deleting non-registrant contacts for %s", self.name)
|
||||||
|
contacts = PublicContact.objects.filter(domain=self)
|
||||||
|
for contact in contacts:
|
||||||
|
if contact.contact_type != PublicContact.ContactTypeChoices.REGISTRANT:
|
||||||
|
self._update_domain_with_contact(contact, rem=True)
|
||||||
|
request = commands.DeleteContact(contact.registry_id)
|
||||||
|
registry.send(request, cleaned=True)
|
||||||
|
|
||||||
|
logger.info("Deleting domain %s", self.name)
|
||||||
request = commands.DeleteDomain(name=self.name)
|
request = commands.DeleteDomain(name=self.name)
|
||||||
registry.send(request, cleaned=True)
|
registry.send(request, cleaned=True)
|
||||||
|
|
||||||
|
@ -1096,7 +1148,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
Returns True if expired, False otherwise.
|
Returns True if expired, False otherwise.
|
||||||
"""
|
"""
|
||||||
if self.expiration_date is None:
|
if self.expiration_date is None:
|
||||||
return True
|
return self.state != self.State.DELETED
|
||||||
now = timezone.now().date()
|
now = timezone.now().date()
|
||||||
return self.expiration_date < now
|
return self.expiration_date < now
|
||||||
|
|
||||||
|
@ -1430,6 +1482,8 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
@transition(field="state", source=[State.ON_HOLD, State.DNS_NEEDED], target=State.DELETED)
|
@transition(field="state", source=[State.ON_HOLD, State.DNS_NEEDED], target=State.DELETED)
|
||||||
def deletedInEpp(self):
|
def deletedInEpp(self):
|
||||||
"""Domain is deleted in epp but is saved in our database.
|
"""Domain is deleted in epp but is saved in our database.
|
||||||
|
Subdomains will be deleted first if not in use by another domain.
|
||||||
|
Contacts for this domain will also be deleted.
|
||||||
Error handling should be provided by the caller."""
|
Error handling should be provided by the caller."""
|
||||||
# While we want to log errors, we want to preserve
|
# While we want to log errors, we want to preserve
|
||||||
# that information when this function is called.
|
# that information when this function is called.
|
||||||
|
@ -1439,8 +1493,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
logger.info("deletedInEpp()-> inside _delete_domain")
|
logger.info("deletedInEpp()-> inside _delete_domain")
|
||||||
self._delete_domain()
|
self._delete_domain()
|
||||||
self.deleted = timezone.now()
|
self.deleted = timezone.now()
|
||||||
|
self.expiration_date = None
|
||||||
except RegistryError as err:
|
except RegistryError as err:
|
||||||
logger.error(f"Could not delete domain. Registry returned error: {err}")
|
logger.error(f"Could not delete domain. Registry returned error: {err}. {err.note}")
|
||||||
raise err
|
raise err
|
||||||
except TransitionNotAllowed as err:
|
except TransitionNotAllowed as err:
|
||||||
logger.error("Could not delete domain. FSM failure: {err}")
|
logger.error("Could not delete domain. FSM failure: {err}")
|
||||||
|
@ -1745,7 +1800,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""delete the host object in registry,
|
"""delete the host object in registry,
|
||||||
will only delete the host object, if it's not being used by another domain
|
will only delete the host object, if it's not being used by another domain
|
||||||
Performs just the DeleteHost epp call
|
Performs just the DeleteHost epp call
|
||||||
Supresses regstry error, as registry can disallow delete for various reasons
|
|
||||||
Args:
|
Args:
|
||||||
hostsToDelete (list[str])- list of nameserver/host names to remove
|
hostsToDelete (list[str])- list of nameserver/host names to remove
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -1764,6 +1818,8 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
else:
|
else:
|
||||||
logger.error("Error _delete_hosts_if_not_used, code was %s error was %s" % (e.code, e))
|
logger.error("Error _delete_hosts_if_not_used, code was %s error was %s" % (e.code, e))
|
||||||
|
|
||||||
|
raise e
|
||||||
|
|
||||||
def _fix_unknown_state(self, cleaned):
|
def _fix_unknown_state(self, cleaned):
|
||||||
"""
|
"""
|
||||||
_fix_unknown_state: Calls _add_missing_contacts_if_unknown
|
_fix_unknown_state: Calls _add_missing_contacts_if_unknown
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
{% extends 'admin/change_form.html' %}
|
{% extends 'admin/change_form.html' %}
|
||||||
{% load i18n static %}
|
{% 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 %}
|
{% block field_sets %}
|
||||||
<div class="display-flex flex-row flex-justify submit-row">
|
<div class="display-flex flex-row flex-justify submit-row">
|
||||||
<div class="flex-align-self-start button-list-mobile">
|
<div class="flex-align-self-start button-list-mobile">
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
{% extends 'admin/change_form.html' %}
|
{% extends 'admin/change_form.html' %}
|
||||||
{% load i18n static %}
|
{% 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 %}
|
{% block field_sets %}
|
||||||
{% for fieldset in adminform %}
|
{% for fieldset in adminform %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
|
|
|
@ -1232,6 +1232,7 @@ class MockEppLib(TestCase):
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
|
registrant="regContact",
|
||||||
ex_date=date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1394,6 +1395,15 @@ class MockEppLib(TestCase):
|
||||||
hosts=["fake.host.com"],
|
hosts=["fake.host.com"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
infoDomainSharedHost = fakedEppObject(
|
||||||
|
"sharedHost.gov",
|
||||||
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
|
contacts=[],
|
||||||
|
hosts=[
|
||||||
|
"ns1.sharedhost.com",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
infoDomainThreeHosts = fakedEppObject(
|
infoDomainThreeHosts = fakedEppObject(
|
||||||
"my-nameserver.gov",
|
"my-nameserver.gov",
|
||||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
|
@ -1604,6 +1614,8 @@ class MockEppLib(TestCase):
|
||||||
return self.mockInfoContactCommands(_request, cleaned)
|
return self.mockInfoContactCommands(_request, cleaned)
|
||||||
case commands.CreateContact:
|
case commands.CreateContact:
|
||||||
return self.mockCreateContactCommands(_request, cleaned)
|
return self.mockCreateContactCommands(_request, cleaned)
|
||||||
|
case commands.DeleteContact:
|
||||||
|
return self.mockDeleteContactCommands(_request, cleaned)
|
||||||
case commands.UpdateDomain:
|
case commands.UpdateDomain:
|
||||||
return self.mockUpdateDomainCommands(_request, cleaned)
|
return self.mockUpdateDomainCommands(_request, cleaned)
|
||||||
case commands.CreateHost:
|
case commands.CreateHost:
|
||||||
|
@ -1611,10 +1623,7 @@ class MockEppLib(TestCase):
|
||||||
case commands.UpdateHost:
|
case commands.UpdateHost:
|
||||||
return self.mockUpdateHostCommands(_request, cleaned)
|
return self.mockUpdateHostCommands(_request, cleaned)
|
||||||
case commands.DeleteHost:
|
case commands.DeleteHost:
|
||||||
return MagicMock(
|
return self.mockDeleteHostCommands(_request, cleaned)
|
||||||
res_data=[self.mockDataHostChange],
|
|
||||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
|
||||||
)
|
|
||||||
case commands.CheckDomain:
|
case commands.CheckDomain:
|
||||||
return self.mockCheckDomainCommand(_request, cleaned)
|
return self.mockCheckDomainCommand(_request, cleaned)
|
||||||
case commands.DeleteDomain:
|
case commands.DeleteDomain:
|
||||||
|
@ -1667,6 +1676,15 @@ class MockEppLib(TestCase):
|
||||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def mockDeleteHostCommands(self, _request, cleaned):
|
||||||
|
host = getattr(_request, "name", None)
|
||||||
|
if "sharedhost.com" in host:
|
||||||
|
raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION, note="ns1.sharedhost.com")
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[self.mockDataHostChange],
|
||||||
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
|
)
|
||||||
|
|
||||||
def mockUpdateDomainCommands(self, _request, cleaned):
|
def mockUpdateDomainCommands(self, _request, cleaned):
|
||||||
if getattr(_request, "name", None) == "dnssec-invalid.gov":
|
if getattr(_request, "name", None) == "dnssec-invalid.gov":
|
||||||
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
|
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
|
||||||
|
@ -1678,10 +1696,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
def mockDeleteDomainCommands(self, _request, cleaned):
|
def mockDeleteDomainCommands(self, _request, cleaned):
|
||||||
if getattr(_request, "name", None) == "failDelete.gov":
|
if getattr(_request, "name", None) == "failDelete.gov":
|
||||||
name = getattr(_request, "name", None)
|
raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
|
||||||
fake_nameserver = "ns1.failDelete.gov"
|
|
||||||
if name in fake_nameserver:
|
|
||||||
raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mockRenewDomainCommand(self, _request, cleaned):
|
def mockRenewDomainCommand(self, _request, cleaned):
|
||||||
|
@ -1721,6 +1736,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
# Define a dictionary to map request names to data and extension values
|
# Define a dictionary to map request names to data and extension values
|
||||||
request_mappings = {
|
request_mappings = {
|
||||||
|
"fake.gov": (self.mockDataInfoDomain, None),
|
||||||
"security.gov": (self.infoDomainNoContact, None),
|
"security.gov": (self.infoDomainNoContact, None),
|
||||||
"dnssec-dsdata.gov": (
|
"dnssec-dsdata.gov": (
|
||||||
self.mockDataInfoDomain,
|
self.mockDataInfoDomain,
|
||||||
|
@ -1751,6 +1767,7 @@ class MockEppLib(TestCase):
|
||||||
"subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None),
|
"subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None),
|
||||||
"ddomain3.gov": (self.InfoDomainWithContacts, None),
|
"ddomain3.gov": (self.InfoDomainWithContacts, None),
|
||||||
"igorville.gov": (self.InfoDomainWithContacts, None),
|
"igorville.gov": (self.InfoDomainWithContacts, None),
|
||||||
|
"sharingiscaring.gov": (self.infoDomainSharedHost, None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Retrieve the corresponding values from the dictionary
|
# Retrieve the corresponding values from the dictionary
|
||||||
|
@ -1801,6 +1818,15 @@ class MockEppLib(TestCase):
|
||||||
raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE)
|
raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE)
|
||||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||||
|
|
||||||
|
def mockDeleteContactCommands(self, _request, cleaned):
|
||||||
|
if getattr(_request, "id", None) == "fail":
|
||||||
|
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
|
||||||
|
else:
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[self.mockDataInfoContact],
|
||||||
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""mock epp send function as this will fail locally"""
|
"""mock epp send function as this will fail locally"""
|
||||||
self.mockSendPatch = patch("registrar.models.domain.registry.send")
|
self.mockSendPatch = patch("registrar.models.domain.registry.send")
|
||||||
|
|
|
@ -853,9 +853,9 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
# We expect 3 in the form + 2 from the js module copy-to-clipboard.js
|
# We expect 4 in the form + 2 from the js module copy-to-clipboard.js
|
||||||
# that gets pulled in the test in django.contrib.staticfiles.finders.FileSystemFinder
|
# that gets pulled in the test in django.contrib.staticfiles.finders.FileSystemFinder
|
||||||
self.assertContains(response, "copy-to-clipboard", count=5)
|
self.assertContains(response, "copy-to-clipboard", count=6)
|
||||||
|
|
||||||
# cleanup this test
|
# cleanup this test
|
||||||
domain_info.delete()
|
domain_info.delete()
|
||||||
|
@ -871,6 +871,17 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
readonly_fields = self.admin.get_readonly_fields(request)
|
readonly_fields = self.admin.get_readonly_fields(request)
|
||||||
|
|
||||||
expected_fields = [
|
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",
|
"other_contacts",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
|
|
@ -16,6 +16,7 @@ from registrar.models import (
|
||||||
Host,
|
Host,
|
||||||
Portfolio,
|
Portfolio,
|
||||||
)
|
)
|
||||||
|
from registrar.models.public_contact import PublicContact
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from .common import (
|
from .common import (
|
||||||
MockSESClient,
|
MockSESClient,
|
||||||
|
@ -59,6 +60,7 @@ class TestDomainAdminAsStaff(MockEppLib):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
Host.objects.all().delete()
|
Host.objects.all().delete()
|
||||||
|
PublicContact.objects.all().delete()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
|
@ -170,7 +172,7 @@ class TestDomainAdminAsStaff(MockEppLib):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_deletion_is_successful(self):
|
def test_deletion_is_successful(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Domain deletion is unsuccessful
|
Scenario: Domain deletion is successful
|
||||||
When the domain is deleted
|
When the domain is deleted
|
||||||
Then a user-friendly success message is returned for displaying on the web
|
Then a user-friendly success message is returned for displaying on the web
|
||||||
And `state` is set to `DELETED`
|
And `state` is set to `DELETED`
|
||||||
|
@ -221,6 +223,55 @@ class TestDomainAdminAsStaff(MockEppLib):
|
||||||
|
|
||||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||||
|
|
||||||
|
# @less_console_noise_decorator
|
||||||
|
def test_deletion_is_unsuccessful(self):
|
||||||
|
"""
|
||||||
|
Scenario: Domain deletion is unsuccessful
|
||||||
|
When the domain is deleted and has shared subdomains
|
||||||
|
Then a user-friendly success message is returned for displaying on the web
|
||||||
|
And `state` is not set to `DELETED`
|
||||||
|
"""
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="sharingiscaring.gov", state=Domain.State.ON_HOLD)
|
||||||
|
# Put in client hold
|
||||||
|
domain.place_client_hold()
|
||||||
|
# Ensure everything is displaying correctly
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Remove from registry")
|
||||||
|
|
||||||
|
# The contents of the modal should exist before and after the post.
|
||||||
|
# Check for the header
|
||||||
|
self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
|
||||||
|
|
||||||
|
# Check for some of its body
|
||||||
|
self.assertContains(response, "When a domain is removed from the registry:")
|
||||||
|
|
||||||
|
# Check for some of the button content
|
||||||
|
self.assertContains(response, "Yes, remove from registry")
|
||||||
|
|
||||||
|
# Test the info dialog
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
request.user = self.client
|
||||||
|
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||||
|
self.admin.do_delete_domain(request, domain)
|
||||||
|
mock_add_message.assert_called_once_with(
|
||||||
|
request,
|
||||||
|
messages.ERROR,
|
||||||
|
"Error deleting this Domain: This subdomain is being used as a hostname on another domain: ns1.sharedhost.com", # noqa
|
||||||
|
extra_tags="",
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(domain.state, Domain.State.ON_HOLD)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_deletion_ready_fsm_failure(self):
|
def test_deletion_ready_fsm_failure(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.db.utils import IntegrityError
|
||||||
from unittest.mock import MagicMock, patch, call
|
from unittest.mock import MagicMock, patch, call
|
||||||
import datetime
|
import datetime
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
|
from api.tests.common import less_console_noise_decorator
|
||||||
from registrar.models import Domain, Host, HostIP
|
from registrar.models import Domain, Host, HostIP
|
||||||
|
|
||||||
from unittest import skip
|
from unittest import skip
|
||||||
|
@ -1454,6 +1455,7 @@ class TestRegistrantNameservers(MockEppLib):
|
||||||
),
|
),
|
||||||
call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
|
call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
|
||||||
]
|
]
|
||||||
|
|
||||||
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
|
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
|
||||||
self.assertFalse(self.domainWithThreeNS.is_active())
|
self.assertFalse(self.domainWithThreeNS.is_active())
|
||||||
|
|
||||||
|
@ -2582,12 +2584,32 @@ class TestAnalystDelete(MockEppLib):
|
||||||
"""
|
"""
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
self.domain_with_contacts, _ = Domain.objects.get_or_create(name="freeman.gov", state=Domain.State.READY)
|
||||||
self.domain_on_hold, _ = Domain.objects.get_or_create(name="fake-on-hold.gov", state=Domain.State.ON_HOLD)
|
self.domain_on_hold, _ = Domain.objects.get_or_create(name="fake-on-hold.gov", state=Domain.State.ON_HOLD)
|
||||||
|
Host.objects.create(name="ns1.sharingiscaring.gov", domain=self.domain_on_hold)
|
||||||
|
PublicContact.objects.create(
|
||||||
|
registry_id="regContact",
|
||||||
|
contact_type=PublicContact.ContactTypeChoices.REGISTRANT,
|
||||||
|
domain=self.domain_with_contacts,
|
||||||
|
)
|
||||||
|
PublicContact.objects.create(
|
||||||
|
registry_id="adminContact",
|
||||||
|
contact_type=PublicContact.ContactTypeChoices.ADMINISTRATIVE,
|
||||||
|
domain=self.domain_with_contacts,
|
||||||
|
)
|
||||||
|
PublicContact.objects.create(
|
||||||
|
registry_id="techContact",
|
||||||
|
contact_type=PublicContact.ContactTypeChoices.TECHNICAL,
|
||||||
|
domain=self.domain_with_contacts,
|
||||||
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
Host.objects.all().delete()
|
||||||
|
PublicContact.objects.all().delete()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_analyst_deletes_domain(self):
|
def test_analyst_deletes_domain(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Analyst permanently deletes a domain
|
Scenario: Analyst permanently deletes a domain
|
||||||
|
@ -2597,59 +2619,163 @@ class TestAnalystDelete(MockEppLib):
|
||||||
|
|
||||||
The deleted date is set.
|
The deleted date is set.
|
||||||
"""
|
"""
|
||||||
with less_console_noise():
|
# Put the domain in client hold
|
||||||
# Put the domain in client hold
|
self.domain.place_client_hold()
|
||||||
self.domain.place_client_hold()
|
# Delete it...
|
||||||
# Delete it...
|
self.domain.deletedInEpp()
|
||||||
self.domain.deletedInEpp()
|
self.domain.save()
|
||||||
self.domain.save()
|
self.mockedSendFunction.assert_has_calls(
|
||||||
self.mockedSendFunction.assert_has_calls(
|
[
|
||||||
[
|
call(
|
||||||
call(
|
commands.DeleteDomain(name="fake.gov"),
|
||||||
commands.DeleteDomain(name="fake.gov"),
|
cleaned=True,
|
||||||
cleaned=True,
|
)
|
||||||
)
|
]
|
||||||
]
|
)
|
||||||
)
|
# Domain itself should not be deleted
|
||||||
# Domain itself should not be deleted
|
self.assertNotEqual(self.domain, None)
|
||||||
self.assertNotEqual(self.domain, None)
|
# Domain should have the right state
|
||||||
# Domain should have the right state
|
self.assertEqual(self.domain.state, Domain.State.DELETED)
|
||||||
self.assertEqual(self.domain.state, Domain.State.DELETED)
|
# Domain should have a deleted
|
||||||
# Domain should have a deleted
|
self.assertNotEqual(self.domain.deleted, None)
|
||||||
self.assertNotEqual(self.domain.deleted, None)
|
# Cache should be invalidated
|
||||||
# Cache should be invalidated
|
self.assertEqual(self.domain._cache, {})
|
||||||
self.assertEqual(self.domain._cache, {})
|
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_deletion_is_unsuccessful(self):
|
def test_deletion_is_unsuccessful(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Domain deletion is unsuccessful
|
Scenario: Domain deletion is unsuccessful
|
||||||
When a subdomain exists
|
When a subdomain exists that is in use by another domain
|
||||||
Then a client error is returned of code 2305
|
Then a client error is returned of code 2305
|
||||||
And `state` is not set to `DELETED`
|
And `state` is not set to `DELETED`
|
||||||
"""
|
"""
|
||||||
with less_console_noise():
|
# Desired domain
|
||||||
# Desired domain
|
domain, _ = Domain.objects.get_or_create(name="sharingiscaring.gov", state=Domain.State.ON_HOLD)
|
||||||
domain, _ = Domain.objects.get_or_create(name="failDelete.gov", state=Domain.State.ON_HOLD)
|
# Put the domain in client hold
|
||||||
# Put the domain in client hold
|
domain.place_client_hold()
|
||||||
domain.place_client_hold()
|
# Delete it
|
||||||
# Delete it
|
with self.assertRaises(RegistryError) as err:
|
||||||
with self.assertRaises(RegistryError) as err:
|
domain.deletedInEpp()
|
||||||
domain.deletedInEpp()
|
domain.save()
|
||||||
domain.save()
|
|
||||||
self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
|
|
||||||
self.mockedSendFunction.assert_has_calls(
|
|
||||||
[
|
|
||||||
call(
|
|
||||||
commands.DeleteDomain(name="failDelete.gov"),
|
|
||||||
cleaned=True,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# Domain itself should not be deleted
|
|
||||||
self.assertNotEqual(domain, None)
|
|
||||||
# State should not have changed
|
|
||||||
self.assertEqual(domain.state, Domain.State.ON_HOLD)
|
|
||||||
|
|
||||||
|
self.assertTrue(err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
|
||||||
|
self.assertEqual(err.msg, "Host ns1.sharingiscaring.gov is in use by: fake-on-hold.gov")
|
||||||
|
# Domain itself should not be deleted
|
||||||
|
self.assertNotEqual(domain, None)
|
||||||
|
# State should not have changed
|
||||||
|
self.assertEqual(domain.state, Domain.State.ON_HOLD)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_deletion_with_host_and_contacts(self):
|
||||||
|
"""
|
||||||
|
Scenario: Domain with related Host and Contacts is Deleted
|
||||||
|
When a contact and host exists that is tied to this domain
|
||||||
|
Then all the needed commands are sent to the registry
|
||||||
|
And `state` is set to `DELETED`
|
||||||
|
"""
|
||||||
|
# Put the domain in client hold
|
||||||
|
self.domain_with_contacts.place_client_hold()
|
||||||
|
# Delete it
|
||||||
|
self.domain_with_contacts.deletedInEpp()
|
||||||
|
self.domain_with_contacts.save()
|
||||||
|
|
||||||
|
# Check that the host and contacts are deleted
|
||||||
|
self.mockedSendFunction.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="freeman.gov",
|
||||||
|
add=[common.Status(state=Domain.Status.CLIENT_HOLD, description="", lang="en")],
|
||||||
|
rem=[],
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.mockedSendFunction.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.InfoDomain(name="freeman.gov", auth_info=None),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.InfoHost(name="fake.host.com"),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="freeman.gov",
|
||||||
|
add=[],
|
||||||
|
rem=[common.HostObjSet(hosts=["fake.host.com"])],
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.mockedSendFunction.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.DeleteHost(name="fake.host.com"),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="freeman.gov",
|
||||||
|
add=[],
|
||||||
|
rem=[common.DomainContact(contact="adminContact", type="admin")],
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.DeleteContact(id="adminContact"),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="freeman.gov",
|
||||||
|
add=[],
|
||||||
|
rem=[common.DomainContact(contact="techContact", type="tech")],
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.DeleteContact(id="techContact"),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
self.mockedSendFunction.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.DeleteDomain(name="freeman.gov"),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Domain itself should not be deleted
|
||||||
|
self.assertNotEqual(self.domain_with_contacts, None)
|
||||||
|
# State should have changed
|
||||||
|
self.assertEqual(self.domain_with_contacts.state, Domain.State.DELETED)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_deletion_ready_fsm_failure(self):
|
def test_deletion_ready_fsm_failure(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Domain deletion is unsuccessful due to FSM rules
|
Scenario: Domain deletion is unsuccessful due to FSM rules
|
||||||
|
@ -2661,15 +2787,14 @@ class TestAnalystDelete(MockEppLib):
|
||||||
|
|
||||||
The deleted date is still null.
|
The deleted date is still null.
|
||||||
"""
|
"""
|
||||||
with less_console_noise():
|
self.assertEqual(self.domain.state, Domain.State.READY)
|
||||||
self.assertEqual(self.domain.state, Domain.State.READY)
|
with self.assertRaises(TransitionNotAllowed) as err:
|
||||||
with self.assertRaises(TransitionNotAllowed) as err:
|
self.domain.deletedInEpp()
|
||||||
self.domain.deletedInEpp()
|
self.domain.save()
|
||||||
self.domain.save()
|
self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION)
|
||||||
self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION)
|
# Domain should not be deleted
|
||||||
# Domain should not be deleted
|
self.assertNotEqual(self.domain, None)
|
||||||
self.assertNotEqual(self.domain, None)
|
# Domain should have the right state
|
||||||
# Domain should have the right state
|
self.assertEqual(self.domain.state, Domain.State.READY)
|
||||||
self.assertEqual(self.domain.state, Domain.State.READY)
|
# deleted should be null
|
||||||
# deleted should be null
|
self.assertEqual(self.domain.deleted, None)
|
||||||
self.assertEqual(self.domain.deleted, None)
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue