mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-16 06:24:12 +02:00
merge main
This commit is contained in:
commit
9cd759dac8
43 changed files with 1344 additions and 215 deletions
|
@ -4,7 +4,7 @@ verify_ssl = true
|
||||||
name = "pypi"
|
name = "pypi"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
django = "4.2.10"
|
django = "4.2.17"
|
||||||
cfenv = "*"
|
cfenv = "*"
|
||||||
django-cors-headers = "*"
|
django-cors-headers = "*"
|
||||||
pycryptodomex = "*"
|
pycryptodomex = "*"
|
||||||
|
|
9
src/Pipfile.lock
generated
9
src/Pipfile.lock
generated
|
@ -1789,11 +1789,12 @@
|
||||||
},
|
},
|
||||||
"waitress": {
|
"waitress": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1",
|
"sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac",
|
||||||
"sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669"
|
"sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.8.0'",
|
"index": "pypi",
|
||||||
"version": "==3.0.0"
|
"markers": "python_full_version >= '3.9.0'",
|
||||||
|
"version": "==3.0.1"
|
||||||
},
|
},
|
||||||
"webob": {
|
"webob": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -22,5 +22,8 @@
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"webpack": "^5.96.1",
|
"webpack": "^5.96.1",
|
||||||
"webpack-stream": "^7.0.0"
|
"webpack-stream": "^7.0.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"semver": "^7.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1830,10 +1830,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
form = DomainRequestAdminForm
|
form = DomainRequestAdminForm
|
||||||
change_form_template = "django/admin/domain_request_change_form.html"
|
change_form_template = "django/admin/domain_request_change_form.html"
|
||||||
|
|
||||||
|
# ------ Filters ------
|
||||||
|
# Define custom filters
|
||||||
class StatusListFilter(MultipleChoiceListFilter):
|
class StatusListFilter(MultipleChoiceListFilter):
|
||||||
"""Custom status filter which is a multiple choice filter"""
|
"""Custom status filter which is a multiple choice filter"""
|
||||||
|
|
||||||
title = "Status"
|
title = "status"
|
||||||
parameter_name = "status__in"
|
parameter_name = "status__in"
|
||||||
|
|
||||||
template = "django/admin/multiple_choice_list_filter.html"
|
template = "django/admin/multiple_choice_list_filter.html"
|
||||||
|
@ -1877,7 +1879,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
If we have a portfolio, use the portfolio's federal type. If not, use the
|
If we have a portfolio, use the portfolio's federal type. If not, use the
|
||||||
organization in the Domain Request object."""
|
organization in the Domain Request object."""
|
||||||
|
|
||||||
title = "federal Type"
|
title = "federal type"
|
||||||
parameter_name = "converted_federal_types"
|
parameter_name = "converted_federal_types"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
|
@ -1965,13 +1967,58 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if self.value() == "0":
|
if self.value() == "0":
|
||||||
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
|
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
|
||||||
|
|
||||||
|
class PortfolioFilter(admin.SimpleListFilter):
|
||||||
|
"""Define a custom filter for portfolio"""
|
||||||
|
|
||||||
|
title = _("portfolio")
|
||||||
|
parameter_name = "portfolio__isnull"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
("1", _("Yes")),
|
||||||
|
("0", _("No")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == "1":
|
||||||
|
return queryset.filter(Q(portfolio__isnull=False))
|
||||||
|
if self.value() == "0":
|
||||||
|
return queryset.filter(Q(portfolio__isnull=True))
|
||||||
|
|
||||||
|
# ------ Custom fields ------
|
||||||
|
def custom_election_board(self, obj):
|
||||||
|
return "Yes" if obj.is_election_board else "No"
|
||||||
|
|
||||||
|
custom_election_board.admin_order_field = "is_election_board" # type: ignore
|
||||||
|
custom_election_board.short_description = "Election office" # type: ignore
|
||||||
|
|
||||||
|
@admin.display(description=_("Requested Domain"))
|
||||||
|
def custom_requested_domain(self, obj):
|
||||||
|
# Example: Show different icons based on `status`
|
||||||
|
url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}"
|
||||||
|
text = obj.requested_domain
|
||||||
|
if obj.portfolio:
|
||||||
|
return format_html('<a href="{}"><img src="/public/admin/img/icon-yes.svg"> {}</a>', url, text)
|
||||||
|
return format_html('<a href="{}">{}</a>', url, text)
|
||||||
|
|
||||||
|
custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore
|
||||||
|
|
||||||
|
# ------ Converted fields ------
|
||||||
|
# These fields map to @Property methods and
|
||||||
|
# require these custom definitions to work properly
|
||||||
@admin.display(description=_("Generic Org Type"))
|
@admin.display(description=_("Generic Org Type"))
|
||||||
def converted_generic_org_type(self, obj):
|
def converted_generic_org_type(self, obj):
|
||||||
return obj.converted_generic_org_type_display
|
return obj.converted_generic_org_type_display
|
||||||
|
|
||||||
@admin.display(description=_("Organization Name"))
|
@admin.display(description=_("Organization Name"))
|
||||||
def converted_organization_name(self, obj):
|
def converted_organization_name(self, obj):
|
||||||
return obj.converted_organization_name
|
# Example: Show different icons based on `status`
|
||||||
|
if obj.portfolio:
|
||||||
|
url = reverse("admin:registrar_portfolio_change", args=[obj.portfolio.id])
|
||||||
|
text = obj.converted_organization_name
|
||||||
|
return format_html('<a href="{}">{}</a>', url, text)
|
||||||
|
else:
|
||||||
|
return obj.converted_organization_name
|
||||||
|
|
||||||
@admin.display(description=_("Federal Agency"))
|
@admin.display(description=_("Federal Agency"))
|
||||||
def converted_federal_agency(self, obj):
|
def converted_federal_agency(self, obj):
|
||||||
|
@ -1989,34 +2036,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
def converted_state_territory(self, obj):
|
def converted_state_territory(self, obj):
|
||||||
return obj.converted_state_territory
|
return obj.converted_state_territory
|
||||||
|
|
||||||
# Columns
|
# ------ Portfolio fields ------
|
||||||
list_display = [
|
|
||||||
"requested_domain",
|
|
||||||
"first_submitted_date",
|
|
||||||
"last_submitted_date",
|
|
||||||
"last_status_update",
|
|
||||||
"status",
|
|
||||||
"custom_election_board",
|
|
||||||
"converted_generic_org_type",
|
|
||||||
"converted_organization_name",
|
|
||||||
"converted_federal_agency",
|
|
||||||
"converted_federal_type",
|
|
||||||
"converted_city",
|
|
||||||
"converted_state_territory",
|
|
||||||
"investigator",
|
|
||||||
]
|
|
||||||
|
|
||||||
orderable_fk_fields = [
|
|
||||||
("requested_domain", "name"),
|
|
||||||
("investigator", ["first_name", "last_name"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
def custom_election_board(self, obj):
|
|
||||||
return "Yes" if obj.is_election_board else "No"
|
|
||||||
|
|
||||||
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
|
# Define methods to display fields from the related portfolio
|
||||||
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
|
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
|
||||||
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
|
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
|
||||||
|
@ -2086,10 +2106,33 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
def status_history(self, obj):
|
def status_history(self, obj):
|
||||||
return "No changelog to display."
|
return "No changelog to display."
|
||||||
|
|
||||||
status_history.short_description = "Status History" # type: ignore
|
status_history.short_description = "Status history" # type: ignore
|
||||||
|
|
||||||
|
# Columns
|
||||||
|
list_display = [
|
||||||
|
"custom_requested_domain",
|
||||||
|
"first_submitted_date",
|
||||||
|
"last_submitted_date",
|
||||||
|
"last_status_update",
|
||||||
|
"status",
|
||||||
|
"custom_election_board",
|
||||||
|
"converted_generic_org_type",
|
||||||
|
"converted_organization_name",
|
||||||
|
"converted_federal_agency",
|
||||||
|
"converted_federal_type",
|
||||||
|
"converted_city",
|
||||||
|
"converted_state_territory",
|
||||||
|
"investigator",
|
||||||
|
]
|
||||||
|
|
||||||
|
orderable_fk_fields = [
|
||||||
|
("requested_domain", "name"),
|
||||||
|
("investigator", ["first_name", "last_name"]),
|
||||||
|
]
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = (
|
list_filter = (
|
||||||
|
PortfolioFilter,
|
||||||
StatusListFilter,
|
StatusListFilter,
|
||||||
GenericOrgFilter,
|
GenericOrgFilter,
|
||||||
FederalTypeFilter,
|
FederalTypeFilter,
|
||||||
|
@ -2099,13 +2142,14 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Search
|
# Search
|
||||||
|
# NOTE: converted fields are included in the override for get_search_results
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"requested_domain__name",
|
"requested_domain__name",
|
||||||
"creator__email",
|
"creator__email",
|
||||||
"creator__first_name",
|
"creator__first_name",
|
||||||
"creator__last_name",
|
"creator__last_name",
|
||||||
]
|
]
|
||||||
search_help_text = "Search by domain or creator."
|
search_help_text = "Search by domain, creator, or organization name."
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(
|
(
|
||||||
|
@ -2271,9 +2315,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"cisa_representative_first_name",
|
"cisa_representative_first_name",
|
||||||
"cisa_representative_last_name",
|
"cisa_representative_last_name",
|
||||||
"cisa_representative_email",
|
"cisa_representative_email",
|
||||||
"requested_suborganization",
|
|
||||||
"suborganization_city",
|
|
||||||
"suborganization_state_territory",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
|
@ -2692,6 +2733,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
qs = qs.filter(portfolio=portfolio_id)
|
qs = qs.filter(portfolio=portfolio_id)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def get_search_results(self, request, queryset, search_term):
|
||||||
|
# Call the parent's method to apply default search logic
|
||||||
|
base_queryset, use_distinct = super().get_search_results(request, queryset, search_term)
|
||||||
|
|
||||||
|
# Add custom search logic for the annotated field
|
||||||
|
if search_term:
|
||||||
|
annotated_queryset = queryset.filter(
|
||||||
|
# converted_organization_name
|
||||||
|
Q(portfolio__organization_name__icontains=search_term)
|
||||||
|
| Q(portfolio__isnull=True, organization_name__icontains=search_term)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine the two querysets using union
|
||||||
|
combined_queryset = base_queryset | annotated_queryset
|
||||||
|
else:
|
||||||
|
combined_queryset = base_queryset
|
||||||
|
|
||||||
|
return combined_queryset, use_distinct
|
||||||
|
|
||||||
|
|
||||||
class TransitionDomainAdmin(ListHeaderAdmin):
|
class TransitionDomainAdmin(ListHeaderAdmin):
|
||||||
"""Custom transition domain admin class."""
|
"""Custom transition domain admin class."""
|
||||||
|
@ -3746,9 +3806,9 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
"senior_official",
|
"senior_official",
|
||||||
]
|
]
|
||||||
|
|
||||||
analyst_readonly_fields = [
|
# Even though this is empty, I will leave it as a stub for easy changes in the future
|
||||||
"organization_name",
|
# rather than strip it out of our logic.
|
||||||
]
|
analyst_readonly_fields = [] # type: ignore
|
||||||
|
|
||||||
def get_admin_users(self, obj):
|
def get_admin_users(self, obj):
|
||||||
# Filter UserPortfolioPermission objects related to the portfolio
|
# Filter UserPortfolioPermission objects related to the portfolio
|
||||||
|
|
|
@ -10,8 +10,7 @@ import { initDomainRequestsTable } from './table-domain-requests.js';
|
||||||
import { initMembersTable } from './table-members.js';
|
import { initMembersTable } from './table-members.js';
|
||||||
import { initMemberDomainsTable } from './table-member-domains.js';
|
import { initMemberDomainsTable } from './table-member-domains.js';
|
||||||
import { initEditMemberDomainsTable } from './table-edit-member-domains.js';
|
import { initEditMemberDomainsTable } from './table-edit-member-domains.js';
|
||||||
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
|
import { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPageRadio } from './portfolio-member-page.js';
|
||||||
import { initAddNewMemberPageListeners } from './portfolio-member-page.js';
|
|
||||||
|
|
||||||
initDomainValidators();
|
initDomainValidators();
|
||||||
|
|
||||||
|
@ -21,13 +20,6 @@ nameserversFormListener();
|
||||||
|
|
||||||
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees');
|
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees');
|
||||||
hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null);
|
hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null);
|
||||||
hookupRadioTogglerListener(
|
|
||||||
'member_access_level',
|
|
||||||
{
|
|
||||||
'admin': 'new-member-admin-permissions',
|
|
||||||
'basic': 'new-member-basic-permissions'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);
|
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);
|
||||||
initializeUrbanizationToggle();
|
initializeUrbanizationToggle();
|
||||||
|
|
||||||
|
@ -44,5 +36,7 @@ initMembersTable();
|
||||||
initMemberDomainsTable();
|
initMemberDomainsTable();
|
||||||
initEditMemberDomainsTable();
|
initEditMemberDomainsTable();
|
||||||
|
|
||||||
initPortfolioMemberPageToggle();
|
// Init the portfolio new member page
|
||||||
|
initPortfolioMemberPageRadio();
|
||||||
|
initPortfolioNewMemberPageToggle();
|
||||||
initAddNewMemberPageListeners();
|
initAddNewMemberPageListeners();
|
||||||
|
|
|
@ -2,9 +2,10 @@ import { uswdsInitializeModals } from './helpers-uswds.js';
|
||||||
import { getCsrfToken } from './helpers.js';
|
import { getCsrfToken } from './helpers.js';
|
||||||
import { generateKebabHTML } from './table-base.js';
|
import { generateKebabHTML } from './table-base.js';
|
||||||
import { MembersTable } from './table-members.js';
|
import { MembersTable } from './table-members.js';
|
||||||
|
import { hookupRadioTogglerListener } from './radios.js';
|
||||||
|
|
||||||
// This is specifically for the Member Profile (Manage Member) Page member/invitation removal
|
// This is specifically for the Member Profile (Manage Member) Page member/invitation removal
|
||||||
export function initPortfolioMemberPageToggle() {
|
export function initPortfolioNewMemberPageToggle() {
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
|
const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
|
||||||
if (wrapperDeleteAction) {
|
if (wrapperDeleteAction) {
|
||||||
|
@ -169,4 +170,29 @@ export function initAddNewMemberPageListeners() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initalize the radio for the member pages
|
||||||
|
export function initPortfolioMemberPageRadio() {
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
let memberForm = document.getElementById("member_form");
|
||||||
|
let newMemberForm = document.getElementById("add_member_form")
|
||||||
|
if (memberForm) {
|
||||||
|
hookupRadioTogglerListener(
|
||||||
|
'role',
|
||||||
|
{
|
||||||
|
'organization_admin': 'member-admin-permissions',
|
||||||
|
'organization_member': 'member-basic-permissions'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}else if (newMemberForm){
|
||||||
|
hookupRadioTogglerListener(
|
||||||
|
'member_access_level',
|
||||||
|
{
|
||||||
|
'admin': 'new-member-admin-permissions',
|
||||||
|
'basic': 'new-member-basic-permissions'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -38,21 +38,21 @@ export function hookupYesNoListener(radioButtonName, elementIdToShowIfYes, eleme
|
||||||
**/
|
**/
|
||||||
export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
|
export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
|
||||||
// Get the radio buttons
|
// Get the radio buttons
|
||||||
let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]');
|
let radioButtons = document.querySelectorAll(`input[name="${radioButtonName}"]`);
|
||||||
|
|
||||||
// Extract the list of all element IDs from the valueToElementMap
|
// Extract the list of all element IDs from the valueToElementMap
|
||||||
let allElementIds = Object.values(valueToElementMap);
|
let allElementIds = Object.values(valueToElementMap);
|
||||||
|
|
||||||
function handleRadioButtonChange() {
|
function handleRadioButtonChange() {
|
||||||
// Find the checked radio button
|
// Find the checked radio button
|
||||||
let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked');
|
let radioButtonChecked = document.querySelector(`input[name="${radioButtonName}"]:checked`);
|
||||||
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
||||||
|
|
||||||
// Hide all elements by default
|
// Hide all elements by default
|
||||||
allElementIds.forEach(function (elementId) {
|
allElementIds.forEach(function (elementId) {
|
||||||
let element = document.getElementById(elementId);
|
let element = document.getElementById(elementId);
|
||||||
if (element) {
|
if (element) {
|
||||||
hideElement(element);
|
hideElement(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,8 +64,8 @@ export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (radioButtons.length) {
|
if (radioButtons && radioButtons.length) {
|
||||||
// Add event listener to each radio button
|
// Add event listener to each radio button
|
||||||
radioButtons.forEach(function (radioButton) {
|
radioButtons.forEach(function (radioButton) {
|
||||||
radioButton.addEventListener('change', handleRadioButtonChange);
|
radioButton.addEventListener('change', handleRadioButtonChange);
|
||||||
|
|
|
@ -31,6 +31,9 @@ export class DomainsTable extends BaseTable {
|
||||||
</td>
|
</td>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
const isExpiring = domain.state_display === "Expiring soon"
|
||||||
|
const iconType = isExpiring ? "error_outline" : "info_outline";
|
||||||
|
const iconColor = isExpiring ? "text-secondary-vivid" : "text-accent-cool"
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<th scope="row" role="rowheader" data-label="Domain name">
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
${domain.name}
|
${domain.name}
|
||||||
|
@ -41,14 +44,14 @@ export class DomainsTable extends BaseTable {
|
||||||
<td data-label="Status">
|
<td data-label="Status">
|
||||||
${domain.state_display}
|
${domain.state_display}
|
||||||
<svg
|
<svg
|
||||||
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
|
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 ${iconColor} no-click-outline-and-cursor-help"
|
||||||
data-position="top"
|
data-position="top"
|
||||||
title="${domain.get_state_help_text}"
|
title="${domain.get_state_help_text}"
|
||||||
focusable="true"
|
focusable="true"
|
||||||
aria-label="${domain.get_state_help_text}"
|
aria-label="${domain.get_state_help_text}"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
|
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#${iconType}"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</td>
|
</td>
|
||||||
${markupForSuborganizationRow}
|
${markupForSuborganizationRow}
|
||||||
|
@ -77,3 +80,30 @@ export function initDomainsTable() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For clicking the "Expiring" checkbox
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const expiringLink = document.getElementById('link-expiring-domains');
|
||||||
|
|
||||||
|
if (expiringLink) {
|
||||||
|
// Grab the selection for the status filter by
|
||||||
|
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
|
||||||
|
|
||||||
|
expiringLink.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
// Loop through all statuses
|
||||||
|
statusCheckboxes.forEach(checkbox => {
|
||||||
|
// To find the for checkbox for "Expiring soon"
|
||||||
|
if (checkbox.value === "expiring") {
|
||||||
|
// If the checkbox is not already checked, check it
|
||||||
|
if (!checkbox.checked) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
// Do the checkbox action
|
||||||
|
let event = new Event('change');
|
||||||
|
checkbox.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -149,6 +149,11 @@ footer {
|
||||||
color: color('primary');
|
color: color('primary');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-radio {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 1.06rem;
|
||||||
|
}
|
||||||
|
|
||||||
abbr[title] {
|
abbr[title] {
|
||||||
// workaround for underlining abbr element
|
// workaround for underlining abbr element
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|
|
@ -42,6 +42,9 @@ h2 {
|
||||||
.usa-form,
|
.usa-form,
|
||||||
.usa-form fieldset {
|
.usa-form fieldset {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
.usa-legend {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.p--blockquote {
|
.p--blockquote {
|
||||||
|
|
|
@ -69,9 +69,19 @@ def portfolio_permissions(request):
|
||||||
"has_organization_requests_flag": False,
|
"has_organization_requests_flag": False,
|
||||||
"has_organization_members_flag": False,
|
"has_organization_members_flag": False,
|
||||||
"is_portfolio_admin": False,
|
"is_portfolio_admin": False,
|
||||||
|
"has_domain_renewal_flag": False,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
|
|
||||||
|
# These feature flags will display and doesn't depend on portfolio
|
||||||
|
portfolio_context.update(
|
||||||
|
{
|
||||||
|
"has_organization_feature_flag": True,
|
||||||
|
"has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Linting: line too long
|
# Linting: line too long
|
||||||
view_suborg = request.user.has_view_suborganization_portfolio_permission(portfolio)
|
view_suborg = request.user.has_view_suborganization_portfolio_permission(portfolio)
|
||||||
edit_suborg = request.user.has_edit_suborganization_portfolio_permission(portfolio)
|
edit_suborg = request.user.has_edit_suborganization_portfolio_permission(portfolio)
|
||||||
|
@ -90,6 +100,7 @@ def portfolio_permissions(request):
|
||||||
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
|
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
|
||||||
"has_organization_members_flag": request.user.has_organization_members_flag(),
|
"has_organization_members_flag": request.user.has_organization_members_flag(),
|
||||||
"is_portfolio_admin": request.user.is_portfolio_admin(portfolio),
|
"is_portfolio_admin": request.user.is_portfolio_admin(portfolio),
|
||||||
|
"has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
|
||||||
}
|
}
|
||||||
return portfolio_context
|
return portfolio_context
|
||||||
|
|
||||||
|
|
|
@ -151,6 +151,27 @@ class UserFixture:
|
||||||
"email": "skey@truss.works",
|
"email": "skey@truss.works",
|
||||||
"title": "Designer",
|
"title": "Designer",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "f20b7a53-f40d-48f8-8c12-f42f35eede92",
|
||||||
|
"first_name": "Kimberly",
|
||||||
|
"last_name": "Aralar",
|
||||||
|
"email": "kimberly.aralar@gsa.gov",
|
||||||
|
"title": "Designer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "4aa78480-6272-42f9-ac29-a034ebdd9231",
|
||||||
|
"first_name": "Kaitlin",
|
||||||
|
"last_name": "Abbitt",
|
||||||
|
"email": "kaitlin.abbitt@cisa.dhs.gov",
|
||||||
|
"title": "Product Manager",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "5e54fd98-6c11-4cb3-82b6-93ed8be50a61",
|
||||||
|
"first_name": "Gina",
|
||||||
|
"last_name": "Summers",
|
||||||
|
"email": "gina.summers@ecstech.com",
|
||||||
|
"title": "Scrum Master",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
STAFF = [
|
STAFF = [
|
||||||
|
@ -257,6 +278,18 @@ class UserFixture:
|
||||||
"last_name": "Key-Analyst",
|
"last_name": "Key-Analyst",
|
||||||
"email": "skey+1@truss.works",
|
"email": "skey+1@truss.works",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "cf2b32fe-280d-4bc0-96c2-99eec09ba4da",
|
||||||
|
"first_name": "Kimberly-Analyst",
|
||||||
|
"last_name": "Aralar-Analyst",
|
||||||
|
"email": "kimberly.aralar+1@gsa.gov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "80db923e-ac64-4128-9b6f-e54b2174a09b",
|
||||||
|
"first_name": "Kaitlin-Analyst",
|
||||||
|
"last_name": "Abbitt-Analyst",
|
||||||
|
"email": "kaitlin.abbitt@gwe.cisa.dhs.gov",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Additional emails to add to the AllowedEmail whitelist.
|
# Additional emails to add to the AllowedEmail whitelist.
|
||||||
|
|
|
@ -530,7 +530,7 @@ class PurposeForm(RegistrarForm):
|
||||||
widget=forms.Textarea(
|
widget=forms.Textarea(
|
||||||
attrs={
|
attrs={
|
||||||
"aria-label": "What is the purpose of your requested domain? Describe how you’ll use your .gov domain. \
|
"aria-label": "What is the purpose of your requested domain? Describe how you’ll use your .gov domain. \
|
||||||
Will it be used for a website, email, or something else? You can enter up to 2000 characters."
|
Will it be used for a website, email, or something else?"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
validators=[
|
validators=[
|
||||||
|
@ -736,7 +736,13 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
|
||||||
required=True,
|
required=True,
|
||||||
# label has to end in a space to get the label_suffix to show
|
# label has to end in a space to get the label_suffix to show
|
||||||
label=("No other employees rationale"),
|
label=("No other employees rationale"),
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"aria-label": "You don’t need to provide names of other employees now, \
|
||||||
|
but it may slow down our assessment of your eligibility. Describe \
|
||||||
|
why there are no other employees who can help verify your request."
|
||||||
|
}
|
||||||
|
),
|
||||||
validators=[
|
validators=[
|
||||||
MaxLengthValidator(
|
MaxLengthValidator(
|
||||||
1000,
|
1000,
|
||||||
|
@ -784,7 +790,12 @@ class AnythingElseForm(BaseDeletableRegistrarForm):
|
||||||
anything_else = forms.CharField(
|
anything_else = forms.CharField(
|
||||||
required=True,
|
required=True,
|
||||||
label="Anything else?",
|
label="Anything else?",
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"aria-label": "Is there anything else you’d like us to know about your domain request? \
|
||||||
|
Provide details below. You can enter up to 2000 characters"
|
||||||
|
}
|
||||||
|
),
|
||||||
validators=[
|
validators=[
|
||||||
MaxLengthValidator(
|
MaxLengthValidator(
|
||||||
2000,
|
2000,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.core.validators import MaxLengthValidator
|
from django.core.validators import MaxLengthValidator
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
PortfolioInvitation,
|
PortfolioInvitation,
|
||||||
|
@ -271,3 +272,210 @@ class NewMemberForm(forms.ModelForm):
|
||||||
if admin_member_error in self.errors:
|
if admin_member_error in self.errors:
|
||||||
del self.errors[admin_member_error]
|
del self.errors[admin_member_error]
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class BasePortfolioMemberForm(forms.Form):
|
||||||
|
"""Base form for the PortfolioMemberForm and PortfolioInvitedMemberForm"""
|
||||||
|
|
||||||
|
# The label for each of these has a red "required" star. We can just embed that here for simplicity.
|
||||||
|
required_star = '<abbr class="usa-hint usa-hint--required" title="required">*</abbr>'
|
||||||
|
role = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
# Uses .value because the choice has a different label (on /admin)
|
||||||
|
(UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, "Admin access"),
|
||||||
|
(UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "Basic access"),
|
||||||
|
],
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
required=True,
|
||||||
|
error_messages={
|
||||||
|
"required": "Member access level is required",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
domain_request_permission_admin = forms.ChoiceField(
|
||||||
|
label=mark_safe(f"Select permission {required_star}"), # nosec
|
||||||
|
choices=[
|
||||||
|
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
|
||||||
|
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"),
|
||||||
|
],
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
required=False,
|
||||||
|
error_messages={
|
||||||
|
"required": "Admin domain request permission is required",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
member_permission_admin = forms.ChoiceField(
|
||||||
|
label=mark_safe(f"Select permission {required_star}"), # nosec
|
||||||
|
choices=[
|
||||||
|
(UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"),
|
||||||
|
(UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "View all members plus manage members"),
|
||||||
|
],
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
required=False,
|
||||||
|
error_messages={
|
||||||
|
"required": "Admin member permission is required",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
domain_request_permission_member = forms.ChoiceField(
|
||||||
|
label=mark_safe(f"Select permission {required_star}"), # nosec
|
||||||
|
choices=[
|
||||||
|
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
|
||||||
|
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"),
|
||||||
|
("no_access", "No access"),
|
||||||
|
],
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
required=False,
|
||||||
|
error_messages={
|
||||||
|
"required": "Basic member permission is required",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tracks what form elements are required for a given role choice.
|
||||||
|
# All of the fields included here have "required=False" by default as they are conditionally required.
|
||||||
|
# see def clean() for more details.
|
||||||
|
ROLE_REQUIRED_FIELDS = {
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||||
|
"domain_request_permission_admin",
|
||||||
|
"member_permission_admin",
|
||||||
|
],
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||||
|
"domain_request_permission_member",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, instance=None, **kwargs):
|
||||||
|
"""Initialize self.instance, self.initial, and descriptions under each radio button.
|
||||||
|
Uses map_instance_to_initial to set the initial dictionary."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if instance:
|
||||||
|
self.instance = instance
|
||||||
|
self.initial = self.map_instance_to_initial(self.instance)
|
||||||
|
# Adds a <p> description beneath each role option
|
||||||
|
self.fields["role"].descriptions = {
|
||||||
|
"organization_admin": UserPortfolioRoleChoices.get_role_description(
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||||
|
),
|
||||||
|
"organization_member": UserPortfolioRoleChoices.get_role_description(
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Saves self.instance by grabbing data from self.cleaned_data.
|
||||||
|
Uses map_cleaned_data_to_instance.
|
||||||
|
"""
|
||||||
|
self.instance = self.map_cleaned_data_to_instance(self.cleaned_data, self.instance)
|
||||||
|
self.instance.save()
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Validates form data based on selected role and its required fields."""
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
role = cleaned_data.get("role")
|
||||||
|
|
||||||
|
# Get required fields for the selected role. Then validate all required fields for the role.
|
||||||
|
required_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
|
||||||
|
for field_name in required_fields:
|
||||||
|
# Helpful error for if this breaks
|
||||||
|
if field_name not in self.fields:
|
||||||
|
raise ValueError(f"ROLE_REQUIRED_FIELDS referenced a non-existent field: {field_name}.")
|
||||||
|
|
||||||
|
if not cleaned_data.get(field_name):
|
||||||
|
self.add_error(field_name, self.fields.get(field_name).error_messages.get("required"))
|
||||||
|
|
||||||
|
# Edgecase: Member uses a special form value for None called "no_access".
|
||||||
|
if cleaned_data.get("domain_request_permission_member") == "no_access":
|
||||||
|
cleaned_data["domain_request_permission_member"] = None
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
# Explanation of how map_instance_to_initial / map_cleaned_data_to_instance work:
|
||||||
|
# map_instance_to_initial => called on init to set self.initial.
|
||||||
|
# Converts the incoming object (usually PortfolioInvitation or UserPortfolioPermission)
|
||||||
|
# into a dictionary representation for the form to use automatically.
|
||||||
|
|
||||||
|
# map_cleaned_data_to_instance => called on save() to save the instance to the db.
|
||||||
|
# Takes the self.cleaned_data dict, and converts this dict back to the object.
|
||||||
|
|
||||||
|
def map_instance_to_initial(self, instance):
|
||||||
|
"""
|
||||||
|
Maps self.instance to self.initial, handling roles and permissions.
|
||||||
|
Returns form data dictionary with appropriate permission levels based on user role:
|
||||||
|
{
|
||||||
|
"role": "organization_admin" or "organization_member",
|
||||||
|
"member_permission_admin": permission level if admin,
|
||||||
|
"domain_request_permission_admin": permission level if admin,
|
||||||
|
"domain_request_permission_member": permission level if member
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# Function variables
|
||||||
|
form_data = {}
|
||||||
|
perms = UserPortfolioPermission.get_portfolio_permissions(
|
||||||
|
instance.roles, instance.additional_permissions, get_list=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the available options for roles, domains, and member.
|
||||||
|
roles = [
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||||
|
]
|
||||||
|
domain_perms = [
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
]
|
||||||
|
member_perms = [
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Build form data based on role (which options are available).
|
||||||
|
# Get which one should be "selected" by assuming that EDIT takes precedence over view,
|
||||||
|
# and ADMIN takes precedence over MEMBER.
|
||||||
|
roles = instance.roles or []
|
||||||
|
selected_role = next((role for role in roles if role in roles), None)
|
||||||
|
form_data = {"role": selected_role}
|
||||||
|
is_admin = selected_role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||||
|
if is_admin:
|
||||||
|
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), None)
|
||||||
|
selected_member_permission = next((perm for perm in member_perms if perm in perms), None)
|
||||||
|
form_data["domain_request_permission_admin"] = selected_domain_permission
|
||||||
|
form_data["member_permission_admin"] = selected_member_permission
|
||||||
|
else:
|
||||||
|
# Edgecase: Member uses a special form value for None called "no_access". This ensures a form selection.
|
||||||
|
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), "no_access")
|
||||||
|
form_data["domain_request_permission_member"] = selected_domain_permission
|
||||||
|
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
def map_cleaned_data_to_instance(self, cleaned_data, instance):
|
||||||
|
"""
|
||||||
|
Maps self.cleaned_data to self.instance, setting roles and permissions.
|
||||||
|
Args:
|
||||||
|
cleaned_data (dict): Cleaned data containing role and permission choices
|
||||||
|
instance: Instance to update
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
instance: Updated instance
|
||||||
|
"""
|
||||||
|
role = cleaned_data.get("role")
|
||||||
|
|
||||||
|
# Handle roles
|
||||||
|
instance.roles = [role]
|
||||||
|
|
||||||
|
# Handle additional_permissions
|
||||||
|
valid_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
|
||||||
|
additional_permissions = {cleaned_data.get(field) for field in valid_fields if cleaned_data.get(field)}
|
||||||
|
|
||||||
|
# Handle EDIT permissions (should be accompanied with a view permission)
|
||||||
|
if UserPortfolioPermissionChoices.EDIT_MEMBERS in additional_permissions:
|
||||||
|
additional_permissions.add(UserPortfolioPermissionChoices.VIEW_MEMBERS)
|
||||||
|
|
||||||
|
if UserPortfolioPermissionChoices.EDIT_REQUESTS in additional_permissions:
|
||||||
|
additional_permissions.add(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||||
|
|
||||||
|
# Only set unique permissions not already defined in the base role
|
||||||
|
role_permissions = UserPortfolioPermission.get_portfolio_permissions(instance.roles, [], get_list=False)
|
||||||
|
instance.additional_permissions = list(additional_permissions - role_permissions)
|
||||||
|
return instance
|
||||||
|
|
|
@ -2,7 +2,7 @@ from itertools import zip_longest
|
||||||
import logging
|
import logging
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import re
|
import re
|
||||||
from datetime import date
|
from datetime import date, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ from .utility.time_stamped_model import TimeStampedModel
|
||||||
from .public_contact import PublicContact
|
from .public_contact import PublicContact
|
||||||
|
|
||||||
from .user_domain_role import UserDomainRole
|
from .user_domain_role import UserDomainRole
|
||||||
|
from waffle.decorators import flag_is_active
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -1152,14 +1153,29 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
now = timezone.now().date()
|
now = timezone.now().date()
|
||||||
return self.expiration_date < now
|
return self.expiration_date < now
|
||||||
|
|
||||||
def state_display(self):
|
def is_expiring(self):
|
||||||
|
"""
|
||||||
|
Check if the domain's expiration date is within 60 days.
|
||||||
|
Return True if domain expiration date exists and within 60 days
|
||||||
|
and otherwise False bc there's no expiration date meaning so not expiring
|
||||||
|
"""
|
||||||
|
if self.expiration_date is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = timezone.now().date()
|
||||||
|
|
||||||
|
threshold_date = now + timedelta(days=60)
|
||||||
|
return now < self.expiration_date <= threshold_date
|
||||||
|
|
||||||
|
def state_display(self, request=None):
|
||||||
"""Return the display status of the domain."""
|
"""Return the display status of the domain."""
|
||||||
if self.is_expired() and self.state != self.State.UNKNOWN:
|
if self.is_expired() and (self.state != self.State.UNKNOWN):
|
||||||
return "Expired"
|
return "Expired"
|
||||||
|
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
|
||||||
|
return "Expiring soon"
|
||||||
elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED:
|
elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED:
|
||||||
return "DNS needed"
|
return "DNS needed"
|
||||||
else:
|
return self.state.capitalize()
|
||||||
return self.state.capitalize()
|
|
||||||
|
|
||||||
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
|
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
|
||||||
"""Maps the Epp contact representation to a PublicContact object.
|
"""Maps the Epp contact representation to a PublicContact object.
|
||||||
|
|
|
@ -14,6 +14,8 @@ from .domain import Domain
|
||||||
from .domain_request import DomainRequest
|
from .domain_request import DomainRequest
|
||||||
from registrar.utility.waffle import flag_is_active_for_user
|
from registrar.utility.waffle import flag_is_active_for_user
|
||||||
from waffle.decorators import flag_is_active
|
from waffle.decorators import flag_is_active
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
|
@ -163,6 +165,20 @@ class User(AbstractUser):
|
||||||
active_requests_count = self.domain_requests_created.filter(status__in=allowed_states).count()
|
active_requests_count = self.domain_requests_created.filter(status__in=allowed_states).count()
|
||||||
return active_requests_count
|
return active_requests_count
|
||||||
|
|
||||||
|
def get_num_expiring_domains(self, request):
|
||||||
|
"""Return number of expiring domains"""
|
||||||
|
domain_ids = self.get_user_domain_ids(request)
|
||||||
|
now = timezone.now().date()
|
||||||
|
expiration_window = 60
|
||||||
|
threshold_date = now + timedelta(days=expiration_window)
|
||||||
|
num_of_expiring_domains = Domain.objects.filter(
|
||||||
|
id__in=domain_ids,
|
||||||
|
expiration_date__isnull=False,
|
||||||
|
expiration_date__lte=threshold_date,
|
||||||
|
expiration_date__gt=now,
|
||||||
|
).count()
|
||||||
|
return num_of_expiring_domains
|
||||||
|
|
||||||
def get_rejected_requests_count(self):
|
def get_rejected_requests_count(self):
|
||||||
"""Return count of rejected requests"""
|
"""Return count of rejected requests"""
|
||||||
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count()
|
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count()
|
||||||
|
@ -259,6 +275,9 @@ class User(AbstractUser):
|
||||||
def is_portfolio_admin(self, portfolio):
|
def is_portfolio_admin(self, portfolio):
|
||||||
return "Admin" in self.portfolio_role_summary(portfolio)
|
return "Admin" in self.portfolio_role_summary(portfolio)
|
||||||
|
|
||||||
|
def has_domain_renewal_flag(self):
|
||||||
|
return flag_is_active_for_user(self, "domain_renewal")
|
||||||
|
|
||||||
def get_first_portfolio(self):
|
def get_first_portfolio(self):
|
||||||
permission = self.portfolio_permissions.first()
|
permission = self.portfolio_permissions.first()
|
||||||
if permission:
|
if permission:
|
||||||
|
|
|
@ -110,8 +110,13 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
return self.get_portfolio_permissions(self.roles, self.additional_permissions)
|
return self.get_portfolio_permissions(self.roles, self.additional_permissions)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_portfolio_permissions(cls, roles, additional_permissions):
|
def get_portfolio_permissions(cls, roles, additional_permissions, get_list=True):
|
||||||
"""Class method to return a list of permissions based on roles and addtl permissions"""
|
"""Class method to return a list of permissions based on roles and addtl permissions.
|
||||||
|
Params:
|
||||||
|
roles => An array of roles
|
||||||
|
additional_permissions => An array of additional_permissions
|
||||||
|
get_list => If true, returns a list of perms. If false, returns a set of perms.
|
||||||
|
"""
|
||||||
# Use a set to avoid duplicate permissions
|
# Use a set to avoid duplicate permissions
|
||||||
portfolio_permissions = set()
|
portfolio_permissions = set()
|
||||||
if roles:
|
if roles:
|
||||||
|
@ -119,7 +124,7 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
portfolio_permissions.update(cls.PORTFOLIO_ROLE_PERMISSIONS.get(role, []))
|
portfolio_permissions.update(cls.PORTFOLIO_ROLE_PERMISSIONS.get(role, []))
|
||||||
if additional_permissions:
|
if additional_permissions:
|
||||||
portfolio_permissions.update(additional_permissions)
|
portfolio_permissions.update(additional_permissions)
|
||||||
return list(portfolio_permissions)
|
return list(portfolio_permissions) if get_list else portfolio_permissions
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_domain_request_permission_display(cls, roles, additional_permissions):
|
def get_domain_request_permission_display(cls, roles, additional_permissions):
|
||||||
|
|
|
@ -4,6 +4,9 @@ from django.apps import apps
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from registrar.utility.waffle import flag_is_active_for_user
|
from registrar.utility.waffle import flag_is_active_for_user
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UserPortfolioRoleChoices(models.TextChoices):
|
class UserPortfolioRoleChoices(models.TextChoices):
|
||||||
|
@ -16,7 +19,28 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_portfolio_role_label(cls, user_portfolio_role):
|
def get_user_portfolio_role_label(cls, user_portfolio_role):
|
||||||
return cls(user_portfolio_role).label if user_portfolio_role else None
|
try:
|
||||||
|
return cls(user_portfolio_role).label if user_portfolio_role else None
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Invalid portfolio role: {user_portfolio_role}")
|
||||||
|
return f"Unknown ({user_portfolio_role})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_role_description(cls, user_portfolio_role):
|
||||||
|
"""Returns a detailed description for a given role."""
|
||||||
|
descriptions = {
|
||||||
|
cls.ORGANIZATION_ADMIN: (
|
||||||
|
"Grants this member access to the organization-wide information "
|
||||||
|
"on domains, domain requests, and members. Domain management can be assigned separately."
|
||||||
|
),
|
||||||
|
cls.ORGANIZATION_MEMBER: (
|
||||||
|
"Grants this member access to the organization. They can be given extra permissions to view all "
|
||||||
|
"organization domain requests and submit domain requests on behalf of the organization. Basic access "
|
||||||
|
"members can’t view all members of an organization or manage them. "
|
||||||
|
"Domain management can be assigned separately."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return descriptions.get(user_portfolio_role)
|
||||||
|
|
||||||
|
|
||||||
class UserPortfolioPermissionChoices(models.TextChoices):
|
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||||
|
|
|
@ -2,15 +2,25 @@
|
||||||
class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
|
class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
|
||||||
{% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
|
{% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
|
||||||
>
|
>
|
||||||
{% if span_for_text %}
|
{% if legend_heading %}
|
||||||
<span>{{ field.label }}</span>
|
<h2 class="{{ legend_classes }}">{{ legend_heading }} </h2>
|
||||||
|
{% if widget.attrs.id == 'id_additional_details-has_cisa_representative' %}
|
||||||
|
<p>.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has <a href="https://www.cisa.gov/about/regions" target="_blank">10 regions</a> that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.</p>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field.label }}
|
{% if span_for_text %}
|
||||||
|
<span>{{ field.label }}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ field.label }}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if widget.attrs.required %}
|
{% if widget.attrs.required %}
|
||||||
<!--Don't add asterisk to one-field forms -->
|
|
||||||
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
|
{% if field.widget_type == 'radioselect' %}
|
||||||
|
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||||
|
<!--Don't add asterisk to one-field forms -->
|
||||||
|
{% elif field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." or field.label == "Has other contacts" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
{% load static custom_filters %}
|
||||||
|
|
||||||
<div class="{{ uswds_input_class }}">
|
<div class="{{ uswds_input_class }}">
|
||||||
{% for group, options, index in widget.optgroups %}
|
{% for group, options, index in widget.optgroups %}
|
||||||
{% if group %}<div><label>{{ group }}</label>{% endif %}
|
{% if group %}<div><label>{{ group }}</label>{% endif %}
|
||||||
|
@ -13,7 +15,17 @@
|
||||||
<label
|
<label
|
||||||
class="{{ uswds_input_class }}__label{% if label_classes %} {{ label_classes }}{% endif %}"
|
class="{{ uswds_input_class }}__label{% if label_classes %} {{ label_classes }}{% endif %}"
|
||||||
for="{{ option.attrs.id }}"
|
for="{{ option.attrs.id }}"
|
||||||
>{{ option.label }}</label>
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
{% comment %} Add a description on each, if available {% endcomment %}
|
||||||
|
{% if field and field.field and field.field.descriptions %}
|
||||||
|
{% with description=field.field.descriptions|get_dict_value:option.value %}
|
||||||
|
{% if description %}
|
||||||
|
<p class="margin-0 margin-top-1">{{ description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if group %}</div>{% endif %}
|
{% if group %}</div>{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -35,18 +35,27 @@
|
||||||
Status:
|
Status:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-primary-darker">
|
<span class="text-primary-darker">
|
||||||
|
|
||||||
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
|
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
|
||||||
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
|
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
|
||||||
Expired
|
Expired
|
||||||
|
{% elif has_domain_renewal_flag and domain.is_expiring %}
|
||||||
|
Expiring soon
|
||||||
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
|
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
|
||||||
DNS needed
|
DNS needed
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ domain.state|title }}
|
{{ domain.state|title }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% if domain.get_state_help_text %}
|
{% if domain.get_state_help_text %}
|
||||||
<div class="padding-top-1 text-primary-darker">
|
<div class="padding-top-1 text-primary-darker">
|
||||||
{{ domain.get_state_help_text }}
|
{% if has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
|
||||||
|
This domain will expire soon. <a href="/not-available-yet">Renew to maintain access.</a>
|
||||||
|
{% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
|
||||||
|
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
|
||||||
|
{% else %}
|
||||||
|
{{ domain.get_state_help_text }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -119,4 +128,4 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {# domain_content #}
|
{% endblock %} {# domain_content #}
|
|
@ -9,15 +9,9 @@
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
|
|
||||||
<fieldset class="usa-fieldset margin-top-2">
|
<fieldset class="usa-fieldset">
|
||||||
<legend>
|
|
||||||
<h2>Are you working with a CISA regional representative on your domain request?</h2>
|
|
||||||
<p>.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has <a href="https://www.cisa.gov/about/regions" target="_blank">10 regions</a> that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.</p>
|
|
||||||
</legend>
|
|
||||||
|
|
||||||
<!-- Toggle -->
|
<!-- Toggle -->
|
||||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
{% with add_class="usa-radio__input--tile" add_legend_class="margin-top-0" add_legend_heading="Are you working with a CISA regional representative on your domain request?" %}
|
||||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
|
||||||
{% input_with_errors forms.0.has_cisa_representative %}
|
{% input_with_errors forms.0.has_cisa_representative %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
||||||
|
@ -31,13 +25,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset class="usa-fieldset margin-top-2">
|
<fieldset class="usa-fieldset margin-top-2">
|
||||||
<legend>
|
|
||||||
<h2>Is there anything else you’d like us to know about your domain request?</h2>
|
|
||||||
</legend>
|
|
||||||
|
|
||||||
<!-- Toggle -->
|
<!-- Toggle -->
|
||||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
{% with add_class="usa-radio__input--tile" add_legend_heading="Is there anything else you’d like us to know about your domain request?" %}
|
||||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
|
||||||
{% input_with_errors forms.2.has_anything_else_text %}
|
{% input_with_errors forms.2.has_anything_else_text %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
||||||
|
@ -45,7 +34,7 @@
|
||||||
|
|
||||||
<div class="margin-top-3" id="anything-else">
|
<div class="margin-top-3" id="anything-else">
|
||||||
<p>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
|
<p>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
|
||||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
{% with attr_maxlength=2000 add_label_class="usa-sr-only" add_legend_class="usa-sr-only" add_legend_heading="Is there anything else you’d like us to know about your domain request?" add_aria_label="Provide details below. You can enter up to 2000 characters" %}
|
||||||
{% input_with_errors forms.3.anything_else %}
|
{% input_with_errors forms.3.anything_else %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
|
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<li><strong>We typically don’t reach out to these employees</strong>, but if contact is necessary, our practice is to coordinate with you first.</li>
|
<li><strong>We typically don’t reach out to these employees</strong>, but if contact is necessary, our practice is to coordinate with you first.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
{% include "includes/required_fields.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_required_fields_help_text %}
|
{% block form_required_fields_help_text %}
|
||||||
|
@ -17,20 +17,14 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
<fieldset class="usa-fieldset margin-top-2">
|
<div class="margin-top-2">
|
||||||
<legend>
|
{% with add_class="usa-radio__input--tile" add_legend_heading="Are there other employees who can help verify your request?" %}
|
||||||
<h2>Are there other employees who can help verify your request?</h2>
|
|
||||||
</legend>
|
|
||||||
|
|
||||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
|
||||||
{% input_with_errors forms.0.has_other_contacts %}
|
{% input_with_errors forms.0.has_other_contacts %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #}
|
{# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #}
|
||||||
|
</div>
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div id="other-employees" class="other-contacts-form">
|
<div id="other-employees" class="other-contacts-form">
|
||||||
{% include "includes/required_fields.html" %}
|
|
||||||
{{ forms.1.management_form }}
|
{{ forms.1.management_form }}
|
||||||
{# forms.1 is a formset and this iterates over its forms #}
|
{# forms.1 is a formset and this iterates over its forms #}
|
||||||
{% for form in forms.1.forms %}
|
{% for form in forms.1.forms %}
|
||||||
|
|
|
@ -54,11 +54,14 @@
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}" id="export-csv">
|
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}" id="export-csv">
|
||||||
<section aria-label="Domain Requests report component" class="margin-top-205">
|
<section aria-label="Domain Requests report component" class="margin-top-205">
|
||||||
<a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
|
<!----------------------------------------------------------------------
|
||||||
|
This link is commented out because we intend to add it back in later.
|
||||||
|
------------------------------------------------------------------------->
|
||||||
|
<!-- <a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
|
||||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
|
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
|
||||||
</svg>Export as CSV
|
</svg>Export as CSV
|
||||||
</a>
|
</a> -->
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,10 +1,30 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||||
{% url 'get_domains_json' as url %}
|
{% url 'get_domains_json' as url %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span id="get_domains_json_url" class="display-none">{{url}}</span>
|
<span id="get_domains_json_url" class="display-none">{{url}}</span>
|
||||||
|
|
||||||
|
<!-- Org model banner (org manager can view, domain manager can edit) -->
|
||||||
|
{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
|
||||||
|
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||||
|
<div class="usa-alert">
|
||||||
|
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
|
||||||
|
<p class="usa-alert__text maxw-none">
|
||||||
|
{% if num_expiring_domains == 1%}
|
||||||
|
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
|
||||||
|
{% else%}
|
||||||
|
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<section class="section-outlined domains margin-top-0{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domains">
|
<section class="section-outlined domains margin-top-0{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domains">
|
||||||
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||||
{% if not portfolio %}
|
{% if not portfolio %}
|
||||||
|
@ -53,7 +73,24 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if portfolio %}
|
|
||||||
|
<!-- Non org model banner -->
|
||||||
|
{% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
|
||||||
|
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||||
|
<div class="usa-alert">
|
||||||
|
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
|
||||||
|
<p class="usa-alert__text maxw-none">
|
||||||
|
{% if num_expiring_domains == 1%}
|
||||||
|
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
|
||||||
|
{% else%}
|
||||||
|
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="display-flex flex-align-center">
|
<div class="display-flex flex-align-center">
|
||||||
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
|
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
|
||||||
<div class="usa-accordion usa-accordion--select margin-right-2">
|
<div class="usa-accordion usa-accordion--select margin-right-2">
|
||||||
|
@ -135,6 +172,19 @@
|
||||||
>Deleted</label
|
>Deleted</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
{% if has_domain_renewal_flag and num_expiring_domains > 0 %}
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-expiring"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="expiring"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-expiring"
|
||||||
|
>Expiring soon</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -149,7 +199,6 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domains__table-wrapper">
|
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domains__table-wrapper">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||||
<caption class="sr-only">Your registered domains</caption>
|
<caption class="sr-only">Your registered domains</caption>
|
||||||
|
@ -200,4 +249,4 @@
|
||||||
<ul class="usa-pagination__list">
|
<ul class="usa-pagination__list">
|
||||||
<!-- Pagination links will be dynamically populated by JS -->
|
<!-- Pagination links will be dynamically populated by JS -->
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
|
@ -71,7 +71,11 @@ error messages, if necessary.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# this is the input field, itself #}
|
{# this is the input field, itself #}
|
||||||
{% include widget.template_name %}
|
{% with aria_label=aria_label %}
|
||||||
|
{% include widget.template_name %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if append_gov %}
|
{% if append_gov %}
|
||||||
<span class="padding-top-05 padding-left-2px">.gov </span>
|
<span class="padding-top-05 padding-left-2px">.gov </span>
|
||||||
|
|
|
@ -15,6 +15,6 @@
|
||||||
|
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
<h1 id="domains-header">Domains</h1>
|
<h1 id="domains-header">Domains</h1>
|
||||||
{% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}
|
{% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count num_expiring_domains=num_expiring_domains%}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,42 +1,132 @@
|
||||||
{% extends 'portfolio_base.html' %}
|
{% extends 'portfolio_base.html' %}
|
||||||
{% load static field_helpers%}
|
{% load static url_helpers %}
|
||||||
|
{% load field_helpers %}
|
||||||
|
|
||||||
{% block title %}Organization member {% endblock %}
|
{% block title %}Organization member{% endblock %}
|
||||||
|
|
||||||
{% load static %}
|
{% block wrapper_class %}
|
||||||
|
{{ block.super }} dashboard--grey-1
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
<div class="grid-row grid-gap">
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
<div class="tablet:grid-col-9" id="main-content">
|
|
||||||
|
|
||||||
{% block messages %}
|
<!-- Navigation breadcrumbs -->
|
||||||
{% include "includes/form_messages.html" %}
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||||
{% endblock %}
|
<ol class="usa-breadcrumb__list">
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
<a href="{% url 'members' %}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
{% if member %}
|
||||||
|
{% url 'member' pk=member.pk as back_url %}
|
||||||
|
{% elif invitation %}
|
||||||
|
{% url 'invitedmember' pk=invitation.pk as back_url %}
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ back_url }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
||||||
|
</li>
|
||||||
|
{% comment %} Manage members {% endcomment %}
|
||||||
|
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||||
|
<span>Member access and permissions</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<h1>Manage member</h1>
|
<!-- Page header -->
|
||||||
|
<h1>Member access and permissions</h1>
|
||||||
<p>
|
|
||||||
{% if member %}
|
|
||||||
{{ member.email }}
|
|
||||||
{% elif invitation %}
|
|
||||||
{{ invitation.email }}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr>
|
{% include "includes/required_fields.html" with remove_margin_top=True %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
<form class="usa-form usa-form--large" method="post" id="member_form" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% input_with_errors form.roles %}
|
<fieldset class="usa-fieldset">
|
||||||
{% input_with_errors form.additional_permissions %}
|
<legend>
|
||||||
<button
|
{% if member and member.email or invitation and invitation.email %}
|
||||||
type="submit"
|
<h2 class="margin-top-1">Member email</h2>
|
||||||
class="usa-button"
|
{% else %}
|
||||||
>Submit</button>
|
<h2 class="margin-top-1">Member</h2>
|
||||||
</form>
|
{% endif %}
|
||||||
|
</legend>
|
||||||
|
<p class="margin-top-0">
|
||||||
|
{% comment %}
|
||||||
|
Show member email if possible, then invitation email.
|
||||||
|
If neither of these are true, show the name or as a last resort just "None".
|
||||||
|
{% endcomment %}
|
||||||
|
{% if member %}
|
||||||
|
{% if member.email %}
|
||||||
|
{{ member.email }}
|
||||||
|
{% else %}
|
||||||
|
{{ member.get_formatted_name }}
|
||||||
|
{% endif %}
|
||||||
|
{% elif invitation %}
|
||||||
|
{% if invitation.email %}
|
||||||
|
{{ invitation.email }}
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<!-- Member email -->
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Member access radio buttons (Toggles other sections) -->
|
||||||
|
<fieldset class="usa-fieldset">
|
||||||
|
<legend>
|
||||||
|
<h2 class="margin-top-0">Member Access</h2>
|
||||||
|
</legend>
|
||||||
|
|
||||||
</div>
|
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||||
|
{% input_with_errors form.role %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Admin access form -->
|
||||||
|
<div id="member-admin-permissions" class="margin-top-2">
|
||||||
|
<h2>Admin access permissions</h2>
|
||||||
|
<p>Member permissions available for admin-level acccess.</p>
|
||||||
|
|
||||||
|
<h3 class="summary-item__title
|
||||||
|
text-primary-dark
|
||||||
|
margin-bottom-0">Organization domain requests</h3>
|
||||||
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||||
|
{% input_with_errors form.domain_request_permission_admin %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<h3 class="summary-item__title
|
||||||
|
text-primary-dark
|
||||||
|
margin-bottom-0
|
||||||
|
margin-top-3">Organization members</h3>
|
||||||
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||||
|
{% input_with_errors form.member_permission_admin %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Basic access form -->
|
||||||
|
<div id="member-basic-permissions" class="margin-top-2">
|
||||||
|
<h2>Basic member permissions</h2>
|
||||||
|
<p>Member permissions available for basic-level acccess.</p>
|
||||||
|
|
||||||
|
<h3 class="margin-bottom-0 summary-item__title text-primary-dark">Organization domain requests</h3>
|
||||||
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||||
|
{% input_with_errors form.domain_request_permission_member %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit/cancel buttons -->
|
||||||
|
<div class="margin-top-3">
|
||||||
|
<a
|
||||||
|
type="button"
|
||||||
|
href="{{ back_url }}"
|
||||||
|
class="usa-button usa-button--outline"
|
||||||
|
name="btn-cancel-click"
|
||||||
|
aria-label="Cancel editing member"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="usa-button">Update Member</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock portfolio_content%}
|
||||||
|
|
|
@ -5,12 +5,6 @@
|
||||||
{% block title %} Domains | {% endblock %}
|
{% block title %} Domains | {% endblock %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
{% block messages %}
|
|
||||||
{% include "includes/form_messages.html" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
<h1 id="domains-header">Domains</h1>
|
<h1 id="domains-header">Domains</h1>
|
||||||
<section class="section-outlined">
|
<section class="section-outlined">
|
||||||
|
|
|
@ -282,3 +282,11 @@ def display_requesting_entity(domain_request):
|
||||||
)
|
)
|
||||||
|
|
||||||
return display
|
return display
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def get_dict_value(dictionary, key):
|
||||||
|
"""Get a value from a dictionary. Returns a string on empty."""
|
||||||
|
if isinstance(dictionary, dict):
|
||||||
|
return dictionary.get(key, "")
|
||||||
|
return ""
|
||||||
|
|
|
@ -57,6 +57,7 @@ def input_with_errors(context, field=None): # noqa: C901
|
||||||
legend_classes = []
|
legend_classes = []
|
||||||
group_classes = []
|
group_classes = []
|
||||||
aria_labels = []
|
aria_labels = []
|
||||||
|
legend_headings = []
|
||||||
sublabel_text = []
|
sublabel_text = []
|
||||||
|
|
||||||
# this will be converted to an attribute string
|
# this will be converted to an attribute string
|
||||||
|
@ -91,6 +92,8 @@ def input_with_errors(context, field=None): # noqa: C901
|
||||||
label_classes.append(value)
|
label_classes.append(value)
|
||||||
elif key == "add_legend_class":
|
elif key == "add_legend_class":
|
||||||
legend_classes.append(value)
|
legend_classes.append(value)
|
||||||
|
elif key == "add_legend_heading":
|
||||||
|
legend_headings.append(value)
|
||||||
|
|
||||||
elif key == "add_group_class":
|
elif key == "add_group_class":
|
||||||
group_classes.append(value)
|
group_classes.append(value)
|
||||||
|
@ -120,9 +123,6 @@ def input_with_errors(context, field=None): # noqa: C901
|
||||||
else:
|
else:
|
||||||
context["label_tag"] = "label"
|
context["label_tag"] = "label"
|
||||||
|
|
||||||
if field.use_fieldset:
|
|
||||||
label_classes.append("usa-legend")
|
|
||||||
|
|
||||||
if field.widget_type == "checkbox":
|
if field.widget_type == "checkbox":
|
||||||
label_classes.append("usa-checkbox__label")
|
label_classes.append("usa-checkbox__label")
|
||||||
elif not field.use_fieldset:
|
elif not field.use_fieldset:
|
||||||
|
@ -153,6 +153,9 @@ def input_with_errors(context, field=None): # noqa: C901
|
||||||
if legend_classes:
|
if legend_classes:
|
||||||
context["legend_classes"] = " ".join(legend_classes)
|
context["legend_classes"] = " ".join(legend_classes)
|
||||||
|
|
||||||
|
if legend_headings:
|
||||||
|
context["legend_heading"] = " ".join(legend_headings)
|
||||||
|
|
||||||
if group_classes:
|
if group_classes:
|
||||||
context["group_classes"] = " ".join(group_classes)
|
context["group_classes"] = " ".join(group_classes)
|
||||||
|
|
||||||
|
|
|
@ -1731,9 +1731,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"cisa_representative_first_name",
|
"cisa_representative_first_name",
|
||||||
"cisa_representative_last_name",
|
"cisa_representative_last_name",
|
||||||
"cisa_representative_email",
|
"cisa_representative_email",
|
||||||
"requested_suborganization",
|
|
||||||
"suborganization_city",
|
|
||||||
"suborganization_state_territory",
|
|
||||||
]
|
]
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
@ -1967,6 +1964,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Grab the current list of table filters
|
# Grab the current list of table filters
|
||||||
readonly_fields = self.admin.get_list_filter(request)
|
readonly_fields = self.admin.get_list_filter(request)
|
||||||
expected_fields = (
|
expected_fields = (
|
||||||
|
DomainRequestAdmin.PortfolioFilter,
|
||||||
DomainRequestAdmin.StatusListFilter,
|
DomainRequestAdmin.StatusListFilter,
|
||||||
DomainRequestAdmin.GenericOrgFilter,
|
DomainRequestAdmin.GenericOrgFilter,
|
||||||
DomainRequestAdmin.FederalTypeFilter,
|
DomainRequestAdmin.FederalTypeFilter,
|
||||||
|
|
|
@ -7,7 +7,7 @@ This file tests the various ways in which the registrar interacts with the regis
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from unittest.mock import MagicMock, patch, call
|
from unittest.mock import MagicMock, patch, call
|
||||||
import datetime
|
from datetime import datetime, date, timedelta
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
from registrar.models import Domain, Host, HostIP
|
from registrar.models import Domain, Host, HostIP
|
||||||
|
@ -2267,13 +2267,13 @@ class TestExpirationDate(MockEppLib):
|
||||||
"""assert that the setter for expiration date is not implemented and will raise error"""
|
"""assert that the setter for expiration date is not implemented and will raise error"""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
self.domain.registry_expiration_date = datetime.date.today()
|
self.domain.registry_expiration_date = date.today()
|
||||||
|
|
||||||
def test_renew_domain(self):
|
def test_renew_domain(self):
|
||||||
"""assert that the renew_domain sets new expiration date in cache and saves to registrar"""
|
"""assert that the renew_domain sets new expiration date in cache and saves to registrar"""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
self.domain.renew_domain()
|
self.domain.renew_domain()
|
||||||
test_date = datetime.date(2023, 5, 25)
|
test_date = date(2023, 5, 25)
|
||||||
self.assertEquals(self.domain._cache["ex_date"], test_date)
|
self.assertEquals(self.domain._cache["ex_date"], test_date)
|
||||||
self.assertEquals(self.domain.expiration_date, test_date)
|
self.assertEquals(self.domain.expiration_date, test_date)
|
||||||
|
|
||||||
|
@ -2295,18 +2295,42 @@ class TestExpirationDate(MockEppLib):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# to do this, need to mock value returned from timezone.now
|
# to do this, need to mock value returned from timezone.now
|
||||||
# set now to 2023-01-01
|
# set now to 2023-01-01
|
||||||
mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0)
|
mocked_datetime = datetime(2023, 1, 1, 12, 0, 0)
|
||||||
# force fetch_cache which sets the expiration date to 2023-05-25
|
# force fetch_cache which sets the expiration date to 2023-05-25
|
||||||
self.domain.statuses
|
self.domain.statuses
|
||||||
with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
|
with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
|
||||||
self.assertFalse(self.domain.is_expired())
|
self.assertFalse(self.domain.is_expired())
|
||||||
|
|
||||||
|
def test_is_expiring_within_threshold(self):
|
||||||
|
"""assert that is_expiring returns true when expiration date is within 60 days"""
|
||||||
|
with less_console_noise():
|
||||||
|
mocked_datetime = datetime(2023, 1, 1, 12, 0, 0)
|
||||||
|
expiration_date = mocked_datetime.date() + timedelta(days=30)
|
||||||
|
|
||||||
|
# set domain's expiration date
|
||||||
|
self.domain.expiration_date = expiration_date
|
||||||
|
|
||||||
|
with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
|
||||||
|
self.assertTrue(self.domain.is_expiring())
|
||||||
|
|
||||||
|
def test_is_not_expiring_outside_threshold(self):
|
||||||
|
"""assert that is_expiring returns false when expiration date is outside 60 days"""
|
||||||
|
with less_console_noise():
|
||||||
|
mocked_datetime = datetime(2023, 1, 1, 12, 0, 0)
|
||||||
|
expiration_date = mocked_datetime.date() + timedelta(days=61)
|
||||||
|
|
||||||
|
# set domain's expiration date
|
||||||
|
self.domain.expiration_date = expiration_date
|
||||||
|
|
||||||
|
with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
|
||||||
|
self.assertFalse(self.domain.is_expiring())
|
||||||
|
|
||||||
def test_expiration_date_updated_on_info_domain_call(self):
|
def test_expiration_date_updated_on_info_domain_call(self):
|
||||||
"""assert that expiration date in db is updated on info domain call"""
|
"""assert that expiration date in db is updated on info domain call"""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# force fetch_cache to be called
|
# force fetch_cache to be called
|
||||||
self.domain.statuses
|
self.domain.statuses
|
||||||
test_date = datetime.date(2023, 5, 25)
|
test_date = date(2023, 5, 25)
|
||||||
self.assertEquals(self.domain.expiration_date, test_date)
|
self.assertEquals(self.domain.expiration_date, test_date)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2322,7 +2346,7 @@ class TestCreationDate(MockEppLib):
|
||||||
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)
|
||||||
# creation_date returned from mockDataInfoDomain with creation date:
|
# creation_date returned from mockDataInfoDomain with creation date:
|
||||||
# cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35)
|
# cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35)
|
||||||
self.creation_date = make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35))
|
self.creation_date = make_aware(datetime(2023, 5, 25, 19, 45, 35))
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
|
@ -2331,7 +2355,7 @@ class TestCreationDate(MockEppLib):
|
||||||
def test_creation_date_setter_not_implemented(self):
|
def test_creation_date_setter_not_implemented(self):
|
||||||
"""assert that the setter for creation date is not implemented and will raise error"""
|
"""assert that the setter for creation date is not implemented and will raise error"""
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
self.domain.creation_date = datetime.date.today()
|
self.domain.creation_date = date.today()
|
||||||
|
|
||||||
def test_creation_date_updated_on_info_domain_call(self):
|
def test_creation_date_updated_on_info_domain_call(self):
|
||||||
"""assert that creation date in db is updated on info domain call"""
|
"""assert that creation date in db is updated on info domain call"""
|
||||||
|
|
|
@ -71,8 +71,8 @@ class CsvReportsTest(MockDbForSharedTests):
|
||||||
fake_open = mock_open()
|
fake_open = mock_open()
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
|
|
||||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
]
|
]
|
||||||
|
@ -93,8 +93,8 @@ class CsvReportsTest(MockDbForSharedTests):
|
||||||
fake_open = mock_open()
|
fake_open = mock_open()
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
|
|
||||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
|
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
|
||||||
|
@ -493,17 +493,17 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# sorted alphabetially by domain name
|
# sorted alphabetially by domain name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||||
"defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
|
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||||
"cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
|
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
|
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
||||||
"ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
|
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||||
"zdomain12.gov,Interstate,,,,,(blank)\n"
|
"zdomain12.gov,Interstate,,,,,(blank)\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
self.maxDiff = None
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -533,16 +533,16 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# sorted alphabetially by domain name
|
# sorted alphabetially by domain name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||||
"defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
|
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||||
"cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
|
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
|
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
||||||
"ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
|
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
self.maxDiff = None
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
|
|
@ -424,6 +424,112 @@ class TestDomainDetail(TestDomainOverview):
|
||||||
self.assertContains(detail_page, "invited@example.com")
|
self.assertContains(detail_page, "invited@example.com")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomainDetailDomainRenewal(TestDomainOverview):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.user = get_user_model().objects.create(
|
||||||
|
first_name="User",
|
||||||
|
last_name="Test",
|
||||||
|
email="bogus@example.gov",
|
||||||
|
phone="8003111234",
|
||||||
|
title="test title",
|
||||||
|
username="usertest",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.expiringdomain, _ = Domain.objects.get_or_create(
|
||||||
|
name="expiringdomain.gov",
|
||||||
|
)
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.expiringdomain, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
DomainInformation.objects.get_or_create(creator=self.user, domain=self.expiringdomain)
|
||||||
|
|
||||||
|
self.portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user)
|
||||||
|
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
def custom_is_expired(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def custom_is_expiring(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
def test_expiring_domain_on_detail_page_as_domain_manager(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
|
||||||
|
Domain, "is_expired", self.custom_is_expired
|
||||||
|
):
|
||||||
|
self.assertEquals(self.expiringdomain.state, Domain.State.UNKNOWN)
|
||||||
|
detail_page = self.client.get(
|
||||||
|
reverse("domain", kwargs={"pk": self.expiringdomain.id}),
|
||||||
|
)
|
||||||
|
self.assertContains(detail_page, "Expiring soon")
|
||||||
|
|
||||||
|
self.assertContains(detail_page, "Renew to maintain access")
|
||||||
|
|
||||||
|
self.assertNotContains(detail_page, "DNS needed")
|
||||||
|
self.assertNotContains(detail_page, "Expired")
|
||||||
|
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_expiring_domain_on_detail_page_in_org_model_as_a_non_domain_manager(self):
|
||||||
|
portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user)
|
||||||
|
non_dom_manage_user = get_user_model().objects.create(
|
||||||
|
first_name="Non Domain",
|
||||||
|
last_name="Manager",
|
||||||
|
email="verybogus@example.gov",
|
||||||
|
phone="8003111234",
|
||||||
|
title="test title again",
|
||||||
|
username="nondomain",
|
||||||
|
)
|
||||||
|
|
||||||
|
non_dom_manage_user.save()
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=non_dom_manage_user,
|
||||||
|
portfolio=portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
expiringdomain2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov")
|
||||||
|
DomainInformation.objects.get_or_create(
|
||||||
|
creator=non_dom_manage_user, domain=expiringdomain2, portfolio=self.portfolio
|
||||||
|
)
|
||||||
|
non_dom_manage_user.refresh_from_db()
|
||||||
|
self.client.force_login(non_dom_manage_user)
|
||||||
|
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
|
||||||
|
Domain, "is_expired", self.custom_is_expired
|
||||||
|
):
|
||||||
|
detail_page = self.client.get(
|
||||||
|
reverse("domain", kwargs={"pk": expiringdomain2.id}),
|
||||||
|
)
|
||||||
|
self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.")
|
||||||
|
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self):
|
||||||
|
portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org2", creator=self.user)
|
||||||
|
|
||||||
|
expiringdomain3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov")
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(user=self.user, domain=expiringdomain3, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
DomainInformation.objects.get_or_create(creator=self.user, domain=expiringdomain3, portfolio=portfolio)
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
|
||||||
|
Domain, "is_expired", self.custom_is_expired
|
||||||
|
):
|
||||||
|
detail_page = self.client.get(
|
||||||
|
reverse("domain", kwargs={"pk": expiringdomain3.id}),
|
||||||
|
)
|
||||||
|
self.assertContains(detail_page, "Renew to maintain access")
|
||||||
|
|
||||||
|
|
||||||
class TestDomainManagers(TestDomainOverview):
|
class TestDomainManagers(TestDomainOverview):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
@ -2348,3 +2454,125 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
||||||
|
|
||||||
# Check that an email was not sent
|
# Check that an email was not sent
|
||||||
self.assertFalse(self.mock_client.send_email.called)
|
self.assertFalse(self.mock_client.send_email.called)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomainRenewal(TestWithUser):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
today = datetime.now()
|
||||||
|
expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||||
|
expiring_date_current = (today + timedelta(days=70)).strftime("%Y-%m-%d")
|
||||||
|
expired_date = (today - timedelta(days=30)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
self.domain_with_expiring_soon_date, _ = Domain.objects.get_or_create(
|
||||||
|
name="igorville.gov", expiration_date=expiring_date
|
||||||
|
)
|
||||||
|
self.domain_with_expired_date, _ = Domain.objects.get_or_create(
|
||||||
|
name="domainwithexpireddate.com", expiration_date=expired_date
|
||||||
|
)
|
||||||
|
|
||||||
|
self.domain_with_current_date, _ = Domain.objects.get_or_create(
|
||||||
|
name="domainwithfarexpireddate.com", expiration_date=expiring_date_current
|
||||||
|
)
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain_with_current_date, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain_with_expired_date, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain_with_expiring_soon_date, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
try:
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
# Remove test_without_domain_renewal_flag when domain renewal is released as a feature
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("domain_renewal", active=False)
|
||||||
|
def test_without_domain_renewal_flag(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
domains_page = self.client.get("/")
|
||||||
|
self.assertNotContains(domains_page, "will expire soon")
|
||||||
|
self.assertNotContains(domains_page, "Expiring soon")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
def test_domain_renewal_flag_single_domain(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
domains_page = self.client.get("/")
|
||||||
|
self.assertContains(domains_page, "One domain will expire soon")
|
||||||
|
self.assertContains(domains_page, "Expiring soon")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
def test_with_domain_renewal_flag_mulitple_domains(self):
|
||||||
|
today = datetime.now()
|
||||||
|
expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||||
|
self.domain_with_another_expiring, _ = Domain.objects.get_or_create(
|
||||||
|
name="domainwithanotherexpiringdate.com", expiration_date=expiring_date
|
||||||
|
)
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain_with_another_expiring, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
domains_page = self.client.get("/")
|
||||||
|
self.assertContains(domains_page, "Multiple domains will expire soon")
|
||||||
|
self.assertContains(domains_page, "Expiring soon")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
def test_with_domain_renewal_flag_no_expiring_domains(self):
|
||||||
|
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete()
|
||||||
|
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
domains_page = self.client.get("/")
|
||||||
|
self.assertNotContains(domains_page, "Expiring soon")
|
||||||
|
self.assertNotContains(domains_page, "will expire soon")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_domain_renewal_flag_single_domain_w_org_feature_flag(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
domains_page = self.client.get("/")
|
||||||
|
self.assertContains(domains_page, "One domain will expire soon")
|
||||||
|
self.assertContains(domains_page, "Expiring soon")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_with_domain_renewal_flag_mulitple_domains_w_org_feature_flag(self):
|
||||||
|
today = datetime.now()
|
||||||
|
expiring_date = (today + timedelta(days=31)).strftime("%Y-%m-%d")
|
||||||
|
self.domain_with_another_expiring_org_model, _ = Domain.objects.get_or_create(
|
||||||
|
name="domainwithanotherexpiringdate_orgmodel.com", expiration_date=expiring_date
|
||||||
|
)
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain_with_another_expiring_org_model, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
domains_page = self.client.get("/")
|
||||||
|
self.assertContains(domains_page, "Multiple domains will expire soon")
|
||||||
|
self.assertContains(domains_page, "Expiring soon")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("domain_renewal", active=True)
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_with_domain_renewal_flag_no_expiring_domains_w_org_feature_flag(self):
|
||||||
|
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete()
|
||||||
|
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
domains_page = self.client.get("/")
|
||||||
|
self.assertNotContains(domains_page, "Expiring soon")
|
||||||
|
self.assertNotContains(domains_page, "will expire soon")
|
||||||
|
|
|
@ -8,24 +8,34 @@ from django_webtest import WebTest # type: ignore
|
||||||
from django.utils.dateparse import parse_date
|
from django.utils.dateparse import parse_date
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
from waffle.testutils import override_flag
|
from waffle.testutils import override_flag
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
class GetDomainsJsonTest(TestWithUser, WebTest):
|
class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
today = datetime.now()
|
||||||
|
expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||||
|
expiring_date_2 = (today + timedelta(days=31)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
# Create test domains
|
# Create test domains
|
||||||
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="unknown")
|
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="unknown")
|
||||||
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="dns needed")
|
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="dns needed")
|
||||||
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready")
|
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready")
|
||||||
self.domain4 = Domain.objects.create(name="example4.com", expiration_date="2024-03-01", state="ready")
|
self.domain4 = Domain.objects.create(name="example4.com", expiration_date="2024-03-01", state="ready")
|
||||||
|
self.domain5 = Domain.objects.create(name="example5.com", expiration_date=expiring_date, state="expiring soon")
|
||||||
|
self.domain6 = Domain.objects.create(
|
||||||
|
name="example6.com", expiration_date=expiring_date_2, state="expiring soon"
|
||||||
|
)
|
||||||
# Create UserDomainRoles
|
# Create UserDomainRoles
|
||||||
UserDomainRole.objects.create(user=self.user, domain=self.domain1)
|
UserDomainRole.objects.create(user=self.user, domain=self.domain1)
|
||||||
UserDomainRole.objects.create(user=self.user, domain=self.domain2)
|
UserDomainRole.objects.create(user=self.user, domain=self.domain2)
|
||||||
UserDomainRole.objects.create(user=self.user, domain=self.domain3)
|
UserDomainRole.objects.create(user=self.user, domain=self.domain3)
|
||||||
|
|
||||||
|
UserDomainRole.objects.create(user=self.user, domain=self.domain5)
|
||||||
|
UserDomainRole.objects.create(user=self.user, domain=self.domain6)
|
||||||
|
|
||||||
# Create Portfolio
|
# Create Portfolio
|
||||||
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Example org")
|
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Example org")
|
||||||
|
|
||||||
|
@ -63,7 +73,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 3)
|
self.assertEqual(len(data["domains"]), 5)
|
||||||
|
|
||||||
# Expected domains
|
# Expected domains
|
||||||
expected_domains = [self.domain1, self.domain2, self.domain3]
|
expected_domains = [self.domain1, self.domain2, self.domain3]
|
||||||
|
@ -310,7 +320,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 1)
|
self.assertEqual(data["total"], 1)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 5)
|
||||||
|
|
||||||
# Check the number of domain requests
|
# Check the number of domain requests
|
||||||
self.assertEqual(len(data["domains"]), 1)
|
self.assertEqual(len(data["domains"]), 1)
|
||||||
|
@ -377,14 +387,15 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_state_filtering(self):
|
def test_state_filtering(self):
|
||||||
"""Test that different states in request get expected responses."""
|
"""Test that different states in request get expected responses."""
|
||||||
|
|
||||||
expected_values = [
|
expected_values = [
|
||||||
("unknown", 1),
|
("unknown", 1),
|
||||||
("ready", 0),
|
("ready", 0),
|
||||||
("expired", 2),
|
("expired", 2),
|
||||||
("ready,expired", 2),
|
("ready,expired", 2),
|
||||||
("unknown,expired", 3),
|
("unknown,expired", 3),
|
||||||
|
("expiring", 2),
|
||||||
]
|
]
|
||||||
|
|
||||||
for state, num_domains in expected_values:
|
for state, num_domains in expected_values:
|
||||||
with self.subTest(state=state, num_domains=num_domains):
|
with self.subTest(state=state, num_domains=num_domains):
|
||||||
response = self.app.get(reverse("get_domains_json"), {"status": state})
|
response = self.app.get(reverse("get_domains_json"), {"status": state})
|
||||||
|
|
|
@ -2940,3 +2940,160 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
||||||
# Validate Database has not changed
|
# Validate Database has not changed
|
||||||
invite_count_after = PortfolioInvitation.objects.count()
|
invite_count_after = PortfolioInvitation.objects.count()
|
||||||
self.assertEqual(invite_count_after, invite_count_before)
|
self.assertEqual(invite_count_after, invite_count_before)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEditPortfolioMemberView(WebTest):
|
||||||
|
"""Tests for the edit member page on portfolios"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = create_user()
|
||||||
|
# Create Portfolio
|
||||||
|
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
|
||||||
|
|
||||||
|
# Add an invited member who has been invited to manage domains
|
||||||
|
self.invited_member_email = "invited@example.com"
|
||||||
|
self.invitation = PortfolioInvitation.objects.create(
|
||||||
|
email=self.invited_member_email,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assign permissions to the user making requests
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
PortfolioInvitation.objects.all().delete()
|
||||||
|
UserPortfolioPermission.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_edit_member_permissions_basic_to_admin(self):
|
||||||
|
"""Tests converting a basic member to admin with full permissions."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Create a basic member to edit
|
||||||
|
basic_member = create_test_user()
|
||||||
|
basic_permission = UserPortfolioPermission.objects.create(
|
||||||
|
user=basic_member,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("member-permissions", kwargs={"pk": basic_permission.id}),
|
||||||
|
{
|
||||||
|
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
"domain_request_permission_admin": UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
"member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify redirect and success message
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
# Verify database changes
|
||||||
|
basic_permission.refresh_from_db()
|
||||||
|
self.assertEqual(basic_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
|
||||||
|
self.assertEqual(
|
||||||
|
set(basic_permission.additional_permissions),
|
||||||
|
{
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_edit_member_permissions_validation(self):
|
||||||
|
"""Tests form validation for required fields based on role."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
member = create_test_user()
|
||||||
|
permission = UserPortfolioPermission.objects.create(
|
||||||
|
user=member, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test missing required admin permissions
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("member-permissions", kwargs={"pk": permission.id}),
|
||||||
|
{
|
||||||
|
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
# Missing required admin fields
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
response.context["form"].errors["domain_request_permission_admin"][0],
|
||||||
|
"Admin domain request permission is required",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.context["form"].errors["member_permission_admin"][0], "Admin member permission is required"
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_edit_invited_member_permissions(self):
|
||||||
|
"""Tests editing permissions for an invited (but not yet joined) member."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Test updating invitation permissions
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
|
||||||
|
{
|
||||||
|
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
"domain_request_permission_admin": UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
"member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
# Verify invitation was updated
|
||||||
|
updated_invitation = PortfolioInvitation.objects.get(pk=self.invitation.id)
|
||||||
|
self.assertEqual(updated_invitation.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
|
||||||
|
self.assertEqual(
|
||||||
|
set(updated_invitation.additional_permissions),
|
||||||
|
{
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_admin_removing_own_admin_role(self):
|
||||||
|
"""Tests an admin removing their own admin role redirects to home."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Get the user's admin permission
|
||||||
|
admin_permission = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
|
||||||
|
{
|
||||||
|
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||||
|
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response["Location"], reverse("home"))
|
||||||
|
|
|
@ -744,30 +744,45 @@ class DomainExport(BaseExport):
|
||||||
):
|
):
|
||||||
security_contact_email = "(blank)"
|
security_contact_email = "(blank)"
|
||||||
|
|
||||||
|
model["status"] = human_readable_status
|
||||||
|
model["first_ready_on"] = first_ready_on
|
||||||
|
model["expiration_date"] = expiration_date
|
||||||
|
model["domain_type"] = domain_type
|
||||||
|
model["security_contact_email"] = security_contact_email
|
||||||
# create a dictionary of fields which can be included in output.
|
# create a dictionary of fields which can be included in output.
|
||||||
# "extra_fields" are precomputed fields (generated in the DB or parsed).
|
# "extra_fields" are precomputed fields (generated in the DB or parsed).
|
||||||
|
FIELDS = cls.get_fields(model)
|
||||||
|
|
||||||
|
row = [FIELDS.get(column, "") for column in columns]
|
||||||
|
|
||||||
|
return row
|
||||||
|
|
||||||
|
# NOTE - this override is temporary.
|
||||||
|
# We are running into a problem where DomainDataFull and DomainDataFederal are
|
||||||
|
# pulling the wrong data.
|
||||||
|
# For example, the portfolio name, rather than the suborganization name.
|
||||||
|
# This can be removed after that gets fixed.
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls, model):
|
||||||
FIELDS = {
|
FIELDS = {
|
||||||
"Domain name": model.get("domain__name"),
|
"Domain name": model.get("domain__name"),
|
||||||
"Status": human_readable_status,
|
"Status": model.get("status"),
|
||||||
"First ready on": first_ready_on,
|
"First ready on": model.get("first_ready_on"),
|
||||||
"Expiration date": expiration_date,
|
"Expiration date": model.get("expiration_date"),
|
||||||
"Domain type": domain_type,
|
"Domain type": model.get("domain_type"),
|
||||||
"Agency": model.get("converted_federal_agency"),
|
"Agency": model.get("converted_federal_agency"),
|
||||||
"Organization name": model.get("converted_organization_name"),
|
"Organization name": model.get("converted_organization_name"),
|
||||||
"City": model.get("converted_city"),
|
"City": model.get("converted_city"),
|
||||||
"State": model.get("converted_state_territory"),
|
"State": model.get("converted_state_territory"),
|
||||||
"SO": model.get("converted_so_name"),
|
"SO": model.get("converted_so_name"),
|
||||||
"SO email": model.get("converted_so_email"),
|
"SO email": model.get("converted_so_email"),
|
||||||
"Security contact email": security_contact_email,
|
"Security contact email": model.get("security_contact_email"),
|
||||||
"Created at": model.get("domain__created_at"),
|
"Created at": model.get("domain__created_at"),
|
||||||
"Deleted": model.get("domain__deleted"),
|
"Deleted": model.get("domain__deleted"),
|
||||||
"Domain managers": model.get("managers"),
|
"Domain managers": model.get("managers"),
|
||||||
"Invited domain managers": model.get("invited_users"),
|
"Invited domain managers": model.get("invited_users"),
|
||||||
}
|
}
|
||||||
|
return FIELDS
|
||||||
row = [FIELDS.get(column, "") for column in columns]
|
|
||||||
|
|
||||||
return row
|
|
||||||
|
|
||||||
def get_filtered_domain_infos_by_org(domain_infos_to_filter, org_to_filter_by):
|
def get_filtered_domain_infos_by_org(domain_infos_to_filter, org_to_filter_by):
|
||||||
"""Returns a list of Domain Requests that has been filtered by the given organization value."""
|
"""Returns a list of Domain Requests that has been filtered by the given organization value."""
|
||||||
|
@ -1077,6 +1092,39 @@ class DomainDataFull(DomainExport):
|
||||||
Inherits from BaseExport -> DomainExport
|
Inherits from BaseExport -> DomainExport
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# NOTE - this override is temporary.
|
||||||
|
# We are running into a problem where DomainDataFull is
|
||||||
|
# pulling the wrong data.
|
||||||
|
# For example, the portfolio name, rather than the suborganization name.
|
||||||
|
# This can be removed after that gets fixed.
|
||||||
|
# The following fields are changed from DomainExport:
|
||||||
|
# converted_organization_name => organization_name
|
||||||
|
# converted_city => city
|
||||||
|
# converted_state_territory => state_territory
|
||||||
|
# converted_so_name => so_name
|
||||||
|
# converted_so_email => senior_official__email
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls, model):
|
||||||
|
FIELDS = {
|
||||||
|
"Domain name": model.get("domain__name"),
|
||||||
|
"Status": model.get("status"),
|
||||||
|
"First ready on": model.get("first_ready_on"),
|
||||||
|
"Expiration date": model.get("expiration_date"),
|
||||||
|
"Domain type": model.get("domain_type"),
|
||||||
|
"Agency": model.get("federal_agency__agency"),
|
||||||
|
"Organization name": model.get("organization_name"),
|
||||||
|
"City": model.get("city"),
|
||||||
|
"State": model.get("state_territory"),
|
||||||
|
"SO": model.get("so_name"),
|
||||||
|
"SO email": model.get("senior_official__email"),
|
||||||
|
"Security contact email": model.get("security_contact_email"),
|
||||||
|
"Created at": model.get("domain__created_at"),
|
||||||
|
"Deleted": model.get("domain__deleted"),
|
||||||
|
"Domain managers": model.get("managers"),
|
||||||
|
"Invited domain managers": model.get("invited_users"),
|
||||||
|
}
|
||||||
|
return FIELDS
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_columns(cls):
|
def get_columns(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1106,9 +1154,9 @@ class DomainDataFull(DomainExport):
|
||||||
"""
|
"""
|
||||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||||
return [
|
return [
|
||||||
"converted_generic_org_type",
|
"organization_type",
|
||||||
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
Coalesce("federal_type", Value("ZZZZZ")),
|
||||||
"converted_federal_agency",
|
"federal_agency",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1164,6 +1212,39 @@ class DomainDataFederal(DomainExport):
|
||||||
Inherits from BaseExport -> DomainExport
|
Inherits from BaseExport -> DomainExport
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# NOTE - this override is temporary.
|
||||||
|
# We are running into a problem where DomainDataFull is
|
||||||
|
# pulling the wrong data.
|
||||||
|
# For example, the portfolio name, rather than the suborganization name.
|
||||||
|
# This can be removed after that gets fixed.
|
||||||
|
# The following fields are changed from DomainExport:
|
||||||
|
# converted_organization_name => organization_name
|
||||||
|
# converted_city => city
|
||||||
|
# converted_state_territory => state_territory
|
||||||
|
# converted_so_name => so_name
|
||||||
|
# converted_so_email => senior_official__email
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls, model):
|
||||||
|
FIELDS = {
|
||||||
|
"Domain name": model.get("domain__name"),
|
||||||
|
"Status": model.get("status"),
|
||||||
|
"First ready on": model.get("first_ready_on"),
|
||||||
|
"Expiration date": model.get("expiration_date"),
|
||||||
|
"Domain type": model.get("domain_type"),
|
||||||
|
"Agency": model.get("federal_agency__agency"),
|
||||||
|
"Organization name": model.get("organization_name"),
|
||||||
|
"City": model.get("city"),
|
||||||
|
"State": model.get("state_territory"),
|
||||||
|
"SO": model.get("so_name"),
|
||||||
|
"SO email": model.get("senior_official__email"),
|
||||||
|
"Security contact email": model.get("security_contact_email"),
|
||||||
|
"Created at": model.get("domain__created_at"),
|
||||||
|
"Deleted": model.get("domain__deleted"),
|
||||||
|
"Domain managers": model.get("managers"),
|
||||||
|
"Invited domain managers": model.get("invited_users"),
|
||||||
|
}
|
||||||
|
return FIELDS
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_columns(cls):
|
def get_columns(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1193,9 +1274,9 @@ class DomainDataFederal(DomainExport):
|
||||||
"""
|
"""
|
||||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||||
return [
|
return [
|
||||||
"converted_generic_org_type",
|
"organization_type",
|
||||||
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
Coalesce("federal_type", Value("ZZZZZ")),
|
||||||
"converted_federal_agency",
|
"federal_agency",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ def get_domains_json(request):
|
||||||
page_number = request.GET.get("page")
|
page_number = request.GET.get("page")
|
||||||
page_obj = paginator.get_page(page_number)
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
domains = [serialize_domain(domain, request.user) for domain in page_obj.object_list]
|
domains = [serialize_domain(domain, request) for domain in page_obj.object_list]
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
|
@ -80,21 +80,27 @@ def apply_state_filter(queryset, request):
|
||||||
status_list.append("dns needed")
|
status_list.append("dns needed")
|
||||||
# Split the status list into normal states and custom states
|
# Split the status list into normal states and custom states
|
||||||
normal_states = [state for state in status_list if state in Domain.State.values]
|
normal_states = [state for state in status_list if state in Domain.State.values]
|
||||||
custom_states = [state for state in status_list if state == "expired"]
|
custom_states = [state for state in status_list if (state == "expired" or state == "expiring")]
|
||||||
# Construct Q objects for normal states that can be queried through ORM
|
# Construct Q objects for normal states that can be queried through ORM
|
||||||
state_query = Q()
|
state_query = Q()
|
||||||
if normal_states:
|
if normal_states:
|
||||||
state_query |= Q(state__in=normal_states)
|
state_query |= Q(state__in=normal_states)
|
||||||
# Handle custom states in Python, as expired can not be queried through ORM
|
# Handle custom states in Python, as expired can not be queried through ORM
|
||||||
if "expired" in custom_states:
|
if "expired" in custom_states:
|
||||||
expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"]
|
expired_domain_ids = [domain.id for domain in queryset if domain.state_display(request) == "Expired"]
|
||||||
state_query |= Q(id__in=expired_domain_ids)
|
state_query |= Q(id__in=expired_domain_ids)
|
||||||
|
if "expiring" in custom_states:
|
||||||
|
expiring_domain_ids = [domain.id for domain in queryset if domain.state_display(request) == "Expiring soon"]
|
||||||
|
state_query |= Q(id__in=expiring_domain_ids)
|
||||||
# Apply the combined query
|
# Apply the combined query
|
||||||
queryset = queryset.filter(state_query)
|
queryset = queryset.filter(state_query)
|
||||||
# If there are filtered states, and expired is not one of them, domains with
|
# If there are filtered states, and expired is not one of them, domains with
|
||||||
# state_display of 'Expired' must be removed
|
# state_display of 'Expired' must be removed
|
||||||
if "expired" not in custom_states:
|
if "expired" not in custom_states:
|
||||||
expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"]
|
expired_domain_ids = [domain.id for domain in queryset if domain.state_display(request) == "Expired"]
|
||||||
|
queryset = queryset.exclude(id__in=expired_domain_ids)
|
||||||
|
if "expiring" not in custom_states:
|
||||||
|
expired_domain_ids = [domain.id for domain in queryset if domain.state_display(request) == "Expiring soon"]
|
||||||
queryset = queryset.exclude(id__in=expired_domain_ids)
|
queryset = queryset.exclude(id__in=expired_domain_ids)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
@ -105,7 +111,7 @@ def apply_sorting(queryset, request):
|
||||||
order = request.GET.get("order", "asc")
|
order = request.GET.get("order", "asc")
|
||||||
if sort_by == "state_display":
|
if sort_by == "state_display":
|
||||||
objects = list(queryset)
|
objects = list(queryset)
|
||||||
objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc"))
|
objects.sort(key=lambda domain: domain.state_display(request), reverse=(order == "desc"))
|
||||||
return objects
|
return objects
|
||||||
else:
|
else:
|
||||||
if order == "desc":
|
if order == "desc":
|
||||||
|
@ -113,7 +119,8 @@ def apply_sorting(queryset, request):
|
||||||
return queryset.order_by(sort_by)
|
return queryset.order_by(sort_by)
|
||||||
|
|
||||||
|
|
||||||
def serialize_domain(domain, user):
|
def serialize_domain(domain, request):
|
||||||
|
user = request.user
|
||||||
suborganization_name = None
|
suborganization_name = None
|
||||||
try:
|
try:
|
||||||
domain_info = domain.domain_info
|
domain_info = domain.domain_info
|
||||||
|
@ -133,7 +140,7 @@ def serialize_domain(domain, user):
|
||||||
"name": domain.name,
|
"name": domain.name,
|
||||||
"expiration_date": domain.expiration_date,
|
"expiration_date": domain.expiration_date,
|
||||||
"state": domain.state,
|
"state": domain.state,
|
||||||
"state_display": domain.state_display(),
|
"state_display": domain.state_display(request),
|
||||||
"get_state_help_text": domain.get_state_help_text(),
|
"get_state_help_text": domain.get_state_help_text(),
|
||||||
"action_url": reverse("domain", kwargs={"pk": domain.id}),
|
"action_url": reverse("domain", kwargs={"pk": domain.id}),
|
||||||
"action_label": ("View" if view_only else "Manage"),
|
"action_label": ("View" if view_only else "Manage"),
|
||||||
|
|
|
@ -8,5 +8,6 @@ def index(request):
|
||||||
if request and request.user and request.user.is_authenticated:
|
if request and request.user and request.user.is_authenticated:
|
||||||
# This controls the creation of a new domain request in the wizard
|
# This controls the creation of a new domain request in the wizard
|
||||||
context["user_domain_count"] = request.user.get_user_domain_ids(request).count()
|
context["user_domain_count"] = request.user.get_user_domain_ids(request).count()
|
||||||
|
context["num_expiring_domains"] = request.user.get_num_expiring_domains(request)
|
||||||
|
|
||||||
return render(request, "home.html", context)
|
return render(request, "home.html", context)
|
||||||
|
|
|
@ -7,7 +7,6 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
|
||||||
from registrar.forms import portfolio as portfolioForms
|
from registrar.forms import portfolio as portfolioForms
|
||||||
from registrar.models import Portfolio, User
|
from registrar.models import Portfolio, User
|
||||||
from registrar.models.domain_invitation import DomainInvitation
|
from registrar.models.domain_invitation import DomainInvitation
|
||||||
|
@ -44,6 +43,8 @@ class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
|
||||||
context = {}
|
context = {}
|
||||||
if self.request and self.request.user and self.request.user.is_authenticated:
|
if self.request and self.request.user and self.request.user.is_authenticated:
|
||||||
context["user_domain_count"] = self.request.user.get_user_domain_ids(request).count()
|
context["user_domain_count"] = self.request.user.get_user_domain_ids(request).count()
|
||||||
|
context["num_expiring_domains"] = request.user.get_num_expiring_domains(request)
|
||||||
|
|
||||||
return render(request, "portfolio_domains.html", context)
|
return render(request, "portfolio_domains.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@ -148,7 +149,7 @@ class PortfolioMemberDeleteView(PortfolioMemberPermission, View):
|
||||||
class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
|
class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
|
||||||
|
|
||||||
template_name = "portfolio_member_permissions.html"
|
template_name = "portfolio_member_permissions.html"
|
||||||
form_class = portfolioForms.PortfolioMemberForm
|
form_class = portfolioForms.BasePortfolioMemberForm
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||||
|
@ -168,12 +169,17 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||||
user = portfolio_permission.user
|
user = portfolio_permission.user
|
||||||
|
|
||||||
form = self.form_class(request.POST, instance=portfolio_permission)
|
form = self.form_class(request.POST, instance=portfolio_permission)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
# Check if user is removing their own admin or edit role
|
||||||
|
removing_admin_role_on_self = (
|
||||||
|
request.user == user
|
||||||
|
and UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_permission.roles
|
||||||
|
and UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in form.cleaned_data.get("role", [])
|
||||||
|
)
|
||||||
form.save()
|
form.save()
|
||||||
return redirect("member", pk=pk)
|
messages.success(self.request, "The member access and permission changes have been saved.")
|
||||||
|
return redirect("member", pk=pk) if not removing_admin_role_on_self else redirect("home")
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
@ -344,7 +350,7 @@ class PortfolioInvitedMemberDeleteView(PortfolioMemberPermission, View):
|
||||||
class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
|
class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
|
||||||
|
|
||||||
template_name = "portfolio_member_permissions.html"
|
template_name = "portfolio_member_permissions.html"
|
||||||
form_class = portfolioForms.PortfolioInvitedMemberForm
|
form_class = portfolioForms.BasePortfolioMemberForm
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||||
|
@ -364,6 +370,7 @@ class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
|
||||||
form = self.form_class(request.POST, instance=portfolio_invitation)
|
form = self.form_class(request.POST, instance=portfolio_invitation)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
|
messages.success(self.request, "The member access and permission changes have been saved.")
|
||||||
return redirect("invitedmember", pk=pk)
|
return redirect("invitedmember", pk=pk)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
|
|
|
@ -13,7 +13,7 @@ defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1,
|
||||||
diff-match-patch==20230430; python_version >= '3.7'
|
diff-match-patch==20230430; python_version >= '3.7'
|
||||||
dj-database-url==2.2.0
|
dj-database-url==2.2.0
|
||||||
dj-email-url==1.0.6
|
dj-email-url==1.0.6
|
||||||
django==4.2.10; python_version >= '3.8'
|
django==4.2.17; python_version >= '3.8'
|
||||||
django-admin-multiple-choice-list-filter==0.1.1
|
django-admin-multiple-choice-list-filter==0.1.1
|
||||||
django-allow-cidr==0.7.1
|
django-allow-cidr==0.7.1
|
||||||
django-auditlog==3.0.0; python_version >= '3.8'
|
django-auditlog==3.0.0; python_version >= '3.8'
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
10038 OUTOFSCOPE http://app:8080/org-name-address
|
10038 OUTOFSCOPE http://app:8080/org-name-address
|
||||||
10038 OUTOFSCOPE http://app:8080/domain_requests/
|
10038 OUTOFSCOPE http://app:8080/domain_requests/
|
||||||
10038 OUTOFSCOPE http://app:8080/domains/
|
10038 OUTOFSCOPE http://app:8080/domains/
|
||||||
|
10038 OUTOFSCOPE http://app:8080/domains/edit
|
||||||
10038 OUTOFSCOPE http://app:8080/organization/
|
10038 OUTOFSCOPE http://app:8080/organization/
|
||||||
10038 OUTOFSCOPE http://app:8080/permissions
|
10038 OUTOFSCOPE http://app:8080/permissions
|
||||||
10038 OUTOFSCOPE http://app:8080/suborganization/
|
10038 OUTOFSCOPE http://app:8080/suborganization/
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue