mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-27 04:58:42 +02:00
Merge branch 'main' into rb/3158-request-table-ui-fixes
This commit is contained in:
commit
81c39c9756
72 changed files with 1976 additions and 542 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": [
|
||||||
|
|
53
src/package-lock.json
generated
53
src/package-lock.json
generated
|
@ -6921,16 +6921,6 @@
|
||||||
"validate-npm-package-license": "^3.0.1"
|
"validate-npm-package-license": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/normalize-package-data/node_modules/semver": {
|
|
||||||
"version": "5.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
|
||||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
@ -7307,39 +7297,6 @@
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pa11y/node_modules/lru-cache": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pa11y/node_modules/semver": {
|
|
||||||
"version": "7.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
|
||||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^6.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pa11y/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/parse-filepath": {
|
"node_modules/parse-filepath": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
|
||||||
|
@ -8888,13 +8845,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "7.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver-greatest-satisfied-range": {
|
"node_modules/semver-greatest-satisfied-range": {
|
||||||
|
|
|
@ -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
|
||||||
|
|
15
src/registrar/assets/src/js/getgov/domain-dnssec.js
Normal file
15
src/registrar/assets/src/js/getgov/domain-dnssec.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { submitForm } from './helpers.js';
|
||||||
|
|
||||||
|
export function initDomainDNSSEC() {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let domain_dnssec_page = document.getElementById("domain-dnssec");
|
||||||
|
if (domain_dnssec_page) {
|
||||||
|
const button = document.getElementById("disable-dnssec-button");
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
submitForm("disable-dnssec-form");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
27
src/registrar/assets/src/js/getgov/domain-dsdata.js
Normal file
27
src/registrar/assets/src/js/getgov/domain-dsdata.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { submitForm } from './helpers.js';
|
||||||
|
|
||||||
|
export function initDomainDSData() {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let domain_dsdata_page = document.getElementById("domain-dsdata");
|
||||||
|
if (domain_dsdata_page) {
|
||||||
|
const override_button = document.getElementById("disable-override-click-button");
|
||||||
|
const cancel_button = document.getElementById("btn-cancel-click-button");
|
||||||
|
const cancel_close_button = document.getElementById("btn-cancel-click-close-button");
|
||||||
|
if (override_button) {
|
||||||
|
override_button.addEventListener("click", function () {
|
||||||
|
submitForm("disable-override-click-form");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (cancel_button) {
|
||||||
|
cancel_button.addEventListener("click", function () {
|
||||||
|
submitForm("btn-cancel-click-form");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (cancel_close_button) {
|
||||||
|
cancel_close_button.addEventListener("click", function () {
|
||||||
|
submitForm("btn-cancel-click-form");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
20
src/registrar/assets/src/js/getgov/domain-managers.js
Normal file
20
src/registrar/assets/src/js/getgov/domain-managers.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { submitForm } from './helpers.js';
|
||||||
|
|
||||||
|
export function initDomainManagersPage() {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let domain_managers_page = document.getElementById("domain-managers");
|
||||||
|
if (domain_managers_page) {
|
||||||
|
// Add event listeners for all buttons matching user-delete-button-{NUMBER}
|
||||||
|
const deleteButtons = document.querySelectorAll('[id^="user-delete-button-"]'); // Select buttons with ID starting with "user-delete-button-"
|
||||||
|
deleteButtons.forEach((button) => {
|
||||||
|
const buttonId = button.id; // e.g., "user-delete-button-1"
|
||||||
|
const number = buttonId.split('-').pop(); // Extract the NUMBER part
|
||||||
|
const formId = `user-delete-form-${number}`; // Generate the corresponding form ID
|
||||||
|
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
submitForm(formId); // Pass the form ID to submitForm
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
12
src/registrar/assets/src/js/getgov/domain-request-form.js
Normal file
12
src/registrar/assets/src/js/getgov/domain-request-form.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { submitForm } from './helpers.js';
|
||||||
|
|
||||||
|
export function initDomainRequestForm() {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const button = document.getElementById("domain-request-form-submit-button");
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
submitForm("submit-domain-request-form");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
19
src/registrar/assets/src/js/getgov/form-errors.js
Normal file
19
src/registrar/assets/src/js/getgov/form-errors.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export function initFormErrorHandling() {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const errorSummary = document.getElementById('form-errors');
|
||||||
|
const firstErrorField = document.querySelector('.usa-input--error');
|
||||||
|
if (firstErrorField) {
|
||||||
|
// Scroll to the first field in error
|
||||||
|
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
|
||||||
|
// Add focus to the first field in error
|
||||||
|
setTimeout(() => {
|
||||||
|
firstErrorField.focus();
|
||||||
|
}, 50);
|
||||||
|
} else if (errorSummary) {
|
||||||
|
// Scroll to the error summary
|
||||||
|
errorSummary.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,9 +1,17 @@
|
||||||
export function hideElement(element) {
|
export function hideElement(element) {
|
||||||
element.classList.add('display-none');
|
if (element) {
|
||||||
|
element.classList.add('display-none');
|
||||||
|
} else {
|
||||||
|
throw new Error('hideElement expected a passed DOM element as an argument, but none was provided.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showElement(element) {
|
export function showElement(element) {
|
||||||
element.classList.remove('display-none');
|
if (element) {
|
||||||
|
element.classList.remove('display-none');
|
||||||
|
} else {
|
||||||
|
throw new Error('showElement expected a passed DOM element as an argument, but none was provided.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,3 +83,16 @@ export function debounce(handler, cooldown=600) {
|
||||||
export function getCsrfToken() {
|
export function getCsrfToken() {
|
||||||
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to submit a form
|
||||||
|
* @param {} form_id - the id of the form to be submitted
|
||||||
|
*/
|
||||||
|
export function submitForm(form_id) {
|
||||||
|
let form = document.getElementById(form_id);
|
||||||
|
if (form) {
|
||||||
|
form.submit();
|
||||||
|
} else {
|
||||||
|
console.error("Form '" + form_id + "' not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,11 @@ 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 { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPageRadio } from './portfolio-member-page.js';
|
import { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPageRadio } from './portfolio-member-page.js';
|
||||||
|
import { initDomainRequestForm } from './domain-request-form.js';
|
||||||
|
import { initDomainManagersPage } from './domain-managers.js';
|
||||||
|
import { initDomainDSData } from './domain-dsdata.js';
|
||||||
|
import { initDomainDNSSEC } from './domain-dnssec.js';
|
||||||
|
import { initFormErrorHandling } from './form-errors.js';
|
||||||
|
|
||||||
initDomainValidators();
|
initDomainValidators();
|
||||||
|
|
||||||
|
@ -36,6 +41,13 @@ initMembersTable();
|
||||||
initMemberDomainsTable();
|
initMemberDomainsTable();
|
||||||
initEditMemberDomainsTable();
|
initEditMemberDomainsTable();
|
||||||
|
|
||||||
|
initDomainRequestForm();
|
||||||
|
initDomainManagersPage();
|
||||||
|
initDomainDSData();
|
||||||
|
initDomainDNSSEC();
|
||||||
|
|
||||||
|
initFormErrorHandling();
|
||||||
|
|
||||||
// Init the portfolio new member page
|
// Init the portfolio new member page
|
||||||
initPortfolioMemberPageRadio();
|
initPortfolioMemberPageRadio();
|
||||||
initPortfolioNewMemberPageToggle();
|
initPortfolioNewMemberPageToggle();
|
||||||
|
|
|
@ -143,7 +143,7 @@ export class BaseTable {
|
||||||
this.statusCheckboxes = document.querySelectorAll(`.${this.sectionSelector} input[name="filter-status"]`);
|
this.statusCheckboxes = document.querySelectorAll(`.${this.sectionSelector} input[name="filter-status"]`);
|
||||||
this.statusIndicator = document.getElementById(`${this.sectionSelector}__filter-indicator`);
|
this.statusIndicator = document.getElementById(`${this.sectionSelector}__filter-indicator`);
|
||||||
this.statusToggle = document.getElementById(`${this.sectionSelector}__usa-button--filter`);
|
this.statusToggle = document.getElementById(`${this.sectionSelector}__usa-button--filter`);
|
||||||
this.noTableWrapper = document.getElementById(`${this.sectionSelector}__no-data`);
|
this.noDataTableWrapper = document.getElementById(`${this.sectionSelector}__no-data`);
|
||||||
this.noSearchResultsWrapper = document.getElementById(`${this.sectionSelector}__no-search-results`);
|
this.noSearchResultsWrapper = document.getElementById(`${this.sectionSelector}__no-search-results`);
|
||||||
this.portfolioElement = document.getElementById('portfolio-js-value');
|
this.portfolioElement = document.getElementById('portfolio-js-value');
|
||||||
this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null;
|
this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null;
|
||||||
|
@ -451,7 +451,7 @@ export class BaseTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no members exist in the list or search returns no results
|
// handle the display of proper messaging in the event that no members exist in the list or search returns no results
|
||||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
this.updateDisplay(data, this.tableWrapper, this.noDataTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||||
// identify the DOM element where the list of results will be inserted into the DOM
|
// identify the DOM element where the list of results will be inserted into the DOM
|
||||||
const tbody = this.tableWrapper.querySelector('tbody');
|
const tbody = this.tableWrapper.querySelector('tbody');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
@ -495,7 +495,8 @@ export class BaseTable {
|
||||||
// Add event listeners to table headers for sorting
|
// Add event listeners to table headers for sorting
|
||||||
initializeTableHeaders() {
|
initializeTableHeaders() {
|
||||||
this.tableHeaders.forEach(header => {
|
this.tableHeaders.forEach(header => {
|
||||||
header.addEventListener('click', () => {
|
header.addEventListener('click', event => {
|
||||||
|
let button = header.querySelector('.usa-table__header__button')
|
||||||
const sortBy = header.getAttribute('data-sortable');
|
const sortBy = header.getAttribute('data-sortable');
|
||||||
let order = 'asc';
|
let order = 'asc';
|
||||||
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||||
|
@ -505,6 +506,13 @@ export class BaseTable {
|
||||||
}
|
}
|
||||||
// load the results with the updated sort
|
// load the results with the updated sort
|
||||||
this.loadTable(1, sortBy, order);
|
this.loadTable(1, sortBy, order);
|
||||||
|
// If the click occurs outside of the button, need to simulate a button click in order
|
||||||
|
// for USWDS listener on the button to execute.
|
||||||
|
// Check first to see if click occurs outside of the button
|
||||||
|
if (!button.contains(event.target)) {
|
||||||
|
// Simulate a button click
|
||||||
|
button.click();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
import { BaseTable } from './table-base.js';
|
import { BaseTable } from './table-base.js';
|
||||||
|
import { hideElement, showElement } from './helpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EditMemberDomainsTable is used for PortfolioMember and PortfolioInvitedMember
|
* EditMemberDomainsTable is used for PortfolioMember and PortfolioInvitedMember
|
||||||
|
@ -18,8 +19,14 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
this.initialDomainAssignmentsOnlyMember = []; // list of initially assigned domains which are readonly
|
this.initialDomainAssignmentsOnlyMember = []; // list of initially assigned domains which are readonly
|
||||||
this.addedDomains = []; // list of domains added to member
|
this.addedDomains = []; // list of domains added to member
|
||||||
this.removedDomains = []; // list of domains removed from member
|
this.removedDomains = []; // list of domains removed from member
|
||||||
|
this.editModeContainer = document.getElementById('domain-assignments-edit-view');
|
||||||
|
this.readonlyModeContainer = document.getElementById('domain-assignments-readonly-view');
|
||||||
|
this.reviewButton = document.getElementById('review-domain-assignments');
|
||||||
|
this.backButton = document.getElementById('back-to-edit-domain-assignments');
|
||||||
|
this.saveButton = document.getElementById('save-domain-assignments');
|
||||||
this.initializeDomainAssignments();
|
this.initializeDomainAssignments();
|
||||||
this.initCancelEditDomainAssignmentButton();
|
this.initCancelEditDomainAssignmentButton();
|
||||||
|
this.initEventListeners();
|
||||||
}
|
}
|
||||||
getBaseUrl() {
|
getBaseUrl() {
|
||||||
return document.getElementById("get_member_domains_json_url");
|
return document.getElementById("get_member_domains_json_url");
|
||||||
|
@ -55,6 +62,14 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
getSearchParams(page, sortBy, order, searchTerm, status, portfolio) {
|
getSearchParams(page, sortBy, order, searchTerm, status, portfolio) {
|
||||||
let searchParams = super.getSearchParams(page, sortBy, order, searchTerm, status, portfolio);
|
let searchParams = super.getSearchParams(page, sortBy, order, searchTerm, status, portfolio);
|
||||||
// Add checkedDomains to searchParams
|
// Add checkedDomains to searchParams
|
||||||
|
let checkedDomains = this.getCheckedDomains();
|
||||||
|
// Append updated checkedDomain IDs to searchParams
|
||||||
|
if (checkedDomains.length > 0) {
|
||||||
|
searchParams.append("checkedDomainIds", checkedDomains.join(","));
|
||||||
|
}
|
||||||
|
return searchParams;
|
||||||
|
}
|
||||||
|
getCheckedDomains() {
|
||||||
// Clone the initial domains to avoid mutating them
|
// Clone the initial domains to avoid mutating them
|
||||||
let checkedDomains = [...this.initialDomainAssignments];
|
let checkedDomains = [...this.initialDomainAssignments];
|
||||||
// Add IDs from addedDomains that are not already in checkedDomains
|
// Add IDs from addedDomains that are not already in checkedDomains
|
||||||
|
@ -70,11 +85,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
checkedDomains.splice(index, 1);
|
checkedDomains.splice(index, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Append updated checkedDomain IDs to searchParams
|
return checkedDomains
|
||||||
if (checkedDomains.length > 0) {
|
|
||||||
searchParams.append("checkedDomainIds", checkedDomains.join(","));
|
|
||||||
}
|
|
||||||
return searchParams;
|
|
||||||
}
|
}
|
||||||
addRow(dataObject, tbody, customTableOptions) {
|
addRow(dataObject, tbody, customTableOptions) {
|
||||||
const domain = dataObject;
|
const domain = dataObject;
|
||||||
|
@ -218,6 +229,122 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateReadonlyDisplay() {
|
||||||
|
let totalAssignedDomains = this.getCheckedDomains().length;
|
||||||
|
|
||||||
|
// Create unassigned domains list
|
||||||
|
const unassignedDomainsList = document.createElement('ul');
|
||||||
|
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
||||||
|
this.removedDomains.forEach(removedDomain => {
|
||||||
|
const removedDomainListItem = document.createElement('li');
|
||||||
|
removedDomainListItem.textContent = removedDomain.name; // Use textContent for security
|
||||||
|
unassignedDomainsList.appendChild(removedDomainListItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create assigned domains list
|
||||||
|
const assignedDomainsList = document.createElement('ul');
|
||||||
|
assignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
||||||
|
this.addedDomains.forEach(addedDomain => {
|
||||||
|
const addedDomainListItem = document.createElement('li');
|
||||||
|
addedDomainListItem.textContent = addedDomain.name; // Use textContent for security
|
||||||
|
assignedDomainsList.appendChild(addedDomainListItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the summary container
|
||||||
|
const domainAssignmentSummary = document.getElementById('domain-assignments-summary');
|
||||||
|
|
||||||
|
// Clear existing content
|
||||||
|
domainAssignmentSummary.innerHTML = '';
|
||||||
|
|
||||||
|
// Append unassigned domains section
|
||||||
|
if (this.removedDomains.length) {
|
||||||
|
const unassignedHeader = document.createElement('h3');
|
||||||
|
unassignedHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
||||||
|
unassignedHeader.textContent = 'Unassigned domains';
|
||||||
|
domainAssignmentSummary.appendChild(unassignedHeader);
|
||||||
|
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append assigned domains section
|
||||||
|
if (this.addedDomains.length) {
|
||||||
|
const assignedHeader = document.createElement('h3');
|
||||||
|
assignedHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
||||||
|
assignedHeader.textContent = 'Assigned domains';
|
||||||
|
domainAssignmentSummary.appendChild(assignedHeader);
|
||||||
|
domainAssignmentSummary.appendChild(assignedDomainsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append total assigned domains section
|
||||||
|
const totalHeader = document.createElement('h3');
|
||||||
|
totalHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
||||||
|
totalHeader.textContent = 'Total assigned domains';
|
||||||
|
domainAssignmentSummary.appendChild(totalHeader);
|
||||||
|
const totalCount = document.createElement('p');
|
||||||
|
totalCount.classList.add('margin-y-0');
|
||||||
|
totalCount.textContent = totalAssignedDomains;
|
||||||
|
domainAssignmentSummary.appendChild(totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
showReadonlyMode() {
|
||||||
|
this.updateReadonlyDisplay();
|
||||||
|
hideElement(this.editModeContainer);
|
||||||
|
showElement(this.readonlyModeContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
showEditMode() {
|
||||||
|
hideElement(this.readonlyModeContainer);
|
||||||
|
showElement(this.editModeContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitChanges() {
|
||||||
|
let memberDomainsEditForm = document.getElementById("member-domains-edit-form");
|
||||||
|
if (memberDomainsEditForm) {
|
||||||
|
// Serialize data to send
|
||||||
|
const addedDomainIds = this.addedDomains.map(domain => domain.id);
|
||||||
|
const addedDomainsInput = document.createElement('input');
|
||||||
|
addedDomainsInput.type = 'hidden';
|
||||||
|
addedDomainsInput.name = 'added_domains'; // Backend will use this key to retrieve data
|
||||||
|
addedDomainsInput.value = JSON.stringify(addedDomainIds); // Stringify the array
|
||||||
|
|
||||||
|
const removedDomainsIds = this.removedDomains.map(domain => domain.id);
|
||||||
|
const removedDomainsInput = document.createElement('input');
|
||||||
|
removedDomainsInput.type = 'hidden';
|
||||||
|
removedDomainsInput.name = 'removed_domains'; // Backend will use this key to retrieve data
|
||||||
|
removedDomainsInput.value = JSON.stringify(removedDomainsIds); // Stringify the array
|
||||||
|
|
||||||
|
// Append input to the form
|
||||||
|
memberDomainsEditForm.appendChild(addedDomainsInput);
|
||||||
|
memberDomainsEditForm.appendChild(removedDomainsInput);
|
||||||
|
|
||||||
|
memberDomainsEditForm.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initEventListeners() {
|
||||||
|
if (this.reviewButton) {
|
||||||
|
this.reviewButton.addEventListener('click', () => {
|
||||||
|
this.showReadonlyMode();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Missing DOM element. Expected element with id review-domain-assignments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.backButton) {
|
||||||
|
this.backButton.addEventListener('click', () => {
|
||||||
|
this.showEditMode();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Missing DOM element. Expected element with id back-to-edit-domain-assignments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.saveButton) {
|
||||||
|
this.saveButton.addEventListener('click', () => {
|
||||||
|
this.submitChanges();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Missing DOM element. Expected element with id save-domain-assignments');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initEditMemberDomainsTable() {
|
export function initEditMemberDomainsTable() {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
|
import { showElement, hideElement } from './helpers.js';
|
||||||
import { BaseTable } from './table-base.js';
|
import { BaseTable } from './table-base.js';
|
||||||
|
|
||||||
export class MemberDomainsTable extends BaseTable {
|
export class MemberDomainsTable extends BaseTable {
|
||||||
|
@ -24,7 +25,28 @@ export class MemberDomainsTable extends BaseTable {
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
|
updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
||||||
|
const { unfiltered_total, total } = data;
|
||||||
|
const searchSection = document.getElementById('edit-member-domains__search');
|
||||||
|
if (!searchSection) console.warn('MemberDomainsTable updateDisplay expected an element with id edit-member-domains__search but none was found');
|
||||||
|
if (unfiltered_total) {
|
||||||
|
showElement(searchSection);
|
||||||
|
if (total) {
|
||||||
|
showElement(dataWrapper);
|
||||||
|
hideElement(noSearchResultsWrapper);
|
||||||
|
hideElement(noDataWrapper);
|
||||||
|
} else {
|
||||||
|
hideElement(dataWrapper);
|
||||||
|
showElement(noSearchResultsWrapper);
|
||||||
|
hideElement(noDataWrapper);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hideElement(searchSection);
|
||||||
|
hideElement(dataWrapper);
|
||||||
|
hideElement(noSearchResultsWrapper);
|
||||||
|
showElement(noDataWrapper);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initMemberDomainsTable() {
|
export function initMemberDomainsTable() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
margin-top: units(1);
|
margin-top: units(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// register-form-review-header is used on the summary page and
|
// header--body is used on the summary page and
|
||||||
// should not be styled like the register form headers
|
// should not be styled like the register form headers
|
||||||
.register-form-step h3 {
|
.register-form-step h3 {
|
||||||
color: color('primary-dark');
|
color: color('primary-dark');
|
||||||
|
@ -25,15 +25,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.register-form-review-header {
|
|
||||||
color: color('primary-dark');
|
|
||||||
margin-top: units(2);
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-weight: font-weight('semibold');
|
|
||||||
// The units mixin can only get us close, so it's between
|
|
||||||
// hardcoding the value and using in markup
|
|
||||||
font-size: 16.96px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.register-form-step h4 {
|
.register-form-step h4 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
@ -88,8 +88,14 @@ th {
|
||||||
}
|
}
|
||||||
|
|
||||||
@include at-media(tablet-lg) {
|
@include at-media(tablet-lg) {
|
||||||
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
|
th[data-sortable] .usa-table__header__button {
|
||||||
right: auto;
|
right: auto;
|
||||||
|
|
||||||
|
&[aria-sort=ascending],
|
||||||
|
&[aria-sort=descending],
|
||||||
|
&:not([aria-sort]) {
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,14 @@ h2 {
|
||||||
color: color('primary-darker');
|
color: color('primary-darker');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header--body {
|
||||||
|
margin-top: units(2);
|
||||||
|
font-weight: font-weight('semibold');
|
||||||
|
// The units mixin can only get us close, so it's between
|
||||||
|
// hardcoding the value and using in markup
|
||||||
|
font-size: 16.96px;
|
||||||
|
}
|
||||||
|
|
||||||
.h4--sm-05 {
|
.h4--sm-05 {
|
||||||
font-size: size('body', 'sm');
|
font-size: size('body', 'sm');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -34,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
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,10 @@ class UserPortfolioPermissionFixture:
|
||||||
user=user,
|
user=user,
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
additional_permissions=[UserPortfolioPermissionChoices.EDIT_MEMBERS],
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
user_portfolio_permissions_to_create.append(user_portfolio_permission)
|
user_portfolio_permissions_to_create.append(user_portfolio_permission)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -37,6 +37,9 @@
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
<h1>Add a domain manager</h1>
|
<h1>Add a domain manager</h1>
|
||||||
{% if has_organization_feature_flag %}
|
{% if has_organization_feature_flag %}
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
<h1>DNSSEC</h1>
|
<h1 id="domain-dnssec">DNSSEC</h1>
|
||||||
|
|
||||||
<p>DNSSEC, or DNS Security Extensions, is an additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
|
<p>DNSSEC, or DNS Security Extensions, is an additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
|
||||||
|
|
||||||
|
@ -78,7 +78,11 @@
|
||||||
aria-labelledby="Are you sure you want to continue?"
|
aria-labelledby="Are you sure you want to continue?"
|
||||||
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
||||||
>
|
>
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to disable DNSSEC?" modal_button=modal_button|safe %}
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to disable DNSSEC?" modal_button_id="disable-dnssec-button" modal_button_text="Confirm" modal_button_class="usa-button--secondary" %}
|
||||||
</div>
|
</div>
|
||||||
|
<form method="post" id="disable-dnssec-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="disable_dnssec" value="1">
|
||||||
|
</form>
|
||||||
|
|
||||||
{% endblock %} {# domain_content #}
|
{% endblock %} {# domain_content #}
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<h1>DS data</h1>
|
<h1 id="domain-dsdata">DS data</h1>
|
||||||
|
|
||||||
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
||||||
|
|
||||||
|
@ -141,7 +141,15 @@
|
||||||
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
||||||
data-force-action
|
data-force-action
|
||||||
>
|
>
|
||||||
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
|
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button_id="disable-override-click-button" modal_button_text="Remove all DS data" modal_button_class="usa-button--secondary" %}
|
||||||
</div>
|
</div>
|
||||||
|
<form method="post" id="disable-override-click-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="disable-override-click" value="1">
|
||||||
|
</form>
|
||||||
|
<form method="post" id="btn-cancel-click-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="btn-cancel-click" value="1">
|
||||||
|
</form>
|
||||||
|
|
||||||
{% 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 #}
|
||||||
|
|
|
@ -130,9 +130,17 @@
|
||||||
aria-describedby="Are you sure you want to submit a domain request?"
|
aria-describedby="Are you sure you want to submit a domain request?"
|
||||||
data-force-action
|
data-force-action
|
||||||
>
|
>
|
||||||
{% include 'includes/modal.html' with is_domain_request_form=True review_form_is_complete=review_form_is_complete modal_heading=modal_heading|safe modal_description=modal_description|safe modal_button=modal_button|safe %}
|
{% if review_form_is_complete %}
|
||||||
|
{% include 'includes/modal.html' with modal_heading="You are about to submit a domain request for " domain_name_modal=requested_domain__name modal_description="Once you submit this request, you won’t be able to edit it until we review it. You’ll only be able to withdraw your request." modal_button_id="domain-request-form-submit-button" modal_button_text="Submit request" %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'includes/modal.html' with modal_heading="Your request form is incomplete" modal_description='This request cannot be submitted yet. Return to the request and visit the steps that are marked as "incomplete."' modal_button_text="Return to request" cancel_button_only=True %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form method="post" id="submit-domain-request-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
|
|
||||||
{% block after_form_content %}{% endblock %}
|
{% block after_form_content %}{% endblock %}
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% if domain_manager_roles %}
|
{% if domain_manager_roles %}
|
||||||
<section class="section-outlined">
|
<section class="section-outlined" id="domain-managers">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||||
<h2 class> Domain managers </h2>
|
<h2 class> Domain managers </h2>
|
||||||
<caption class="sr-only">Domain managers</caption>
|
<caption class="sr-only">Domain managers</caption>
|
||||||
|
@ -89,12 +89,13 @@
|
||||||
aria-describedby="You will be removed from this domain"
|
aria-describedby="You will be removed from this domain"
|
||||||
data-force-action
|
data-force-action
|
||||||
>
|
>
|
||||||
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
|
{% with domain_name=domain.name|force_escape counter_str=forloop.counter|stringformat:"s" %}
|
||||||
{% with domain_name=domain.name|force_escape %}
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove myself" modal_button_class="usa-button--secondary" %}
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button_self|safe %}
|
{% endwith %}
|
||||||
{% endwith %}
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}" >
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div
|
<div
|
||||||
class="usa-modal"
|
class="usa-modal"
|
||||||
|
@ -103,12 +104,13 @@
|
||||||
aria-describedby="{{ item.permission.user.email }} will be removed"
|
aria-describedby="{{ item.permission.user.email }} will be removed"
|
||||||
data-force-action
|
data-force-action
|
||||||
>
|
>
|
||||||
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
|
{% with email=item.permission.user.email|default:item.permission.user|force_escape domain_name=domain.name|force_escape counter_str=forloop.counter|stringformat:"s" %}
|
||||||
{% with email=item.permission.user.email|default:item.permission.user|force_escape domain_name=domain.name|force_escape %}
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove domain manager" modal_button_class="usa-button--secondary" %}
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button|safe %}
|
{% endwith %}
|
||||||
{% endwith %}
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -213,7 +213,7 @@
|
||||||
|
|
||||||
{# We always show this field even if None #}
|
{# We always show this field even if None #}
|
||||||
{% if DomainRequest %}
|
{% if DomainRequest %}
|
||||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
<h3 class="header--body text-primary-dark margin-bottom-0">CISA Regional Representative</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.cisa_representative_first_name %}
|
{% if DomainRequest.cisa_representative_first_name %}
|
||||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||||
|
@ -221,7 +221,7 @@
|
||||||
No
|
No
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="register-form-review-header">Anything else</h3>
|
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.anything_else %}
|
{% if DomainRequest.anything_else %}
|
||||||
{{DomainRequest.anything_else}}
|
{{DomainRequest.anything_else}}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
|
<div id="form-errors">
|
||||||
{% for error in form.non_field_errors %}
|
{% for error in form.non_field_errors %}
|
||||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
|
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
{{ error|escape }}
|
{{ error|escape }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,5 +15,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
<div class="section-outlined__header margin-bottom-3 grid-row" id="edit-member-domains__search">
|
||||||
<!-- ---------- SEARCH ---------- -->
|
<!-- ---------- SEARCH ---------- -->
|
||||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||||
<section aria-label="Member domains search component" class="margin-top-2">
|
<section aria-label="Member domains search component" class="margin-top-2">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% load static form_helpers url_helpers %}
|
{% load static form_helpers url_helpers %}
|
||||||
|
{% load custom_filters %}
|
||||||
|
|
||||||
<div class="usa-modal__content">
|
<div class="usa-modal__content">
|
||||||
<div class="usa-modal__main">
|
<div class="usa-modal__main">
|
||||||
|
@ -24,39 +25,51 @@
|
||||||
<div class="usa-modal__footer">
|
<div class="usa-modal__footer">
|
||||||
<ul class="usa-button-group">
|
<ul class="usa-button-group">
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
{% if not_form and modal_button %}
|
{% if cancel_button_only %}
|
||||||
{{ modal_button }}
|
<button
|
||||||
|
type="button"
|
||||||
|
class="{{ modal_button_class|button_class }}"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
{% if modal_button_text %}
|
||||||
|
{{ modal_button_text }}
|
||||||
|
{% else %}
|
||||||
|
Cancel
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
{% elif modal_button_id and modal_button_text %}
|
||||||
|
{% comment %} Adding button id allows for onclick listeners on button by id,
|
||||||
|
which execute form submission on form elsewhere on a page outside modal.{% endcomment %}
|
||||||
|
<button
|
||||||
|
id="{{ modal_button_id }}"
|
||||||
|
type="button"
|
||||||
|
class="{{ modal_button_class|button_class }}"
|
||||||
|
>
|
||||||
|
{{ modal_button_text }}
|
||||||
|
</button>
|
||||||
{% elif modal_button_url and modal_button_text %}
|
{% elif modal_button_url and modal_button_text %}
|
||||||
<a
|
<a
|
||||||
href="{{ modal_button_url }}"
|
href="{{ modal_button_url }}"
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button"
|
class="{{ modal_button_class|button_class }}"
|
||||||
>
|
>
|
||||||
{{ modal_button_text }}
|
{{ modal_button_text }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ modal_button }}
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||||
in addition to being a close modal hook {% endcomment %}
|
in addition to being a close modal hook {% endcomment %}
|
||||||
{% if cancel_button_resets_ds_form %}
|
{% if cancel_button_resets_ds_form %}
|
||||||
<form method="post">
|
<button
|
||||||
{% csrf_token %}
|
type="submit"
|
||||||
<button
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
type="submit"
|
id="btn-cancel-click-button"
|
||||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
data-close-modal
|
||||||
name="btn-cancel-click"
|
>
|
||||||
data-close-modal
|
Cancel
|
||||||
>
|
</button>
|
||||||
Cancel
|
{% elif not cancel_button_only %}
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% elif not is_domain_request_form or review_form_is_complete %}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
@ -72,20 +85,17 @@
|
||||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||||
in addition to being a close modal hook {% endcomment %}
|
in addition to being a close modal hook {% endcomment %}
|
||||||
{% if cancel_button_resets_ds_form %}
|
{% if cancel_button_resets_ds_form %}
|
||||||
<form method="post">
|
<button
|
||||||
{% csrf_token %}
|
type="submit"
|
||||||
<button
|
class="usa-button usa-modal__close"
|
||||||
type="submit"
|
aria-label="Close this window"
|
||||||
class="usa-button usa-modal__close"
|
id="btn-cancel-click-close-button"
|
||||||
aria-label="Close this window"
|
data-close-modal
|
||||||
name="btn-cancel-click"
|
>
|
||||||
data-close-modal
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
>
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
</svg>
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
</button>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if domain_request.alternative_domains.all %}
|
{% if domain_request.alternative_domains.all %}
|
||||||
<h3 class="register-form-review-header">Alternative domains</h3>
|
<h3 class="header--body text-primary-dark margin-bottom-0">Alternative domains</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% for site in domain_request.alternative_domains.all %}
|
{% for site in domain_request.alternative_domains.all %}
|
||||||
<li>{{ site.website }}</li>
|
<li>{{ site.website }}</li>
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if domain_request.alternative_domains.all %}
|
{% if domain_request.alternative_domains.all %}
|
||||||
<h3 class="register-form-review-header">Alternative domains</h3>
|
<h3 class="header--body text-primary-dark margin-bottom-0">Alternative domains</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% for site in domain_request.alternative_domains.all %}
|
{% for site in domain_request.alternative_domains.all %}
|
||||||
<li>{{ site.website }}</li>
|
<li>{{ site.website }}</li>
|
||||||
|
@ -132,7 +132,7 @@
|
||||||
{% with title=form_titles|get_item:step %}
|
{% with title=form_titles|get_item:step %}
|
||||||
{% if domain_request.has_additional_details %}
|
{% if domain_request.has_additional_details %}
|
||||||
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
<h3 class="header--body text-primary-dark margin-bottom-0">CISA Regional Representative</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if domain_request.cisa_representative_first_name %}
|
{% if domain_request.cisa_representative_first_name %}
|
||||||
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="register-form-review-header">Anything else</h3>
|
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if domain_request.anything_else %}
|
{% if domain_request.anything_else %}
|
||||||
{{domain_request.anything_else}}
|
{{domain_request.anything_else}}
|
||||||
|
|
|
@ -216,7 +216,7 @@
|
||||||
|
|
||||||
{# We always show this field even if None #}
|
{# We always show this field even if None #}
|
||||||
{% if DomainRequest %}
|
{% if DomainRequest %}
|
||||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
<h3 class="header--body text-primary-dark margin-bottom-0">CISA Regional Representative</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.cisa_representative_first_name %}
|
{% if DomainRequest.cisa_representative_first_name %}
|
||||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||||
|
@ -224,7 +224,7 @@
|
||||||
No
|
No
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="register-form-review-header">Anything else</h3>
|
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.anything_else %}
|
{% if DomainRequest.anything_else %}
|
||||||
{{DomainRequest.anything_else}}
|
{{DomainRequest.anything_else}}
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
</form>
|
</form>
|
||||||
{% elif not form.full_name.value and not form.title.value and not form.email.value %}
|
{% elif not form.full_name.value and not form.title.value and not form.email.value %}
|
||||||
<p>
|
<p>
|
||||||
Your senior official is a person within your organization who can authorize domain requests.
|
|
||||||
We don't have information about your organization's senior official. To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
We don't have information about your organization's senior official. To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub_header_text %}
|
{% if sub_header_text %}
|
||||||
<h4 class="register-form-review-header">{{ sub_header_text }}</h4>
|
<h4 class="header--body text-primary-dark margin-bottom-0">{{ sub_header_text }}</h4>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if permissions %}
|
{% if permissions %}
|
||||||
{% include "includes/member_permissions.html" with permissions=value %}
|
{% include "includes/member_permissions.html" with permissions=value %}
|
||||||
|
|
|
@ -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 %}
|
|
@ -17,53 +17,109 @@
|
||||||
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
||||||
<ol class="usa-breadcrumb__list">
|
<ol class="usa-breadcrumb__list">
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url2 }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
<a href="{{ url2 }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url3 }}" class="usa-breadcrumb__link"><span>Domain assignments</span></a>
|
<a href="{{ url3 }}" class="usa-breadcrumb__link"><span>Domain assignments</span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-breadcrumb__list-item usa-current edit-domain-assignments-breadcrumb" aria-current="page">
|
<li class="usa-breadcrumb__list-item usa-current domain-assignments-edit-breadcrumb" aria-current="page">
|
||||||
<span>Edit domain assignments</span>
|
<span>Edit domain assignments</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<h1 class="margin-bottom-3">Edit domain assignments</h1>
|
<section id="domain-assignments-edit-view">
|
||||||
|
<h1 class="margin-bottom-3">Edit domain assignments</h1>
|
||||||
|
|
||||||
<p class="margin-bottom-0">
|
<p class="margin-bottom-0">
|
||||||
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
When you save this form the member will get an email to notify them of any changes.
|
When you save this form the member will get an email to notify them of any changes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% include "includes/member_domains_edit_table.html" %}
|
{% include "includes/member_domains_edit_table.html" %}
|
||||||
|
|
||||||
<ul class="usa-button-group">
|
<ul class="usa-button-group">
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button
|
<button
|
||||||
id="cancel-edit-domain-assignments"
|
id="cancel-edit-domain-assignments"
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button usa-button--outline"
|
class="usa-button usa-button--outline"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
id="review-domain-assignments"
|
||||||
class="usa-button"
|
type="button"
|
||||||
>
|
class="usa-button"
|
||||||
Review
|
>
|
||||||
</button>
|
Review
|
||||||
</li>
|
</button>
|
||||||
</ul>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="domain-assignments-readonly-view" class="display-none">
|
||||||
|
<h1 class="margin-bottom-3">Review domain assignments</h1>
|
||||||
|
|
||||||
|
<h2 class="text-primary-dark">Would you like to continue with the following domain assignment changes for
|
||||||
|
{% if member %}
|
||||||
|
{{ member.email }}
|
||||||
|
{% else %}
|
||||||
|
{{ portfolio_invitation.email }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>When you save this form the member will get an email to notify them of any changes.</p>
|
||||||
|
|
||||||
|
<div id="domain-assignments-summary" class="margin-bottom-2">
|
||||||
|
<!-- AJAX will populate this summary -->
|
||||||
|
<h3 class="header--body text-primary margin-bottom-1">Unassigned domains</h3>
|
||||||
|
<ul class="usa-list usa-list--unstyled">
|
||||||
|
<li>item1</li>
|
||||||
|
<li>item2</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 class="header--body text-primary-dark margin-bottom-0">Assigned domains</h3>
|
||||||
|
<ul class="usa-list usa-list--unstyled">
|
||||||
|
<li>item1</li>
|
||||||
|
<li>item2</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--outline"
|
||||||
|
id="back-to-edit-domain-assignments"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
id="save-domain-assignments"
|
||||||
|
type="button"
|
||||||
|
class="usa-button"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<form method="post" id="member-domains-edit-form" action="{{ request.path }}"> {% csrf_token %} </form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -134,7 +134,6 @@
|
||||||
id="invite-member-modal"
|
id="invite-member-modal"
|
||||||
aria-labelledby="invite-member-heading"
|
aria-labelledby="invite-member-heading"
|
||||||
aria-describedby="confirm-invite-description"
|
aria-describedby="confirm-invite-description"
|
||||||
style="display: none;"
|
|
||||||
>
|
>
|
||||||
<div class="usa-modal__content">
|
<div class="usa-modal__content">
|
||||||
<div class="usa-modal__main">
|
<div class="usa-modal__main">
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
{% include "includes/form_messages.html" %}
|
{% include "includes/form_messages.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
<h1>Organization</h1>
|
<h1>Organization</h1>
|
||||||
|
|
||||||
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
||||||
|
@ -33,7 +35,6 @@
|
||||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate>
|
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -290,3 +290,9 @@ def get_dict_value(dictionary, key):
|
||||||
if isinstance(dictionary, dict):
|
if isinstance(dictionary, dict):
|
||||||
return dictionary.get(key, "")
|
return dictionary.get(key, "")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def button_class(custom_class):
|
||||||
|
default_class = "usa-button"
|
||||||
|
return f"{default_class} {custom_class}" if custom_class else default_class
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -16,7 +16,7 @@ from registrar.utility.csv_export import (
|
||||||
DomainDataType,
|
DomainDataType,
|
||||||
DomainDataFederal,
|
DomainDataFederal,
|
||||||
DomainDataTypeUser,
|
DomainDataTypeUser,
|
||||||
DomainRequestsDataType,
|
DomainRequestDataType,
|
||||||
DomainGrowth,
|
DomainGrowth,
|
||||||
DomainManaged,
|
DomainManaged,
|
||||||
DomainUnmanaged,
|
DomainUnmanaged,
|
||||||
|
@ -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"),
|
||||||
|
@ -456,11 +456,11 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
portfolio.delete()
|
portfolio.delete()
|
||||||
|
|
||||||
def _run_domain_request_data_type_user_export(self, request):
|
def _run_domain_request_data_type_user_export(self, request):
|
||||||
"""Helper function to run the exporting_dr_data_to_csv function on DomainRequestsDataType"""
|
"""Helper function to run the export_data_to_csv function on DomainRequestDataType"""
|
||||||
|
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
|
|
||||||
DomainRequestsDataType.exporting_dr_data_to_csv(csv_file, request=request)
|
DomainRequestDataType.export_data_to_csv(csv_file, request=request)
|
||||||
|
|
||||||
csv_file.seek(0)
|
csv_file.seek(0)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -773,9 +773,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# Content
|
# Content
|
||||||
"city5.gov,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
"city5.gov,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
||||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"city2.gov,In review,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
|
"city2.gov,In review,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,NY,2,,,,0,1,city1.gov,,,,,"
|
||||||
"Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
"Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"city3.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,"
|
"city3.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,NY,2,,,,0,1,"
|
||||||
'"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
|
'"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
|
||||||
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
||||||
'Testy Tester testy2@town.com",'
|
'Testy Tester testy2@town.com",'
|
||||||
|
@ -785,7 +785,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||||
"Testy Tester testy2@town.com,"
|
"Testy Tester testy2@town.com,"
|
||||||
"cisaRep@igorville.gov,city.com,\n"
|
"cisaRep@igorville.gov,city.com,\n"
|
||||||
"city6.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
|
"city6.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,NY,2,,,,0,1,city1.gov,,,,,"
|
||||||
"Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
|
"Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
|
||||||
"cisaRep@igorville.gov,city.com,\n"
|
"cisaRep@igorville.gov,city.com,\n"
|
||||||
)
|
)
|
||||||
|
@ -794,6 +794,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -94,6 +94,12 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
DomainInvitation.objects.create(
|
DomainInvitation.objects.create(
|
||||||
email=cls.invited_member_email, domain=cls.domain2, status=DomainInvitation.DomainInvitationStatus.INVITED
|
email=cls.invited_member_email, domain=cls.domain2, status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
)
|
)
|
||||||
|
DomainInvitation.objects.create(
|
||||||
|
email=cls.invited_member_email, domain=cls.domain3, status=DomainInvitation.DomainInvitationStatus.CANCELED
|
||||||
|
)
|
||||||
|
DomainInvitation.objects.create(
|
||||||
|
email=cls.invited_member_email, domain=cls.domain4, status=DomainInvitation.DomainInvitationStatus.RETRIEVED
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
|
@ -138,7 +144,8 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
def test_get_portfolio_invitedmember_domains_json_authenticated(self):
|
def test_get_portfolio_invitedmember_domains_json_authenticated(self):
|
||||||
"""Test that portfolio invitedmember's domains are returned properly for an authenticated user."""
|
"""Test that portfolio invitedmember's domains are returned properly for an authenticated user.
|
||||||
|
CANCELED and RETRIEVED invites should be ignored."""
|
||||||
response = self.app.get(
|
response = self.app.get(
|
||||||
reverse("get_member_domains_json"),
|
reverse("get_member_domains_json"),
|
||||||
params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "true"},
|
params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "true"},
|
||||||
|
|
|
@ -157,7 +157,7 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
def test_get_portfolio_invited_json_authenticated(self):
|
def test_get_portfolio_invited_json_authenticated(self):
|
||||||
"""Test that portfolio invitees are returned properly for an authenticated user."""
|
"""Test that portfolio invitees are returned properly for an authenticated user."""
|
||||||
"""Also tests that reposnse is 200 when no domains"""
|
"""Also tests that response is 200 when no domains"""
|
||||||
UserPortfolioPermission.objects.create(
|
UserPortfolioPermission.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
|
@ -258,13 +258,14 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
role=UserDomainRole.Roles.MANAGER,
|
role=UserDomainRole.Roles.MANAGER,
|
||||||
)
|
)
|
||||||
|
|
||||||
# create domain for which user is manager and domain not in portfolio
|
# create another domain in the portfolio
|
||||||
domain2 = Domain.objects.create(
|
domain2 = Domain.objects.create(
|
||||||
name="somedomain2.com",
|
name="thissecondpermtestsmultipleperms@lets.notbreak",
|
||||||
)
|
)
|
||||||
DomainInformation.objects.create(
|
DomainInformation.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
domain=domain2,
|
domain=domain2,
|
||||||
|
portfolio=self.portfolio,
|
||||||
)
|
)
|
||||||
UserDomainRole.objects.create(
|
UserDomainRole.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
@ -272,6 +273,20 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
role=UserDomainRole.Roles.MANAGER,
|
role=UserDomainRole.Roles.MANAGER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# create domain for which user is manager and domain not in portfolio
|
||||||
|
domain3 = Domain.objects.create(
|
||||||
|
name="somedomain3.com",
|
||||||
|
)
|
||||||
|
DomainInformation.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=domain3,
|
||||||
|
)
|
||||||
|
UserDomainRole.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
domain=domain3,
|
||||||
|
role=UserDomainRole.Roles.MANAGER,
|
||||||
|
)
|
||||||
|
|
||||||
response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id})
|
response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json
|
data = response.json
|
||||||
|
@ -279,7 +294,8 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
# Check if the domain appears in the response JSON and that domain2 does not
|
# Check if the domain appears in the response JSON and that domain2 does not
|
||||||
domain_names = [domain_name for member in data["members"] for domain_name in member.get("domain_names", [])]
|
domain_names = [domain_name for member in data["members"] for domain_name in member.get("domain_names", [])]
|
||||||
self.assertIn("somedomain1.com", domain_names)
|
self.assertIn("somedomain1.com", domain_names)
|
||||||
self.assertNotIn("somedomain2.com", domain_names)
|
self.assertIn("thissecondpermtestsmultipleperms@lets.notbreak", domain_names)
|
||||||
|
self.assertNotIn("somedomain3.com", domain_names)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
|
@ -318,19 +334,33 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
domain=domain,
|
domain=domain,
|
||||||
)
|
)
|
||||||
|
|
||||||
# create a domain not in the portfolio
|
# create another domain in the portfolio
|
||||||
domain2 = Domain.objects.create(
|
domain2 = Domain.objects.create(
|
||||||
name="somedomain2.com",
|
name="thissecondinvitetestsasubqueryinjson@lets.notbreak",
|
||||||
)
|
)
|
||||||
DomainInformation.objects.create(
|
DomainInformation.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
domain=domain2,
|
domain=domain2,
|
||||||
|
portfolio=self.portfolio,
|
||||||
)
|
)
|
||||||
DomainInvitation.objects.create(
|
DomainInvitation.objects.create(
|
||||||
email=self.email6,
|
email=self.email6,
|
||||||
domain=domain2,
|
domain=domain2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# create a domain not in the portfolio
|
||||||
|
domain3 = Domain.objects.create(
|
||||||
|
name="somedomain3.com",
|
||||||
|
)
|
||||||
|
DomainInformation.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=domain3,
|
||||||
|
)
|
||||||
|
DomainInvitation.objects.create(
|
||||||
|
email=self.email6,
|
||||||
|
domain=domain3,
|
||||||
|
)
|
||||||
|
|
||||||
response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id})
|
response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json
|
data = response.json
|
||||||
|
@ -338,7 +368,8 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
# Check if the domain appears in the response JSON and domain2 does not
|
# Check if the domain appears in the response JSON and domain2 does not
|
||||||
domain_names = [domain_name for member in data["members"] for domain_name in member.get("domain_names", [])]
|
domain_names = [domain_name for member in data["members"] for domain_name in member.get("domain_names", [])]
|
||||||
self.assertIn("somedomain1.com", domain_names)
|
self.assertIn("somedomain1.com", domain_names)
|
||||||
self.assertNotIn("somedomain2.com", domain_names)
|
self.assertIn("thissecondinvitetestsasubqueryinjson@lets.notbreak", domain_names)
|
||||||
|
self.assertNotIn("somedomain3.com", domain_names)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
|
|
|
@ -14,6 +14,7 @@ from registrar.models import (
|
||||||
Suborganization,
|
Suborganization,
|
||||||
AllowedEmail,
|
AllowedEmail,
|
||||||
)
|
)
|
||||||
|
from registrar.models.domain_invitation import DomainInvitation
|
||||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||||
from registrar.models.user_group import UserGroup
|
from registrar.models.user_group import UserGroup
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
|
@ -25,6 +26,7 @@ from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -1927,7 +1929,7 @@ class TestPortfolioMemberDomainsView(TestWithUser, WebTest):
|
||||||
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio")
|
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio")
|
||||||
|
|
||||||
# Assign permissions to the user making requests
|
# Assign permissions to the user making requests
|
||||||
UserPortfolioPermission.objects.create(
|
cls.portfolio_permission = UserPortfolioPermission.objects.create(
|
||||||
user=cls.user,
|
user=cls.user,
|
||||||
portfolio=cls.portfolio,
|
portfolio=cls.portfolio,
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
@ -2106,11 +2108,22 @@ class TestPortfolioMemberDomainsEditView(TestPortfolioMemberDomainsView):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
cls.url = reverse("member-domains-edit", kwargs={"pk": cls.portfolio_permission.pk})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
names = ["1.gov", "2.gov", "3.gov"]
|
||||||
|
Domain.objects.bulk_create([Domain(name=name) for name in names])
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
|
@ -2162,16 +2175,140 @@ class TestPortfolioMemberDomainsEditView(TestPortfolioMemberDomainsView):
|
||||||
# Make sure the response is not found
|
# Make sure the response is not found
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_valid_added_domains(self):
|
||||||
|
"""Test that domains can be successfully added."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"added_domains": json.dumps([1, 2, 3]), # Mock domain IDs
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that the UserDomainRole objects were created
|
||||||
|
self.assertEqual(UserDomainRole.objects.filter(user=self.user, role=UserDomainRole.Roles.MANAGER).count(), 3)
|
||||||
|
|
||||||
|
# Check for a success message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_valid_removed_domains(self):
|
||||||
|
"""Test that domains can be successfully removed."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Create some UserDomainRole objects
|
||||||
|
domains = [1, 2, 3]
|
||||||
|
UserDomainRole.objects.bulk_create([UserDomainRole(domain_id=domain, user=self.user) for domain in domains])
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"removed_domains": json.dumps([1, 2]),
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that the UserDomainRole objects were deleted
|
||||||
|
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 1)
|
||||||
|
self.assertEqual(UserDomainRole.objects.filter(domain_id=3, user=self.user).count(), 1)
|
||||||
|
|
||||||
|
# Check for a success message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||||
|
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_invalid_added_domains_data(self):
|
||||||
|
"""Test that an error is returned for invalid added domains data."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"added_domains": "json-statham",
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that no UserDomainRole objects were created
|
||||||
|
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
|
||||||
|
|
||||||
|
# Check for an error message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
str(messages[0]), "Invalid data for added domains. If the issue persists, please contact help@get.gov."
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_invalid_removed_domains_data(self):
|
||||||
|
"""Test that an error is returned for invalid removed domains data."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"removed_domains": "not-a-json",
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that no UserDomainRole objects were deleted
|
||||||
|
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
|
||||||
|
|
||||||
|
# Check for an error message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
str(messages[0]), "Invalid data for removed domains. If the issue persists, please contact help@get.gov."
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_no_changes(self):
|
||||||
|
"""Test that no changes message is displayed when no changes are made."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.post(self.url, {})
|
||||||
|
|
||||||
|
# Check that no UserDomainRole objects were created or deleted
|
||||||
|
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
|
||||||
|
|
||||||
|
# Check for an info message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(str(messages[0]), "No changes detected.")
|
||||||
|
|
||||||
|
|
||||||
class TestPortfolioInvitedMemberEditDomainsView(TestPortfolioInvitedMemberDomainsView):
|
class TestPortfolioInvitedMemberEditDomainsView(TestPortfolioInvitedMemberDomainsView):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
cls.url = reverse("invitedmember-domains-edit", kwargs={"pk": cls.invitation.pk})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
names = ["1.gov", "2.gov", "3.gov"]
|
||||||
|
Domain.objects.bulk_create([Domain(name=name) for name in names])
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
DomainInvitation.objects.all().delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
|
@ -2222,6 +2359,175 @@ class TestPortfolioInvitedMemberEditDomainsView(TestPortfolioInvitedMemberDomain
|
||||||
# Make sure the response is not found
|
# Make sure the response is not found
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_valid_added_domains(self):
|
||||||
|
"""Test adding new domains successfully."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"added_domains": json.dumps([1, 2, 3]), # Mock domain IDs
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that the DomainInvitation objects were created
|
||||||
|
self.assertEqual(
|
||||||
|
DomainInvitation.objects.filter(
|
||||||
|
email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
|
).count(),
|
||||||
|
3,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for a success message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_existing_and_new_added_domains(self):
|
||||||
|
"""Test updating existing and adding new invitations."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Create existing invitations
|
||||||
|
DomainInvitation.objects.bulk_create(
|
||||||
|
[
|
||||||
|
DomainInvitation(
|
||||||
|
domain_id=1, email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.CANCELED
|
||||||
|
),
|
||||||
|
DomainInvitation(
|
||||||
|
domain_id=2, email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"added_domains": json.dumps([1, 2, 3]),
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that status for domain_id=1 was updated to INVITED
|
||||||
|
self.assertEqual(
|
||||||
|
DomainInvitation.objects.get(domain_id=1, email="invited@example.com").status,
|
||||||
|
DomainInvitation.DomainInvitationStatus.INVITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that domain_id=3 was created as INVITED
|
||||||
|
self.assertTrue(
|
||||||
|
DomainInvitation.objects.filter(
|
||||||
|
domain_id=3, email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for a success message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_valid_removed_domains(self):
|
||||||
|
"""Test removing domains successfully."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Create existing invitations
|
||||||
|
DomainInvitation.objects.bulk_create(
|
||||||
|
[
|
||||||
|
DomainInvitation(
|
||||||
|
domain_id=1, email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
|
),
|
||||||
|
DomainInvitation(
|
||||||
|
domain_id=2, email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"removed_domains": json.dumps([1]),
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that the status for domain_id=1 was updated to CANCELED
|
||||||
|
self.assertEqual(
|
||||||
|
DomainInvitation.objects.get(domain_id=1, email="invited@example.com").status,
|
||||||
|
DomainInvitation.DomainInvitationStatus.CANCELED,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that domain_id=2 remains INVITED
|
||||||
|
self.assertEqual(
|
||||||
|
DomainInvitation.objects.get(domain_id=2, email="invited@example.com").status,
|
||||||
|
DomainInvitation.DomainInvitationStatus.INVITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for a success message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_invalid_added_domains_data(self):
|
||||||
|
"""Test handling of invalid JSON for added domains."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"added_domains": "not-a-json",
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that no DomainInvitation objects were created
|
||||||
|
self.assertEqual(DomainInvitation.objects.count(), 0)
|
||||||
|
|
||||||
|
# Check for an error message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
str(messages[0]), "Invalid data for added domains. If the issue persists, please contact help@get.gov."
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_invalid_removed_domains_data(self):
|
||||||
|
"""Test handling of invalid JSON for removed domains."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"removed_domains": "json-sudeikis",
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
# Check that no DomainInvitation objects were updated
|
||||||
|
self.assertEqual(DomainInvitation.objects.count(), 0)
|
||||||
|
|
||||||
|
# Check for an error message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
str(messages[0]), "Invalid data for removed domains. If the issue persists, please contact help@get.gov."
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_post_with_no_changes(self):
|
||||||
|
"""Test the case where no changes are made."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.post(self.url, {})
|
||||||
|
|
||||||
|
# Check that no DomainInvitation objects were created or updated
|
||||||
|
self.assertEqual(DomainInvitation.objects.count(), 0)
|
||||||
|
|
||||||
|
# Check for an info message and a redirect
|
||||||
|
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||||
|
messages = list(response.wsgi_request._messages)
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(str(messages[0]), "No changes detected.")
|
||||||
|
|
||||||
|
|
||||||
class TestRequestingEntity(WebTest):
|
class TestRequestingEntity(WebTest):
|
||||||
"""The requesting entity page is a domain request form that only exists
|
"""The requesting entity page is a domain request form that only exists
|
||||||
|
|
|
@ -538,11 +538,23 @@ class DomainExport(BaseExport):
|
||||||
# model objects as we export data, trying to reinstate model objects in order to grab @property
|
# model objects as we export data, trying to reinstate model objects in order to grab @property
|
||||||
# values negatively impacts performance. Therefore, we will follow best practice and use annotations
|
# values negatively impacts performance. Therefore, we will follow best practice and use annotations
|
||||||
return {
|
return {
|
||||||
"converted_generic_org_type": Case(
|
"converted_org_type": Case(
|
||||||
# When portfolio is present, use its value instead
|
# When portfolio is present and is_election_board is True
|
||||||
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
When(
|
||||||
|
portfolio__isnull=False,
|
||||||
|
portfolio__organization_type__isnull=False,
|
||||||
|
is_election_board=True,
|
||||||
|
then=Concat(F("portfolio__organization_type"), Value("_election")),
|
||||||
|
),
|
||||||
|
# When portfolio is present and is_election_board is False or None
|
||||||
|
When(
|
||||||
|
Q(is_election_board=False) | Q(is_election_board__isnull=True),
|
||||||
|
portfolio__isnull=False,
|
||||||
|
portfolio__organization_type__isnull=False,
|
||||||
|
then=F("portfolio__organization_type"),
|
||||||
|
),
|
||||||
# Otherwise, return the natively assigned value
|
# Otherwise, return the natively assigned value
|
||||||
default=F("generic_org_type"),
|
default=F("organization_type"),
|
||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
),
|
),
|
||||||
"converted_federal_agency": Case(
|
"converted_federal_agency": Case(
|
||||||
|
@ -573,20 +585,6 @@ class DomainExport(BaseExport):
|
||||||
default=F("organization_name"),
|
default=F("organization_name"),
|
||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
),
|
),
|
||||||
"converted_city": Case(
|
|
||||||
# When portfolio is present, use its value instead
|
|
||||||
When(portfolio__isnull=False, then=F("portfolio__city")),
|
|
||||||
# Otherwise, return the natively assigned value
|
|
||||||
default=F("city"),
|
|
||||||
output_field=CharField(),
|
|
||||||
),
|
|
||||||
"converted_state_territory": Case(
|
|
||||||
# When portfolio is present, use its value instead
|
|
||||||
When(portfolio__isnull=False, then=F("portfolio__state_territory")),
|
|
||||||
# Otherwise, return the natively assigned value
|
|
||||||
default=F("state_territory"),
|
|
||||||
output_field=CharField(),
|
|
||||||
),
|
|
||||||
"converted_so_email": Case(
|
"converted_so_email": Case(
|
||||||
# When portfolio is present, use its value instead
|
# When portfolio is present, use its value instead
|
||||||
When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
|
||||||
|
@ -727,7 +725,8 @@ class DomainExport(BaseExport):
|
||||||
first_ready_on = "(blank)"
|
first_ready_on = "(blank)"
|
||||||
|
|
||||||
# organization_type has organization_type AND is_election
|
# organization_type has organization_type AND is_election
|
||||||
domain_org_type = model.get("converted_generic_org_type")
|
# domain_org_type includes "- Election" org_type variants
|
||||||
|
domain_org_type = model.get("converted_org_type")
|
||||||
human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
|
human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
|
||||||
domain_federal_type = model.get("converted_federal_type")
|
domain_federal_type = model.get("converted_federal_type")
|
||||||
human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
|
human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
|
||||||
|
@ -744,30 +743,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("city"),
|
||||||
"State": model.get("converted_state_territory"),
|
"State": model.get("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."""
|
||||||
|
@ -893,7 +907,7 @@ class DomainDataType(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",
|
"converted_org_type",
|
||||||
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
||||||
"converted_federal_agency",
|
"converted_federal_agency",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
|
@ -972,111 +986,45 @@ class DomainDataTypeUser(DomainDataType):
|
||||||
return Q(domain__id__in=request.user.get_user_domain_ids(request))
|
return Q(domain__id__in=request.user.get_user_domain_ids(request))
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestsDataType:
|
|
||||||
"""
|
|
||||||
The DomainRequestsDataType report, but filtered based on the current request user
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_filter_conditions(cls, request=None, **kwargs):
|
|
||||||
if request is None or not hasattr(request, "user") or not request.user.is_authenticated:
|
|
||||||
return Q(id__in=[])
|
|
||||||
|
|
||||||
request_ids = request.user.get_user_domain_request_ids(request)
|
|
||||||
return Q(id__in=request_ids)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_queryset(cls, request):
|
|
||||||
return DomainRequest.objects.filter(cls.get_filter_conditions(request))
|
|
||||||
|
|
||||||
def safe_get(attribute, default="N/A"):
|
|
||||||
# Return the attribute value or default if not present
|
|
||||||
return attribute if attribute is not None else default
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def exporting_dr_data_to_csv(cls, response, request=None):
|
|
||||||
import csv
|
|
||||||
|
|
||||||
writer = csv.writer(response)
|
|
||||||
|
|
||||||
# CSV headers
|
|
||||||
writer.writerow(
|
|
||||||
[
|
|
||||||
"Domain request",
|
|
||||||
"Region",
|
|
||||||
"Status",
|
|
||||||
"Election office",
|
|
||||||
"Federal type",
|
|
||||||
"Domain type",
|
|
||||||
"Request additional details",
|
|
||||||
"Creator approved domains count",
|
|
||||||
"Creator active requests count",
|
|
||||||
"Alternative domains",
|
|
||||||
"Other contacts",
|
|
||||||
"Current websites",
|
|
||||||
"Federal agency",
|
|
||||||
"SO first name",
|
|
||||||
"SO last name",
|
|
||||||
"SO email",
|
|
||||||
"SO title/role",
|
|
||||||
"Creator first name",
|
|
||||||
"Creator last name",
|
|
||||||
"Creator email",
|
|
||||||
"Organization name",
|
|
||||||
"City",
|
|
||||||
"State/territory",
|
|
||||||
"Request purpose",
|
|
||||||
"CISA regional representative",
|
|
||||||
"Last submitted date",
|
|
||||||
"First submitted date",
|
|
||||||
"Last status update",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
queryset = cls.get_queryset(request)
|
|
||||||
for request in queryset:
|
|
||||||
writer.writerow(
|
|
||||||
[
|
|
||||||
request.requested_domain,
|
|
||||||
cls.safe_get(getattr(request, "region_field", None)),
|
|
||||||
request.status,
|
|
||||||
cls.safe_get(getattr(request, "election_office", None)),
|
|
||||||
request.converted_federal_type,
|
|
||||||
cls.safe_get(getattr(request, "domain_type", None)),
|
|
||||||
cls.safe_get(getattr(request, "additional_details", None)),
|
|
||||||
cls.safe_get(getattr(request, "creator_approved_domains_count", None)),
|
|
||||||
cls.safe_get(getattr(request, "creator_active_requests_count", None)),
|
|
||||||
cls.safe_get(getattr(request, "all_alternative_domains", None)),
|
|
||||||
cls.safe_get(getattr(request, "all_other_contacts", None)),
|
|
||||||
cls.safe_get(getattr(request, "all_current_websites", None)),
|
|
||||||
cls.safe_get(getattr(request, "converted_federal_agency", None)),
|
|
||||||
cls.safe_get(getattr(request.converted_senior_official, "first_name", None)),
|
|
||||||
cls.safe_get(getattr(request.converted_senior_official, "last_name", None)),
|
|
||||||
cls.safe_get(getattr(request.converted_senior_official, "email", None)),
|
|
||||||
cls.safe_get(getattr(request.converted_senior_official, "title", None)),
|
|
||||||
cls.safe_get(getattr(request.creator, "first_name", None)),
|
|
||||||
cls.safe_get(getattr(request.creator, "last_name", None)),
|
|
||||||
cls.safe_get(getattr(request.creator, "email", None)),
|
|
||||||
cls.safe_get(getattr(request, "converted_organization_name", None)),
|
|
||||||
cls.safe_get(getattr(request, "converted_city", None)),
|
|
||||||
cls.safe_get(getattr(request, "converted_state_territory", None)),
|
|
||||||
cls.safe_get(getattr(request, "purpose", None)),
|
|
||||||
cls.safe_get(getattr(request, "cisa_representative_email", None)),
|
|
||||||
cls.safe_get(getattr(request, "last_submitted_date", None)),
|
|
||||||
cls.safe_get(getattr(request, "first_submitted_date", None)),
|
|
||||||
cls.safe_get(getattr(request, "last_status_update", None)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class DomainDataFull(DomainExport):
|
class DomainDataFull(DomainExport):
|
||||||
"""
|
"""
|
||||||
Shows security contacts, filtered by state
|
Shows security contacts, filtered by state
|
||||||
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 +1054,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 +1112,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 +1174,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1679,20 +1660,6 @@ class DomainRequestExport(BaseExport):
|
||||||
default=F("organization_name"),
|
default=F("organization_name"),
|
||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
),
|
),
|
||||||
"converted_city": Case(
|
|
||||||
# When portfolio is present, use its value instead
|
|
||||||
When(portfolio__isnull=False, then=F("portfolio__city")),
|
|
||||||
# Otherwise, return the natively assigned value
|
|
||||||
default=F("city"),
|
|
||||||
output_field=CharField(),
|
|
||||||
),
|
|
||||||
"converted_state_territory": Case(
|
|
||||||
# When portfolio is present, use its value instead
|
|
||||||
When(portfolio__isnull=False, then=F("portfolio__state_territory")),
|
|
||||||
# Otherwise, return the natively assigned value
|
|
||||||
default=F("state_territory"),
|
|
||||||
output_field=CharField(),
|
|
||||||
),
|
|
||||||
"converted_so_email": Case(
|
"converted_so_email": Case(
|
||||||
# When portfolio is present, use its value instead
|
# When portfolio is present, use its value instead
|
||||||
When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
|
||||||
|
@ -1871,8 +1838,8 @@ class DomainRequestExport(BaseExport):
|
||||||
"Investigator": model.get("investigator__email"),
|
"Investigator": model.get("investigator__email"),
|
||||||
# Untouched fields
|
# Untouched fields
|
||||||
"Organization name": model.get("converted_organization_name"),
|
"Organization name": model.get("converted_organization_name"),
|
||||||
"City": model.get("converted_city"),
|
"City": model.get("city"),
|
||||||
"State/territory": model.get("converted_state_territory"),
|
"State/territory": model.get("state_territory"),
|
||||||
"Request purpose": model.get("purpose"),
|
"Request purpose": model.get("purpose"),
|
||||||
"CISA regional representative": model.get("cisa_representative_email"),
|
"CISA regional representative": model.get("cisa_representative_email"),
|
||||||
"Last submitted date": model.get("last_submitted_date"),
|
"Last submitted date": model.get("last_submitted_date"),
|
||||||
|
@ -1884,6 +1851,92 @@ class DomainRequestExport(BaseExport):
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
class DomainRequestDataType(DomainRequestExport):
|
||||||
|
"""
|
||||||
|
The DomainRequestDataType report, but filtered based on the current request user
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_columns(cls):
|
||||||
|
"""
|
||||||
|
Overrides the columns for CSV export specific to DomainRequestDataType.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
"Domain request",
|
||||||
|
"Region",
|
||||||
|
"Status",
|
||||||
|
"Election office",
|
||||||
|
"Federal type",
|
||||||
|
"Domain type",
|
||||||
|
"Request additional details",
|
||||||
|
"Creator approved domains count",
|
||||||
|
"Creator active requests count",
|
||||||
|
"Alternative domains",
|
||||||
|
"Other contacts",
|
||||||
|
"Current websites",
|
||||||
|
"Federal agency",
|
||||||
|
"SO first name",
|
||||||
|
"SO last name",
|
||||||
|
"SO email",
|
||||||
|
"SO title/role",
|
||||||
|
"Creator first name",
|
||||||
|
"Creator last name",
|
||||||
|
"Creator email",
|
||||||
|
"Organization name",
|
||||||
|
"City",
|
||||||
|
"State/territory",
|
||||||
|
"Request purpose",
|
||||||
|
"CISA regional representative",
|
||||||
|
"Last submitted date",
|
||||||
|
"First submitted date",
|
||||||
|
"Last status update",
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_filter_conditions(cls, request=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Get a Q object of filter conditions to filter when building queryset.
|
||||||
|
"""
|
||||||
|
if request is None or not hasattr(request, "user") or not request.user:
|
||||||
|
# Return nothing
|
||||||
|
return Q(id__in=[])
|
||||||
|
else:
|
||||||
|
# Get all domain requests the user is associated with
|
||||||
|
return Q(id__in=request.user.get_user_domain_request_ids(request))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_select_related(cls):
|
||||||
|
"""
|
||||||
|
Get a list of tables to pass to select_related when building queryset.
|
||||||
|
"""
|
||||||
|
return ["creator", "senior_official", "federal_agency", "investigator", "requested_domain"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prefetch_related(cls):
|
||||||
|
"""
|
||||||
|
Get a list of tables to pass to prefetch_related when building queryset.
|
||||||
|
"""
|
||||||
|
return ["current_websites", "other_contacts", "alternative_domains"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_related_table_fields(cls):
|
||||||
|
"""
|
||||||
|
Get a list of fields from related tables.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
"requested_domain__name",
|
||||||
|
"federal_agency__agency",
|
||||||
|
"senior_official__first_name",
|
||||||
|
"senior_official__last_name",
|
||||||
|
"senior_official__email",
|
||||||
|
"senior_official__title",
|
||||||
|
"creator__first_name",
|
||||||
|
"creator__last_name",
|
||||||
|
"creator__email",
|
||||||
|
"investigator__email",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestGrowth(DomainRequestExport):
|
class DomainRequestGrowth(DomainRequestExport):
|
||||||
"""
|
"""
|
||||||
Shows submitted requests within a date range, sorted
|
Shows submitted requests within a date range, sorted
|
||||||
|
|
|
@ -805,15 +805,6 @@ class DomainDNSSECView(DomainFormBaseView):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
has_dnssec_records = self.object.dnssecdata is not None
|
has_dnssec_records = self.object.dnssecdata is not None
|
||||||
|
|
||||||
# Create HTML for the modal button
|
|
||||||
modal_button = (
|
|
||||||
'<button type="submit" '
|
|
||||||
'class="usa-button usa-button--secondary" '
|
|
||||||
'name="disable_dnssec">Confirm</button>'
|
|
||||||
)
|
|
||||||
|
|
||||||
context["modal_button"] = modal_button
|
|
||||||
context["has_dnssec_records"] = has_dnssec_records
|
context["has_dnssec_records"] = has_dnssec_records
|
||||||
context["dnssec_enabled"] = self.request.session.pop("dnssec_enabled", False)
|
context["dnssec_enabled"] = self.request.session.pop("dnssec_enabled", False)
|
||||||
|
|
||||||
|
@ -906,15 +897,6 @@ class DomainDsDataView(DomainFormBaseView):
|
||||||
# to preserve the context["form"]
|
# to preserve the context["form"]
|
||||||
context = super().get_context_data(form=formset)
|
context = super().get_context_data(form=formset)
|
||||||
context["trigger_modal"] = True
|
context["trigger_modal"] = True
|
||||||
# Create HTML for the modal button
|
|
||||||
modal_button = (
|
|
||||||
'<button type="submit" '
|
|
||||||
'class="usa-button usa-button--secondary" '
|
|
||||||
'name="disable-override-click">Remove all DS data</button>'
|
|
||||||
)
|
|
||||||
|
|
||||||
# context to back out of a broken form on all fields delete
|
|
||||||
context["modal_button"] = modal_button
|
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
if formset.is_valid() or override:
|
if formset.is_valid() or override:
|
||||||
|
@ -1050,9 +1032,6 @@ class DomainUsersView(DomainBaseView):
|
||||||
# Add conditionals to the context (such as "can_delete_users")
|
# Add conditionals to the context (such as "can_delete_users")
|
||||||
context = self._add_booleans_to_context(context)
|
context = self._add_booleans_to_context(context)
|
||||||
|
|
||||||
# Add modal buttons to the context (such as for delete)
|
|
||||||
context = self._add_modal_buttons_to_context(context)
|
|
||||||
|
|
||||||
# Get portfolio from session (if set)
|
# Get portfolio from session (if set)
|
||||||
portfolio = self.request.session.get("portfolio")
|
portfolio = self.request.session.get("portfolio")
|
||||||
|
|
||||||
|
@ -1149,26 +1128,6 @@ class DomainUsersView(DomainBaseView):
|
||||||
context["can_delete_users"] = can_delete_users
|
context["can_delete_users"] = can_delete_users
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def _add_modal_buttons_to_context(self, context):
|
|
||||||
"""Adds modal buttons (and their HTML) to the context"""
|
|
||||||
# Create HTML for the modal button
|
|
||||||
modal_button = (
|
|
||||||
'<button type="submit" '
|
|
||||||
'class="usa-button usa-button--secondary" '
|
|
||||||
'name="delete_domain_manager">Yes, remove domain manager</button>'
|
|
||||||
)
|
|
||||||
context["modal_button"] = modal_button
|
|
||||||
|
|
||||||
# Create HTML for the modal button when deleting yourself
|
|
||||||
modal_button_self = (
|
|
||||||
'<button type="submit" '
|
|
||||||
'class="usa-button usa-button--secondary" '
|
|
||||||
'name="delete_domain_manager_self">Yes, remove myself</button>'
|
|
||||||
)
|
|
||||||
context["modal_button_self"] = modal_button_self
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class DomainAddUserView(DomainFormBaseView):
|
class DomainAddUserView(DomainFormBaseView):
|
||||||
"""Inside of a domain's user management, a form for adding users.
|
"""Inside of a domain's user management, a form for adding users.
|
||||||
|
|
|
@ -448,34 +448,21 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
non_org_steps_complete = DomainRequest._form_complete(self.domain_request, self.request)
|
non_org_steps_complete = DomainRequest._form_complete(self.domain_request, self.request)
|
||||||
org_steps_complete = len(self.db_check_for_unlocking_steps()) == len(self.steps)
|
org_steps_complete = len(self.db_check_for_unlocking_steps()) == len(self.steps)
|
||||||
if (not self.is_portfolio and non_org_steps_complete) or (self.is_portfolio and org_steps_complete):
|
if (not self.is_portfolio and non_org_steps_complete) or (self.is_portfolio and org_steps_complete):
|
||||||
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
|
||||||
context = {
|
context = {
|
||||||
"not_form": False,
|
|
||||||
"form_titles": self.titles,
|
"form_titles": self.titles,
|
||||||
"steps": self.steps,
|
"steps": self.steps,
|
||||||
"visited": self.storage.get("step_history", []),
|
"visited": self.storage.get("step_history", []),
|
||||||
"is_federal": self.domain_request.is_federal(),
|
"is_federal": self.domain_request.is_federal(),
|
||||||
"modal_button": modal_button,
|
|
||||||
"modal_heading": "You are about to submit a domain request for ",
|
|
||||||
"domain_name_modal": str(self.domain_request.requested_domain),
|
|
||||||
"modal_description": "Once you submit this request, you won’t be able to edit it until we review it.\
|
|
||||||
You’ll only be able to withdraw your request.",
|
|
||||||
"review_form_is_complete": True,
|
"review_form_is_complete": True,
|
||||||
"user": self.request.user,
|
"user": self.request.user,
|
||||||
"requested_domain__name": requested_domain_name,
|
"requested_domain__name": requested_domain_name,
|
||||||
}
|
}
|
||||||
else: # form is not complete
|
else: # form is not complete
|
||||||
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
|
||||||
context = {
|
context = {
|
||||||
"not_form": True,
|
|
||||||
"form_titles": self.titles,
|
"form_titles": self.titles,
|
||||||
"steps": self.steps,
|
"steps": self.steps,
|
||||||
"visited": self.storage.get("step_history", []),
|
"visited": self.storage.get("step_history", []),
|
||||||
"is_federal": self.domain_request.is_federal(),
|
"is_federal": self.domain_request.is_federal(),
|
||||||
"modal_button": modal_button,
|
|
||||||
"modal_heading": "Your request form is incomplete",
|
|
||||||
"modal_description": 'This request cannot be submitted yet.\
|
|
||||||
Return to the request and visit the steps that are marked as "incomplete."',
|
|
||||||
"review_form_is_complete": False,
|
"review_form_is_complete": False,
|
||||||
"user": self.request.user,
|
"user": self.request.user,
|
||||||
"requested_domain__name": requested_domain_name,
|
"requested_domain__name": requested_domain_name,
|
||||||
|
|
|
@ -109,6 +109,10 @@ def apply_sorting(queryset, request):
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
|
||||||
|
# Handle special case for 'creator'
|
||||||
|
if sort_by == "creator":
|
||||||
|
sort_by = "creator__email"
|
||||||
|
|
||||||
if order == "desc":
|
if order == "desc":
|
||||||
sort_by = f"-{sort_by}"
|
sort_by = f"-{sort_by}"
|
||||||
return queryset.order_by(sort_by)
|
return queryset.order_by(sort_by)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -90,7 +90,9 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
||||||
domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list(
|
domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list(
|
||||||
"domain_id", flat=True
|
"domain_id", flat=True
|
||||||
)
|
)
|
||||||
domain_invitations = DomainInvitation.objects.filter(email=email).values_list("domain_id", flat=True)
|
domain_invitations = DomainInvitation.objects.filter(
|
||||||
|
email=email, status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
|
).values_list("domain_id", flat=True)
|
||||||
return domain_info_ids.intersection(domain_invitations)
|
return domain_info_ids.intersection(domain_invitations)
|
||||||
else:
|
else:
|
||||||
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
||||||
|
|
|
@ -12,6 +12,7 @@ from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from registrar.views.utility.mixins import PortfolioMembersPermission
|
from registrar.views.utility.mixins import PortfolioMembersPermission
|
||||||
from registrar.models.utility.orm_helper import ArrayRemoveNull
|
from registrar.models.utility.orm_helper import ArrayRemoveNull
|
||||||
|
from django.contrib.postgres.aggregates import StringAgg
|
||||||
|
|
||||||
|
|
||||||
class PortfolioMembersJson(PortfolioMembersPermission, View):
|
class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
|
@ -119,11 +120,22 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
|
|
||||||
def initial_invitations_search(self, portfolio):
|
def initial_invitations_search(self, portfolio):
|
||||||
"""Perform initial invitations search and get related DomainInvitation data based on the email."""
|
"""Perform initial invitations search and get related DomainInvitation data based on the email."""
|
||||||
# Get DomainInvitation query for matching email and for the portfolio
|
|
||||||
domain_invitations = DomainInvitation.objects.filter(
|
# Subquery to get concatenated domain information for each email
|
||||||
email=OuterRef("email"), # Check if email matches the OuterRef("email")
|
domain_invitations = (
|
||||||
domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio
|
DomainInvitation.objects.filter(email=OuterRef("email"), domain__domain_info__portfolio=portfolio)
|
||||||
).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField()))
|
.annotate(
|
||||||
|
concatenated_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())
|
||||||
|
)
|
||||||
|
.values("concatenated_info")
|
||||||
|
)
|
||||||
|
|
||||||
|
concatenated_domain_info = (
|
||||||
|
domain_invitations.values("email")
|
||||||
|
.annotate(domain_info=StringAgg("concatenated_info", delimiter=", "))
|
||||||
|
.values("domain_info")
|
||||||
|
)
|
||||||
|
|
||||||
# PortfolioInvitation query
|
# PortfolioInvitation query
|
||||||
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
|
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
|
||||||
invitations = invitations.annotate(
|
invitations = invitations.annotate(
|
||||||
|
@ -136,7 +148,12 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
# Use ArrayRemove to return an empty list when no domain invitations are found
|
# Use ArrayRemove to return an empty list when no domain invitations are found
|
||||||
domain_info=ArrayRemoveNull(
|
domain_info=ArrayRemoveNull(
|
||||||
ArrayAgg(
|
ArrayAgg(
|
||||||
Subquery(domain_invitations.values("domain_info")),
|
# We've pre-concatenated the domain infos to limit the subquery to return a single virtual 'row',
|
||||||
|
# otherwise we'll trigger a "more than one row returned by a subquery used as an expression"
|
||||||
|
# when an email matches multiple domain invitations.
|
||||||
|
# We'll take care when processing the list of one single concatenated items item
|
||||||
|
# in serialize_members.
|
||||||
|
Subquery(concatenated_domain_info),
|
||||||
distinct=True,
|
distinct=True,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -153,6 +170,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
"domain_info",
|
"domain_info",
|
||||||
"type",
|
"type",
|
||||||
)
|
)
|
||||||
|
|
||||||
return invitations
|
return invitations
|
||||||
|
|
||||||
def apply_search_term(self, queryset, request):
|
def apply_search_term(self, queryset, request):
|
||||||
|
@ -190,10 +208,19 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles") or [])
|
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles") or [])
|
||||||
action_url = reverse(item["type"], kwargs={"pk": item["id"]})
|
action_url = reverse(item["type"], kwargs={"pk": item["id"]})
|
||||||
|
|
||||||
|
item_type = item.get("type", "")
|
||||||
|
|
||||||
|
# Ensure domain_info is properly processed for invites -
|
||||||
|
# we need to un-concatenate the subquery
|
||||||
|
domain_info_list = item.get("domain_info", [])
|
||||||
|
if item_type == "invitedmember" and isinstance(domain_info_list, list) and domain_info_list:
|
||||||
|
# Split the first item in the list if it exists
|
||||||
|
domain_info_list = domain_info_list[0].split(", ")
|
||||||
|
|
||||||
# Serialize member data
|
# Serialize member data
|
||||||
member_json = {
|
member_json = {
|
||||||
"id": item.get("id", ""), # id is id of UserPortfolioPermission or PortfolioInvitation
|
"id": item.get("id", ""), # id is id of UserPortfolioPermission or PortfolioInvitation
|
||||||
"type": item.get("type", ""), # source is member or invitedmember
|
"type": item_type, # source is member or invitedmember
|
||||||
"name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])),
|
"name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])),
|
||||||
"email": item.get("email_display", ""),
|
"email": item.get("email_display", ""),
|
||||||
"member_display": item.get("member_display", ""),
|
"member_display": item.get("member_display", ""),
|
||||||
|
@ -203,9 +230,9 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
),
|
),
|
||||||
# split domain_info array values into ids to form urls, and names
|
# split domain_info array values into ids to form urls, and names
|
||||||
"domain_urls": [
|
"domain_urls": [
|
||||||
reverse("domain", kwargs={"pk": domain_info.split(":")[0]}) for domain_info in item.get("domain_info")
|
reverse("domain", kwargs={"pk": domain_info.split(":")[0]}) for domain_info in domain_info_list
|
||||||
],
|
],
|
||||||
"domain_names": [domain_info.split(":")[1] for domain_info in item.get("domain_info")],
|
"domain_names": [domain_info.split(":")[1] for domain_info in domain_info_list],
|
||||||
"is_admin": is_admin,
|
"is_admin": is_admin,
|
||||||
"last_active": item.get("last_active"),
|
"last_active": item.get("last_active"),
|
||||||
"action_url": action_url,
|
"action_url": action_url,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -8,10 +9,13 @@ 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.portfolio_invitation import PortfolioInvitation
|
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||||
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from registrar.utility.email import EmailSendingError
|
from registrar.utility.email import EmailSendingError
|
||||||
|
from registrar.utility.enums import DefaultUserValues
|
||||||
from registrar.views.utility.mixins import PortfolioMemberPermission
|
from registrar.views.utility.mixins import PortfolioMemberPermission
|
||||||
from registrar.views.utility.permission_views import (
|
from registrar.views.utility.permission_views import (
|
||||||
PortfolioDomainRequestsPermissionView,
|
PortfolioDomainRequestsPermissionView,
|
||||||
|
@ -26,6 +30,7 @@ from registrar.views.utility.permission_views import (
|
||||||
)
|
)
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -39,6 +44,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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,6 +227,86 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
"""
|
||||||
|
Handles adding and removing domains for a portfolio member.
|
||||||
|
"""
|
||||||
|
added_domains = request.POST.get("added_domains")
|
||||||
|
removed_domains = request.POST.get("removed_domains")
|
||||||
|
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||||
|
member = portfolio_permission.user
|
||||||
|
|
||||||
|
added_domain_ids = self._parse_domain_ids(added_domains, "added domains")
|
||||||
|
if added_domain_ids is None:
|
||||||
|
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||||
|
|
||||||
|
removed_domain_ids = self._parse_domain_ids(removed_domains, "removed domains")
|
||||||
|
if removed_domain_ids is None:
|
||||||
|
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||||
|
|
||||||
|
if added_domain_ids or removed_domain_ids:
|
||||||
|
try:
|
||||||
|
self._process_added_domains(added_domain_ids, member)
|
||||||
|
self._process_removed_domains(removed_domain_ids, member)
|
||||||
|
messages.success(request, "The domain assignment changes have been saved.")
|
||||||
|
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||||
|
except IntegrityError:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"A database error occurred while saving changes. If the issue persists, "
|
||||||
|
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||||
|
)
|
||||||
|
logger.error("A database error occurred while saving changes.")
|
||||||
|
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"An unexpected error occurred: {str(e)}. If the issue persists, "
|
||||||
|
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||||
|
)
|
||||||
|
logger.error(f"An unexpected error occurred: {str(e)}")
|
||||||
|
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
|
||||||
|
else:
|
||||||
|
messages.info(request, "No changes detected.")
|
||||||
|
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||||
|
|
||||||
|
def _parse_domain_ids(self, domain_data, domain_type):
|
||||||
|
"""
|
||||||
|
Parses the domain IDs from the request and handles JSON errors.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return json.loads(domain_data) if domain_data else []
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
f"Invalid data for {domain_type}. If the issue persists, "
|
||||||
|
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||||
|
)
|
||||||
|
logger.error(f"Invalid data for {domain_type}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _process_added_domains(self, added_domain_ids, member):
|
||||||
|
"""
|
||||||
|
Processes added domains by bulk creating UserDomainRole instances.
|
||||||
|
"""
|
||||||
|
if added_domain_ids:
|
||||||
|
# Bulk create UserDomainRole instances for added domains
|
||||||
|
UserDomainRole.objects.bulk_create(
|
||||||
|
[
|
||||||
|
UserDomainRole(domain_id=domain_id, user=member, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
for domain_id in added_domain_ids
|
||||||
|
],
|
||||||
|
ignore_conflicts=True, # Avoid duplicate entries
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_removed_domains(self, removed_domain_ids, member):
|
||||||
|
"""
|
||||||
|
Processes removed domains by deleting corresponding UserDomainRole instances.
|
||||||
|
"""
|
||||||
|
if removed_domain_ids:
|
||||||
|
# Delete UserDomainRole instances for removed domains
|
||||||
|
UserDomainRole.objects.filter(domain_id__in=removed_domain_ids, user=member).delete()
|
||||||
|
|
||||||
|
|
||||||
class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
|
class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
|
||||||
|
|
||||||
|
@ -346,6 +433,106 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
"""
|
||||||
|
Handles adding and removing domains for a portfolio invitee.
|
||||||
|
"""
|
||||||
|
added_domains = request.POST.get("added_domains")
|
||||||
|
removed_domains = request.POST.get("removed_domains")
|
||||||
|
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||||
|
email = portfolio_invitation.email
|
||||||
|
|
||||||
|
added_domain_ids = self._parse_domain_ids(added_domains, "added domains")
|
||||||
|
if added_domain_ids is None:
|
||||||
|
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||||
|
|
||||||
|
removed_domain_ids = self._parse_domain_ids(removed_domains, "removed domains")
|
||||||
|
if removed_domain_ids is None:
|
||||||
|
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||||
|
|
||||||
|
if added_domain_ids or removed_domain_ids:
|
||||||
|
try:
|
||||||
|
self._process_added_domains(added_domain_ids, email)
|
||||||
|
self._process_removed_domains(removed_domain_ids, email)
|
||||||
|
messages.success(request, "The domain assignment changes have been saved.")
|
||||||
|
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||||
|
except IntegrityError:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"A database error occurred while saving changes. If the issue persists, "
|
||||||
|
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||||
|
)
|
||||||
|
logger.error("A database error occurred while saving changes.")
|
||||||
|
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"An unexpected error occurred: {str(e)}. If the issue persists, "
|
||||||
|
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||||
|
)
|
||||||
|
logger.error(f"An unexpected error occurred: {str(e)}.")
|
||||||
|
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
|
||||||
|
else:
|
||||||
|
messages.info(request, "No changes detected.")
|
||||||
|
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||||
|
|
||||||
|
def _parse_domain_ids(self, domain_data, domain_type):
|
||||||
|
"""
|
||||||
|
Parses the domain IDs from the request and handles JSON errors.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return json.loads(domain_data) if domain_data else []
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
f"Invalid data for {domain_type}. If the issue persists, "
|
||||||
|
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||||
|
)
|
||||||
|
logger.error(f"Invalid data for {domain_type}.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _process_added_domains(self, added_domain_ids, email):
|
||||||
|
"""
|
||||||
|
Processes added domain invitations by updating existing invitations
|
||||||
|
or creating new ones.
|
||||||
|
"""
|
||||||
|
if not added_domain_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update existing invitations from CANCELED to INVITED
|
||||||
|
existing_invitations = DomainInvitation.objects.filter(domain_id__in=added_domain_ids, email=email)
|
||||||
|
existing_invitations.update(status=DomainInvitation.DomainInvitationStatus.INVITED)
|
||||||
|
|
||||||
|
# Determine which domains need new invitations
|
||||||
|
existing_domain_ids = existing_invitations.values_list("domain_id", flat=True)
|
||||||
|
new_domain_ids = set(added_domain_ids) - set(existing_domain_ids)
|
||||||
|
|
||||||
|
# Bulk create new invitations
|
||||||
|
DomainInvitation.objects.bulk_create(
|
||||||
|
[
|
||||||
|
DomainInvitation(
|
||||||
|
domain_id=domain_id,
|
||||||
|
email=email,
|
||||||
|
status=DomainInvitation.DomainInvitationStatus.INVITED,
|
||||||
|
)
|
||||||
|
for domain_id in new_domain_ids
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_removed_domains(self, removed_domain_ids, email):
|
||||||
|
"""
|
||||||
|
Processes removed domain invitations by updating their status to CANCELED.
|
||||||
|
"""
|
||||||
|
if not removed_domain_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update invitations from INVITED to CANCELED
|
||||||
|
DomainInvitation.objects.filter(
|
||||||
|
domain_id__in=removed_domain_ids,
|
||||||
|
email=email,
|
||||||
|
status=DomainInvitation.DomainInvitationStatus.INVITED,
|
||||||
|
).update(status=DomainInvitation.DomainInvitationStatus.CANCELED)
|
||||||
|
|
||||||
|
|
||||||
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
||||||
"""Some users have access to the underlying portfolio, but not any domains.
|
"""Some users have access to the underlying portfolio, but not any domains.
|
||||||
|
|
|
@ -203,7 +203,7 @@ class ExportDataTypeRequests(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = 'attachment; filename="domain-requests.csv"'
|
response["Content-Disposition"] = 'attachment; filename="domain-requests.csv"'
|
||||||
csv_export.DomainRequestsDataType.exporting_dr_data_to_csv(response, request=request)
|
csv_export.DomainRequestDataType.export_data_to_csv(response, request=request)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue