diff --git a/src/Pipfile b/src/Pipfile index fdf127d7c..07b1db715 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -django = "4.2.10" +django = "4.2.17" cfenv = "*" django-cors-headers = "*" pycryptodomex = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 33b858314..56daf5db5 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1789,11 +1789,12 @@ }, "waitress": { "hashes": [ - "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1", - "sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669" + "sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac", + "sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3" ], - "markers": "python_full_version >= '3.8.0'", - "version": "==3.0.0" + "index": "pypi", + "markers": "python_full_version >= '3.9.0'", + "version": "==3.0.1" }, "webob": { "hashes": [ diff --git a/src/package-lock.json b/src/package-lock.json index 22fb31857..d78b5132f 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -6921,16 +6921,6 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7307,39 +7297,6 @@ "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -8888,13 +8845,15 @@ } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-greatest-satisfied-range": { diff --git a/src/package.json b/src/package.json index e433d0126..7f7448bd2 100644 --- a/src/package.json +++ b/src/package.json @@ -22,5 +22,8 @@ "sass-loader": "^12.6.0", "webpack": "^5.96.1", "webpack-stream": "^7.0.0" + }, + "overrides": { + "semver": "^7.5.3" } } diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4465b7098..52e214bb9 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1830,10 +1830,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): form = DomainRequestAdminForm change_form_template = "django/admin/domain_request_change_form.html" + # ------ Filters ------ + # Define custom filters class StatusListFilter(MultipleChoiceListFilter): """Custom status filter which is a multiple choice filter""" - title = "Status" + title = "status" parameter_name = "status__in" 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 organization in the Domain Request object.""" - title = "federal Type" + title = "federal type" parameter_name = "converted_federal_types" def lookups(self, request, model_admin): @@ -1965,13 +1967,58 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if self.value() == "0": 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(' {}', url, text) + return format_html('{}', 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")) def converted_generic_org_type(self, obj): return obj.converted_generic_org_type_display @admin.display(description=_("Organization Name")) 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('{}', url, text) + else: + return obj.converted_organization_name @admin.display(description=_("Federal Agency")) def converted_federal_agency(self, obj): @@ -1989,34 +2036,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def converted_state_territory(self, obj): return obj.converted_state_territory - # Columns - 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 - + # ------ Portfolio fields ------ # Define methods to display fields from the related portfolio def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]: return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None @@ -2086,10 +2106,33 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def status_history(self, obj): 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 list_filter = ( + PortfolioFilter, StatusListFilter, GenericOrgFilter, FederalTypeFilter, @@ -2099,13 +2142,14 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Search + # NOTE: converted fields are included in the override for get_search_results search_fields = [ "requested_domain__name", "creator__email", "creator__first_name", "creator__last_name", ] - search_help_text = "Search by domain or creator." + search_help_text = "Search by domain, creator, or organization name." fieldsets = [ ( @@ -2271,9 +2315,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "cisa_representative_first_name", "cisa_representative_last_name", "cisa_representative_email", - "requested_suborganization", - "suborganization_city", - "suborganization_state_territory", ] autocomplete_fields = [ @@ -2692,6 +2733,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): qs = qs.filter(portfolio=portfolio_id) 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): """Custom transition domain admin class.""" @@ -3746,9 +3806,9 @@ class PortfolioAdmin(ListHeaderAdmin): "senior_official", ] - analyst_readonly_fields = [ - "organization_name", - ] + # Even though this is empty, I will leave it as a stub for easy changes in the future + # rather than strip it out of our logic. + analyst_readonly_fields = [] # type: ignore def get_admin_users(self, obj): # Filter UserPortfolioPermission objects related to the portfolio diff --git a/src/registrar/assets/src/js/getgov/domain-dnssec.js b/src/registrar/assets/src/js/getgov/domain-dnssec.js new file mode 100644 index 000000000..860359fe0 --- /dev/null +++ b/src/registrar/assets/src/js/getgov/domain-dnssec.js @@ -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"); + }); + } + } + }); +} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/domain-dsdata.js b/src/registrar/assets/src/js/getgov/domain-dsdata.js new file mode 100644 index 000000000..7c0871bec --- /dev/null +++ b/src/registrar/assets/src/js/getgov/domain-dsdata.js @@ -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"); + }); + } + } + }); +} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/domain-managers.js b/src/registrar/assets/src/js/getgov/domain-managers.js new file mode 100644 index 000000000..26eccd8cd --- /dev/null +++ b/src/registrar/assets/src/js/getgov/domain-managers.js @@ -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 + }); + }); + } + }); +} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/domain-request-form.js b/src/registrar/assets/src/js/getgov/domain-request-form.js new file mode 100644 index 000000000..d9b660a50 --- /dev/null +++ b/src/registrar/assets/src/js/getgov/domain-request-form.js @@ -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"); + }); + } + }); +} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/form-errors.js b/src/registrar/assets/src/js/getgov/form-errors.js new file mode 100644 index 000000000..ec1faaccf --- /dev/null +++ b/src/registrar/assets/src/js/getgov/form-errors.js @@ -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' }); + } + + }); +} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/helpers.js b/src/registrar/assets/src/js/getgov/helpers.js index 1afd84520..7d1449bac 100644 --- a/src/registrar/assets/src/js/getgov/helpers.js +++ b/src/registrar/assets/src/js/getgov/helpers.js @@ -1,9 +1,17 @@ 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) { - 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() { 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."); + } +} diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js index f5ebc83a3..6ff402aa4 100644 --- a/src/registrar/assets/src/js/getgov/main.js +++ b/src/registrar/assets/src/js/getgov/main.js @@ -11,6 +11,11 @@ import { initMembersTable } from './table-members.js'; import { initMemberDomainsTable } from './table-member-domains.js'; import { initEditMemberDomainsTable } from './table-edit-member-domains.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(); @@ -36,6 +41,13 @@ initMembersTable(); initMemberDomainsTable(); initEditMemberDomainsTable(); +initDomainRequestForm(); +initDomainManagersPage(); +initDomainDSData(); +initDomainDNSSEC(); + +initFormErrorHandling(); + // Init the portfolio new member page initPortfolioMemberPageRadio(); initPortfolioNewMemberPageToggle(); diff --git a/src/registrar/assets/src/js/getgov/table-base.js b/src/registrar/assets/src/js/getgov/table-base.js index e526c6b5f..e1d5c11ce 100644 --- a/src/registrar/assets/src/js/getgov/table-base.js +++ b/src/registrar/assets/src/js/getgov/table-base.js @@ -143,7 +143,7 @@ export class BaseTable { this.statusCheckboxes = document.querySelectorAll(`.${this.sectionSelector} input[name="filter-status"]`); this.statusIndicator = document.getElementById(`${this.sectionSelector}__filter-indicator`); 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.portfolioElement = document.getElementById('portfolio-js-value'); 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 - 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 const tbody = this.tableWrapper.querySelector('tbody'); tbody.innerHTML = ''; @@ -495,7 +495,8 @@ export class BaseTable { // Add event listeners to table headers for sorting initializeTableHeaders() { 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'); let order = 'asc'; // 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 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(); + } }); }); } diff --git a/src/registrar/assets/src/js/getgov/table-domains.js b/src/registrar/assets/src/js/getgov/table-domains.js index 20d9ef7de..a6373a5c2 100644 --- a/src/registrar/assets/src/js/getgov/table-domains.js +++ b/src/registrar/assets/src/js/getgov/table-domains.js @@ -31,6 +31,9 @@ export class DomainsTable extends BaseTable { ` } + 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 = ` ${domain.name} @@ -41,14 +44,14 @@ export class DomainsTable extends BaseTable { ${domain.state_display} - + ${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) + } + } + }); + }); + } +}); \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/table-edit-member-domains.js b/src/registrar/assets/src/js/getgov/table-edit-member-domains.js index 95492d46f..86aa39c37 100644 --- a/src/registrar/assets/src/js/getgov/table-edit-member-domains.js +++ b/src/registrar/assets/src/js/getgov/table-edit-member-domains.js @@ -1,5 +1,6 @@ import { BaseTable } from './table-base.js'; +import { hideElement, showElement } from './helpers.js'; /** * 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.addedDomains = []; // list of domains added to 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.initCancelEditDomainAssignmentButton(); + this.initEventListeners(); } getBaseUrl() { return document.getElementById("get_member_domains_json_url"); @@ -55,6 +62,14 @@ export class EditMemberDomainsTable extends BaseTable { getSearchParams(page, sortBy, order, searchTerm, status, portfolio) { let searchParams = super.getSearchParams(page, sortBy, order, searchTerm, status, portfolio); // 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 let checkedDomains = [...this.initialDomainAssignments]; // Add IDs from addedDomains that are not already in checkedDomains @@ -70,11 +85,7 @@ export class EditMemberDomainsTable extends BaseTable { checkedDomains.splice(index, 1); } }); - // Append updated checkedDomain IDs to searchParams - if (checkedDomains.length > 0) { - searchParams.append("checkedDomainIds", checkedDomains.join(",")); - } - return searchParams; + return checkedDomains } addRow(dataObject, tbody, customTableOptions) { const domain = dataObject; @@ -217,7 +228,123 @@ 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() { diff --git a/src/registrar/assets/src/js/getgov/table-member-domains.js b/src/registrar/assets/src/js/getgov/table-member-domains.js index 54e9d1212..7f89eee52 100644 --- a/src/registrar/assets/src/js/getgov/table-member-domains.js +++ b/src/registrar/assets/src/js/getgov/table-member-domains.js @@ -1,4 +1,5 @@ +import { showElement, hideElement } from './helpers.js'; import { BaseTable } from './table-base.js'; export class MemberDomainsTable extends BaseTable { @@ -24,7 +25,28 @@ export class MemberDomainsTable extends BaseTable { `; 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() { diff --git a/src/registrar/assets/src/sass/_theme/_base.scss b/src/registrar/assets/src/sass/_theme/_base.scss index 8d475270b..62f9f436e 100644 --- a/src/registrar/assets/src/sass/_theme/_base.scss +++ b/src/registrar/assets/src/sass/_theme/_base.scss @@ -149,6 +149,11 @@ footer { color: color('primary'); } +.usa-radio { + margin-top: 1rem; + font-size: 1.06rem; +} + abbr[title] { // workaround for underlining abbr element border-bottom: none; diff --git a/src/registrar/assets/src/sass/_theme/_register-form.scss b/src/registrar/assets/src/sass/_theme/_register-form.scss index 41d2980e3..fcc5b5ae6 100644 --- a/src/registrar/assets/src/sass/_theme/_register-form.scss +++ b/src/registrar/assets/src/sass/_theme/_register-form.scss @@ -12,7 +12,7 @@ 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 .register-form-step h3 { 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 { margin-bottom: 0; diff --git a/src/registrar/assets/src/sass/_theme/_tables.scss b/src/registrar/assets/src/sass/_theme/_tables.scss index 45f0b5245..ea160396e 100644 --- a/src/registrar/assets/src/sass/_theme/_tables.scss +++ b/src/registrar/assets/src/sass/_theme/_tables.scss @@ -88,8 +88,14 @@ th { } @include at-media(tablet-lg) { - th[data-sortable]:not([aria-sort]) .usa-table__header__button { + th[data-sortable] .usa-table__header__button { right: auto; + + &[aria-sort=ascending], + &[aria-sort=descending], + &:not([aria-sort]) { + right: auto; + } } } } diff --git a/src/registrar/assets/src/sass/_theme/_typography.scss b/src/registrar/assets/src/sass/_theme/_typography.scss index 466b6f975..db19a595b 100644 --- a/src/registrar/assets/src/sass/_theme/_typography.scss +++ b/src/registrar/assets/src/sass/_theme/_typography.scss @@ -23,6 +23,14 @@ h2 { 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 { font-size: size('body', 'sm'); font-weight: normal; @@ -34,6 +42,9 @@ h2 { .usa-form, .usa-form fieldset { font-size: 1rem; + .usa-legend { + font-size: 1rem; + } } .p--blockquote { diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 9f5d0162f..7230b04c6 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -69,9 +69,19 @@ def portfolio_permissions(request): "has_organization_requests_flag": False, "has_organization_members_flag": False, "is_portfolio_admin": False, + "has_domain_renewal_flag": False, } try: 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 view_suborg = request.user.has_view_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_members_flag": request.user.has_organization_members_flag(), "is_portfolio_admin": request.user.is_portfolio_admin(portfolio), + "has_domain_renewal_flag": request.user.has_domain_renewal_flag(), } return portfolio_context diff --git a/src/registrar/fixtures/fixtures_user_portfolio_permissions.py b/src/registrar/fixtures/fixtures_user_portfolio_permissions.py index 15265cfa8..5f9fd64ef 100644 --- a/src/registrar/fixtures/fixtures_user_portfolio_permissions.py +++ b/src/registrar/fixtures/fixtures_user_portfolio_permissions.py @@ -60,7 +60,10 @@ class UserPortfolioPermissionFixture: user=user, portfolio=portfolio, 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) else: diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index a8cdb5b9a..e40998231 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -151,6 +151,27 @@ class UserFixture: "email": "skey@truss.works", "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 = [ @@ -257,6 +278,18 @@ class UserFixture: "last_name": "Key-Analyst", "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. diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 572ef6399..289b3da0b 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -530,7 +530,7 @@ class PurposeForm(RegistrarForm): widget=forms.Textarea( attrs={ "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=[ @@ -736,7 +736,13 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm): required=True, # label has to end in a space to get the label_suffix to show 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=[ MaxLengthValidator( 1000, @@ -784,7 +790,12 @@ class AnythingElseForm(BaseDeletableRegistrarForm): anything_else = forms.CharField( required=True, 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=[ MaxLengthValidator( 2000, diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 19e96719f..6eb2fac07 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -2,7 +2,7 @@ from itertools import zip_longest import logging import ipaddress import re -from datetime import date +from datetime import date, timedelta from typing import Optional 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 .user_domain_role import UserDomainRole +from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) @@ -1152,14 +1153,29 @@ class Domain(TimeStampedModel, DomainHelper): now = timezone.now().date() 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.""" - if self.is_expired() and self.state != self.State.UNKNOWN: + if self.is_expired() and (self.state != self.State.UNKNOWN): 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: 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): """Maps the Epp contact representation to a PublicContact object. diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index bdfc6f804..2b5b56a78 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -14,6 +14,8 @@ from .domain import Domain from .domain_request import DomainRequest from registrar.utility.waffle import flag_is_active_for_user from waffle.decorators import flag_is_active +from django.utils import timezone +from datetime import timedelta 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() 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): """Return count of rejected requests""" return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count() @@ -259,6 +275,9 @@ class User(AbstractUser): def is_portfolio_admin(self, 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): permission = self.portfolio_permissions.first() if permission: diff --git a/src/registrar/templates/django/forms/label.html b/src/registrar/templates/django/forms/label.html index 545ccf781..3783c0fef 100644 --- a/src/registrar/templates/django/forms/label.html +++ b/src/registrar/templates/django/forms/label.html @@ -2,15 +2,25 @@ 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 span_for_text %} - {{ field.label }} + {% if legend_heading %} +

{{ legend_heading }}

+ {% if widget.attrs.id == 'id_additional_details-has_cisa_representative' %} +

.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has 10 regions that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.

+ {% endif %} {% else %} - {{ field.label }} + {% if span_for_text %} + {{ field.label }} + {% else %} + {{ field.label }} + {% endif %} {% endif %} {% if widget.attrs.required %} - - {% 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' %} + Select one. * + + {% 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 %} * {% endif %} diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index 1429127e6..b09f1f814 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -37,6 +37,9 @@ {% endif %} {% endblock breadcrumb %} + + {% include "includes/form_errors.html" with form=form %} +

Add a domain manager

{% if has_organization_feature_flag %}

diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index add7ca725..a5b8e52cb 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -35,18 +35,27 @@ Status: + {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} 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 %} DNS needed {% else %} - {{ domain.state|title }} + {{ domain.state|title }} {% endif %} {% if domain.get_state_help_text %}

- {{ domain.get_state_help_text }} + {% if has_domain_renewal_flag and domain.is_expiring and is_domain_manager %} + This domain will expire soon. Renew to maintain access. + {% 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 %}
{% endif %}

@@ -119,4 +128,4 @@ {% endif %} -{% endblock %} {# domain_content #} +{% endblock %} {# domain_content #} \ No newline at end of file diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index cfec053c2..a795fb2fc 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -27,7 +27,7 @@ {% endif %} {% endblock breadcrumb %} -

DNSSEC

+

DNSSEC

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.

@@ -78,7 +78,11 @@ aria-labelledby="Are you sure you want to continue?" 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" %} +
+ {% csrf_token %} + +
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 0f60235e1..5ebb264c4 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -42,7 +42,7 @@ {% include "includes/form_errors.html" with form=form %} {% endfor %} -

DS data

+

DS data

In order to enable DNSSEC, you must first configure it with your DNS hosting service.

@@ -141,7 +141,15 @@ aria-describedby="Your DNSSEC records will be deleted from the registry." 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" %} +
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html index 2a581bbd2..86fa79fa3 100644 --- a/src/registrar/templates/domain_request_additional_details.html +++ b/src/registrar/templates/domain_request_additional_details.html @@ -9,15 +9,9 @@ {% block form_fields %} -
- -

Are you working with a CISA regional representative on your domain request?

-

.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has 10 regions that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.

-
- +
- Select one. * - {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% 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?" %} {% input_with_errors forms.0.has_cisa_representative %} {% endwith %} {# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #} @@ -31,13 +25,8 @@
- -

Is there anything else you’d like us to know about your domain request?

-
- - Select one. * - {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% with add_class="usa-radio__input--tile" add_legend_heading="Is there anything else you’d like us to know about your domain request?" %} {% input_with_errors forms.2.has_anything_else_text %} {% endwith %} {# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #} @@ -45,7 +34,7 @@

Provide details below. *

- {% 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 %} {% endwith %} {# forms.3 is a form for inputting the e-mail of a cisa representative #} diff --git a/src/registrar/templates/domain_request_form.html b/src/registrar/templates/domain_request_form.html index a076220cb..2c2716a3c 100644 --- a/src/registrar/templates/domain_request_form.html +++ b/src/registrar/templates/domain_request_form.html @@ -130,9 +130,17 @@ aria-describedby="Are you sure you want to submit a domain request?" 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 %}
+
+ {% csrf_token %} +
+ {% block after_form_content %}{% endblock %} diff --git a/src/registrar/templates/domain_request_other_contacts.html b/src/registrar/templates/domain_request_other_contacts.html index 72e4abd8b..b3c1be8b4 100644 --- a/src/registrar/templates/domain_request_other_contacts.html +++ b/src/registrar/templates/domain_request_other_contacts.html @@ -9,7 +9,7 @@
  • We typically don’t reach out to these employees, but if contact is necessary, our practice is to coordinate with you first.
  • - + {% include "includes/required_fields.html" %} {% endblock %} {% block form_required_fields_help_text %} @@ -17,20 +17,14 @@ {% endblock %} {% block form_fields %} -
    - -

    Are there other employees who can help verify your request?

    -
    - - {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} +
    + {% with add_class="usa-radio__input--tile" add_legend_heading="Are there other employees who can help verify your request?" %} {% input_with_errors forms.0.has_other_contacts %} {% endwith %} {# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #} - -
    +
    - {% include "includes/required_fields.html" %} {{ forms.1.management_form }} {# forms.1 is a formset and this iterates over its forms #} {% for form in forms.1.forms %} diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index af292d9d5..f42e738e1 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -49,7 +49,7 @@ {% if domain_manager_roles %} -
    +

    Domain managers

    @@ -89,12 +89,13 @@ aria-describedby="You will be removed from this domain" data-force-action > - - {% 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 "|add:domain_name|add:"."|safe modal_button=modal_button_self|safe %} - {% endwith %} - + {% with domain_name=domain.name|force_escape counter_str=forloop.counter|stringformat:"s" %} + {% 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 "|add:domain_name|add:"."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove myself" modal_button_class="usa-button--secondary" %} + {% endwith %} + + {% csrf_token %} + {% else %}
    -
    - {% 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=""|add:email|add:" will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} - {% endwith %} - + {% with email=item.permission.user.email|default:item.permission.user|force_escape domain_name=domain.name|force_escape counter_str=forloop.counter|stringformat:"s" %} + {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description=""|add:email|add:" will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove domain manager" modal_button_class="usa-button--secondary" %} + {% endwith %}
    + + {% csrf_token %} + {% endif %} {% else %} CISA Regional Representative +

    CISA Regional Representative

      {% if DomainRequest.cisa_representative_first_name %} {{ DomainRequest.get_formatted_cisa_rep_name }} @@ -221,7 +221,7 @@ No {% endif %}
    -

    Anything else

    +

    Anything else

    Domain managers
    @@ -200,4 +249,4 @@
    - + \ No newline at end of file diff --git a/src/registrar/templates/includes/form_errors.html b/src/registrar/templates/includes/form_errors.html index dbbecae36..52c82aaf0 100644 --- a/src/registrar/templates/includes/form_errors.html +++ b/src/registrar/templates/includes/form_errors.html @@ -1,6 +1,7 @@ {% if form.errors %} +
    {% for error in form.non_field_errors %} -
    +
    {% endfor %} -{% endfor %} + {% endfor %} +
    {% endif %} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index 31900ac57..80768e2b8 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -71,7 +71,11 @@ error messages, if necessary. {% endif %} {# this is the input field, itself #} - {% include widget.template_name %} + {% with aria_label=aria_label %} + {% include widget.template_name %} + {% endwith %} + + {% if append_gov %} .gov diff --git a/src/registrar/templates/includes/member_domains_table.html b/src/registrar/templates/includes/member_domains_table.html index 600b9ba97..c85773b9a 100644 --- a/src/registrar/templates/includes/member_domains_table.html +++ b/src/registrar/templates/includes/member_domains_table.html @@ -34,7 +34,7 @@ {% endif %} -
    +
    Your registered domains