diff --git a/docs/developer/adding-feature-flags.md b/docs/developer/adding-feature-flags.md index dd97c7497..dc51b9e85 100644 --- a/docs/developer/adding-feature-flags.md +++ b/docs/developer/adding-feature-flags.md @@ -9,10 +9,10 @@ We use [django-waffle](https://waffle.readthedocs.io/en/stable/) for our feature 3. Click `Add waffle flag`. 4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them. -### Enabling the profile_feature flag +### Enabling a feature flag 1. On the app, navigate to `\admin`. 2. Under models, click `Waffle flags`. -3. Click the `profile_feature` record. This should exist by default, if not - create one with that name. +3. Click the featue flag record. This should exist by default, if not - create one with that name. 4. (Important) Set the field `Everyone` to `Unknown`. This field overrides all other settings when set to anything else. 5. Configure the settings as you see fit. diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index b85ea6335..35b2e3971 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -126,7 +126,15 @@ class AvailableAPITest(MockEppLib): def setUp(self): super().setUp() - self.user = get_user_model().objects.create(username="username") + username = "test_user" + first_name = "First" + last_name = "Last" + email = "info@example.com" + title = "title" + phone = "8080102431" + self.user = get_user_model().objects.create( + username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone + ) def test_available_get(self): self.client.force_login(self.user) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ba42ac7e5..15f1ccb79 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1976,11 +1976,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): so we should display that information using this function. """ - - if hasattr(obj, "creator"): - recipient = obj.creator - else: - recipient = None + recipient = obj.creator # Displays a warning in admin when an email cannot be sent if recipient and recipient.email: @@ -2183,7 +2179,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Initialize extra_context and add filtered entries extra_context = extra_context or {} extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries - extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") # Denote if an action needed email was sent or not email_sent = request.session.get("action_needed_email_sent", False) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index cd42fd322..027ef4344 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -33,6 +33,14 @@ const showElement = (element) => { element.classList.remove('display-none'); }; +/** + * Helper function to get the CSRF token from the cookie + * +*/ +function getCsrfToken() { + return document.querySelector('input[name="csrfmiddlewaretoken"]').value; +} + /** * Helper function that scrolls to an element * @param {string} attributeName - The string "class" or "id" @@ -994,59 +1002,169 @@ function unloadModals() { window.modal.off(); } -/** +class LoadTableBase { + constructor(tableSelector, tableWrapperSelector, searchFieldId, searchSubmitId, resetSearchBtn, resetFiltersBtn, noDataDisplay, noSearchresultsDisplay) { + this.tableWrapper = document.querySelector(tableWrapperSelector); + this.tableHeaders = document.querySelectorAll(`${tableSelector} th[data-sortable]`); + this.currentSortBy = 'id'; + this.currentOrder = 'asc'; + this.currentStatus = []; + this.currentSearchTerm = ''; + this.scrollToTable = false; + this.searchInput = document.querySelector(searchFieldId); + this.searchSubmit = document.querySelector(searchSubmitId); + this.tableAnnouncementRegion = document.querySelector(`${tableWrapperSelector} .usa-table__announcement-region`); + this.resetSearchButton = document.querySelector(resetSearchBtn); + this.resetFiltersButton = document.querySelector(resetFiltersBtn); + // NOTE: these 3 can't be used if filters are active on a page with more than 1 table + this.statusCheckboxes = document.querySelectorAll('input[name="filter-status"]'); + this.statusIndicator = document.querySelector('.filter-indicator'); + this.statusToggle = document.querySelector('.usa-button--filter'); + this.noTableWrapper = document.querySelector(noDataDisplay); + this.noSearchResultsWrapper = document.querySelector(noSearchresultsDisplay); + this.portfolioElement = document.getElementById('portfolio-js-value'); + this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null; + this.initializeTableHeaders(); + this.initializeSearchHandler(); + this.initializeStatusToggleHandler(); + this.initializeFilterCheckboxes(); + this.initializeResetSearchButton(); + this.initializeResetFiltersButton(); + this.initializeAccordionAccessibilityListeners(); + } + + /** * Generalized function to update pagination for a list. * @param {string} itemName - The name displayed in the counter * @param {string} paginationSelector - CSS selector for the pagination container. * @param {string} counterSelector - CSS selector for the pagination counter. - * @param {string} linkAnchor - CSS selector for the header element to anchor the links to. - * @param {Function} loadPageFunction - Function to call when a page link is clicked. + * @param {string} tableSelector - CSS selector for the header element to anchor the links to. * @param {number} currentPage - The current page number (starting with 1). * @param {number} numPages - The total number of pages. * @param {boolean} hasPrevious - Whether there is a page before the current page. * @param {boolean} hasNext - Whether there is a page after the current page. - * @param {number} totalItems - The total number of items. - * @param {string} searchTerm - The search term - */ -function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) { - const paginationContainer = document.querySelector(paginationSelector); - const paginationCounter = document.querySelector(counterSelector); - const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); - paginationCounter.innerHTML = ''; - paginationButtons.innerHTML = ''; + * @param {number} total - The total number of items. + */ + updatePagination( + itemName, + paginationSelector, + counterSelector, + parentTableSelector, + currentPage, + numPages, + hasPrevious, + hasNext, + totalItems, + ) { + const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); + const counterSelectorEl = document.querySelector(counterSelector); + const paginationSelectorEl = document.querySelector(paginationSelector); + counterSelectorEl.innerHTML = ''; + paginationButtons.innerHTML = ''; - // Buttons should only be displayed if there are more than one pages of results - paginationButtons.classList.toggle('display-none', numPages <= 1); + // Buttons should only be displayed if there are more than one pages of results + paginationButtons.classList.toggle('display-none', numPages <= 1); - // Counter should only be displayed if there is more than 1 item - paginationContainer.classList.toggle('display-none', totalItems < 1); + // Counter should only be displayed if there is more than 1 item + paginationSelectorEl.classList.toggle('display-none', totalItems < 1); - paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${searchTerm ? ' for ' + '"' + searchTerm + '"' : ''}`; + counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`; - if (hasPrevious) { - const prevPageItem = document.createElement('li'); - prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - prevPageItem.innerHTML = ` - - - Previous - - `; - prevPageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - loadPageFunction(currentPage - 1); - }); - paginationButtons.appendChild(prevPageItem); + if (hasPrevious) { + const prevPageItem = document.createElement('li'); + prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; + prevPageItem.innerHTML = ` + + + Previous + + `; + prevPageItem.querySelector('a').addEventListener('click', (event) => { + event.preventDefault(); + this.loadTable(currentPage - 1); + }); + paginationButtons.appendChild(prevPageItem); + } + + // Add first page and ellipsis if necessary + if (currentPage > 2) { + paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage)); + if (currentPage > 3) { + const ellipsis = document.createElement('li'); + ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; + ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); + ellipsis.innerHTML = ''; + paginationButtons.appendChild(ellipsis); + } + } + + // Add pages around the current page + for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) { + paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage)); + } + + // Add last page and ellipsis if necessary + if (currentPage < numPages - 1) { + if (currentPage < numPages - 2) { + const ellipsis = document.createElement('li'); + ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; + ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); + ellipsis.innerHTML = ''; + paginationButtons.appendChild(ellipsis); + } + paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage)); + } + + if (hasNext) { + const nextPageItem = document.createElement('li'); + nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; + nextPageItem.innerHTML = ` + + Next + + + `; + nextPageItem.querySelector('a').addEventListener('click', (event) => { + event.preventDefault(); + this.loadTable(currentPage + 1); + }); + paginationButtons.appendChild(nextPageItem); + } } + /** + * A helper that toggles content/ no content/ no search results + * + */ + updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => { + const { unfiltered_total, total } = data; + if (unfiltered_total) { + if (total) { + showElement(dataWrapper); + hideElement(noSearchResultsWrapper); + hideElement(noDataWrapper); + } else { + hideElement(dataWrapper); + showElement(noSearchResultsWrapper); + hideElement(noDataWrapper); + } + } else { + hideElement(dataWrapper); + hideElement(noSearchResultsWrapper); + showElement(noDataWrapper); + } + }; + // Helper function to create a page item - function createPageItem(page) { + createPageItem(page, parentTableSelector, currentPage) { const pageItem = document.createElement('li'); pageItem.className = 'usa-pagination__item usa-pagination__page-no'; pageItem.innerHTML = ` - ${page} + ${page} `; if (page === currentPage) { pageItem.querySelector('a').classList.add('usa-current'); @@ -1054,134 +1172,212 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc } pageItem.querySelector('a').addEventListener('click', (event) => { event.preventDefault(); - loadPageFunction(page); + this.loadTable(page); }); return pageItem; } - // Add first page and ellipsis if necessary - if (currentPage > 2) { - paginationButtons.appendChild(createPageItem(1)); - if (currentPage > 3) { - const ellipsis = document.createElement('li'); - ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; - ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); - ellipsis.innerHTML = ''; - paginationButtons.appendChild(ellipsis); - } + /** + * A helper that resets sortable table headers + * + */ + unsetHeader = (header) => { + header.removeAttribute('aria-sort'); + let headerName = header.innerText; + const headerLabel = `${headerName}, sortable column, currently unsorted"`; + const headerButtonLabel = `Click to sort by ascending order.`; + header.setAttribute("aria-label", headerLabel); + header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel); + }; + + // Abstract method (to be implemented in the child class) + loadTable(page, sortBy, order) { + throw new Error('loadData() must be implemented in a subclass'); } - // Add pages around the current page - for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) { - paginationButtons.appendChild(createPageItem(i)); - } - - // Add last page and ellipsis if necessary - if (currentPage < numPages - 1) { - if (currentPage < numPages - 2) { - const ellipsis = document.createElement('li'); - ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; - ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); - ellipsis.innerHTML = ''; - paginationButtons.appendChild(ellipsis); - } - paginationButtons.appendChild(createPageItem(numPages)); - } - - if (hasNext) { - const nextPageItem = document.createElement('li'); - nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - nextPageItem.innerHTML = ` - - Next - - - `; - nextPageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - loadPageFunction(currentPage + 1); + // Add event listeners to table headers for sorting + initializeTableHeaders() { + this.tableHeaders.forEach(header => { + header.addEventListener('click', () => { + const sortBy = header.getAttribute('data-sortable'); + let order = 'asc'; + // sort order will be ascending, unless the currently sorted column is ascending, and the user + // is selecting the same column to sort in descending order + if (sortBy === this.currentSortBy) { + order = this.currentOrder === 'asc' ? 'desc' : 'asc'; + } + // load the results with the updated sort + this.loadTable(1, sortBy, order); + }); + }); + } + + initializeSearchHandler() { + this.searchSubmit.addEventListener('click', (e) => { + e.preventDefault(); + this.currentSearchTerm = this.searchInput.value; + // If the search is blank, we match the resetSearch functionality + if (this.currentSearchTerm) { + showElement(this.resetSearchButton); + } else { + hideElement(this.resetSearchButton); + } + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + }); + } + + initializeStatusToggleHandler() { + if (this.statusToggle) { + this.statusToggle.addEventListener('click', () => { + toggleCaret(this.statusToggle); + }); + } + } + + // Add event listeners to status filter checkboxes + initializeFilterCheckboxes() { + this.statusCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', () => { + const checkboxValue = checkbox.value; + + // Update currentStatus array based on checkbox state + if (checkbox.checked) { + this.currentStatus.push(checkboxValue); + } else { + const index = this.currentStatus.indexOf(checkboxValue); + if (index > -1) { + this.currentStatus.splice(index, 1); + } + } + + // Manage visibility of reset filters button + if (this.currentStatus.length == 0) { + hideElement(this.resetFiltersButton); + } else { + showElement(this.resetFiltersButton); + } + + // Disable the auto scroll + this.scrollToTable = false; + + // Call loadTable with updated status + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + this.updateStatusIndicator(); + }); + }); + } + + // Reset UI and accessibility + resetHeaders() { + this.tableHeaders.forEach(header => { + // Unset sort UI in headers + this.unsetHeader(header); + }); + // Reset the announcement region + this.tableAnnouncementRegion.innerHTML = ''; + } + + resetSearch() { + this.searchInput.value = ''; + this.currentSearchTerm = ''; + hideElement(this.resetSearchButton); + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + } + + initializeResetSearchButton() { + if (this.resetSearchButton) { + this.resetSearchButton.addEventListener('click', () => { + this.resetSearch(); + }); + } + } + + resetFilters() { + this.currentStatus = []; + this.statusCheckboxes.forEach(checkbox => { + checkbox.checked = false; + }); + hideElement(this.resetFiltersButton); + + // Disable the auto scroll + this.scrollToTable = false; + + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + this.updateStatusIndicator(); + // No need to toggle close the filters. The focus shift will trigger that for us. + } + + initializeResetFiltersButton() { + if (this.resetFiltersButton) { + this.resetFiltersButton.addEventListener('click', () => { + this.resetFilters(); + }); + } + } + + updateStatusIndicator() { + this.statusIndicator.innerHTML = ''; + // Even if the element is empty, it'll mess up the flex layout unless we set display none + hideElement(this.statusIndicator); + if (this.currentStatus.length) + this.statusIndicator.innerHTML = '(' + this.currentStatus.length + ')'; + showElement(this.statusIndicator); + } + + closeFilters() { + if (this.statusToggle.getAttribute("aria-expanded") === "true") { + this.statusToggle.click(); + } + } + + initializeAccordionAccessibilityListeners() { + // Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter, + // user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button. + // NOTE: We may need to evolve this as we add more filters. + document.addEventListener('focusin', (event) => { + const accordion = document.querySelector('.usa-accordion--select'); + const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + + if (accordionThatIsOpen && !accordion.contains(event.target)) { + this.closeFilters(); + } + }); + + // Close when user clicks outside + // NOTE: We may need to evolve this as we add more filters. + document.addEventListener('click', (event) => { + const accordion = document.querySelector('.usa-accordion--select'); + const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + + if (accordionThatIsOpen && !accordion.contains(event.target)) { + this.closeFilters(); + } }); - paginationButtons.appendChild(nextPageItem); } } -/** - * A helper that toggles content/ no content/ no search results - * -*/ -const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => { - const { unfiltered_total, total } = data; - if (unfiltered_total) { - if (total) { - showElement(dataWrapper); - hideElement(noSearchResultsWrapper); - hideElement(noDataWrapper); - } else { - hideElement(dataWrapper); - showElement(noSearchResultsWrapper); - hideElement(noDataWrapper); - } - } else { - hideElement(dataWrapper); - hideElement(noSearchResultsWrapper); - showElement(noDataWrapper); +class DomainsTable extends LoadTableBase { + + constructor() { + super('.domains__table', '.domains__table-wrapper', '#domains__search-field', '#domains__search-field-submit', '.domains__reset-search', '.domains__reset-filters', '.domains__no-data', '.domains__no-search-results'); } -}; - -/** - * A helper that resets sortable table headers - * -*/ -const unsetHeader = (header) => { - header.removeAttribute('aria-sort'); - let headerName = header.innerText; - const headerLabel = `${headerName}, sortable column, currently unsorted"`; - const headerButtonLabel = `Click to sort by ascending order.`; - header.setAttribute("aria-label", headerLabel); - header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel); -}; - -/** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domains list and associated functionality on the home page of the app. - * - */ -document.addEventListener('DOMContentLoaded', function() { - const domainsWrapper = document.querySelector('.domains__table-wrapper'); - - if (domainsWrapper) { - let currentSortBy = 'id'; - let currentOrder = 'asc'; - const noDomainsWrapper = document.querySelector('.domains__no-data'); - const noSearchResultsWrapper = document.querySelector('.domains__no-search-results'); - let scrollToTable = false; - let currentStatus = []; - let currentSearchTerm = ''; - const domainsSearchInput = document.getElementById('domains__search-field'); - const domainsSearchSubmit = document.getElementById('domains__search-field-submit'); - const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]'); - const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region'); - const resetSearchButton = document.querySelector('.domains__reset-search'); - const resetFiltersButton = document.querySelector('.domains__reset-filters'); - const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]'); - const statusIndicator = document.querySelector('.domain__filter-indicator'); - const statusToggle = document.querySelector('.usa-button--filter'); - const portfolioElement = document.getElementById('portfolio-js-value'); - const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null; - - /** + /** * Loads rows in the domains list, as well as updates pagination around the domains list * based on the supplied attributes. * @param {*} page - the page number of the results (starts with 1) * @param {*} sortBy - the sort column option * @param {*} order - the sort order {asc, desc} * @param {*} scroll - control for the scrollToElement functionality + * @param {*} status - control for the status filter * @param {*} searchTerm - the search term * @param {*} portfolio - the portfolio id */ - function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) { + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + // fetch json of page of domais, given params let baseUrl = document.getElementById("get_domains_json_url"); if (!baseUrl) { @@ -1194,10 +1390,19 @@ document.addEventListener('DOMContentLoaded', function() { } // fetch json of page of domains, given params - let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}` + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "status": status, + "search_term": searchTerm + } + ); if (portfolio) - url += `&portfolio=${portfolio}` + searchParams.append("portfolio", portfolio) + let url = `${baseUrlValue}?${searchParams.toString()}` fetch(url) .then(response => response.json()) .then(data => { @@ -1207,7 +1412,7 @@ document.addEventListener('DOMContentLoaded', function() { } // handle the display of proper messaging in the event that no domains exist in the list or search returns no results - updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, currentSearchTerm); + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); // identify the DOM element where the domain list will be inserted into the DOM const domainList = document.querySelector('.domains__table tbody'); @@ -1225,7 +1430,7 @@ document.addEventListener('DOMContentLoaded', function() { let markupForSuborganizationRow = ''; - if (portfolioValue) { + if (this.portfolioValue) { markupForSuborganizationRow = ` ${suborganization} @@ -1271,181 +1476,438 @@ document.addEventListener('DOMContentLoaded', function() { // Do not scroll on first page load if (scroll) ScrollToElement('class', 'domains'); - scrollToTable = true; + this.scrollToTable = true; // update pagination - updatePagination( + this.updatePagination( 'domain', '#domains-pagination', '#domains-pagination .usa-pagination__counter', '#domains', - loadDomains, data.page, data.num_pages, data.has_previous, data.has_next, data.total, - currentSearchTerm ); - currentSortBy = sortBy; - currentOrder = order; - currentSearchTerm = searchTerm; + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; }) .catch(error => console.error('Error fetching domains:', error)); + } +} + + +class DomainRequestsTable extends LoadTableBase { + + constructor() { + super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results'); + } + /** + * Loads rows in the domains list, as well as updates pagination around the domains list + * based on the supplied attributes. + * @param {*} page - the page number of the results (starts with 1) + * @param {*} sortBy - the sort column option + * @param {*} order - the sort order {asc, desc} + * @param {*} scroll - control for the scrollToElement functionality + * @param {*} status - control for the status filter + * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id + */ + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) { + let baseUrl = document.getElementById("get_domain_requests_json_url"); + if (!baseUrl) { + return; } - // Add event listeners to table headers for sorting - tableHeaders.forEach(header => { - header.addEventListener('click', function() { - const sortBy = this.getAttribute('data-sortable'); - let order = 'asc'; - // sort order will be ascending, unless the currently sorted column is ascending, and the user - // is selecting the same column to sort in descending order - if (sortBy === currentSortBy) { - order = currentOrder === 'asc' ? 'desc' : 'asc'; - } - // load the results with the updated sort - loadDomains(1, sortBy, order); - }); - }); + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } - domainsSearchSubmit.addEventListener('click', function(e) { - e.preventDefault(); - currentSearchTerm = domainsSearchInput.value; - // If the search is blank, we match the resetSearch functionality - if (currentSearchTerm) { - showElement(resetSearchButton); - } else { - hideElement(resetSearchButton); + // add searchParams + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "status": status, + "search_term": searchTerm } - loadDomains(1, 'id', 'asc'); - resetHeaders(); - }); + ); + if (portfolio) + searchParams.append("portfolio", portfolio) - if (statusToggle) { - statusToggle.addEventListener('click', function() { - toggleCaret(statusToggle); - }); - } + let url = `${baseUrlValue}?${searchParams.toString()}` + fetch(url) + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error in AJAX call: ' + data.error); + return; + } - // Add event listeners to status filter checkboxes - statusCheckboxes.forEach(checkbox => { - checkbox.addEventListener('change', function() { - const checkboxValue = this.value; - - // Update currentStatus array based on checkbox state - if (this.checked) { - currentStatus.push(checkboxValue); + // handle the display of proper messaging in the event that no requests exist in the list or search returns no results + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); + + // identify the DOM element where the domain request list will be inserted into the DOM + const tbody = document.querySelector('.domain-requests__table tbody'); + tbody.innerHTML = ''; + + // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases + // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, + // which will cause bad delete requests to be sent. + const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); + preExistingModalPlaceholders.forEach(element => { + element.remove(); + }); + + // remove any existing modal elements from the DOM so they can be properly re-initialized + // after the DOM content changes and there are new delete modal buttons added + unloadModals(); + + let needsDeleteColumn = false; + + needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); + + // Remove existing delete th and td if they exist + let existingDeleteTh = document.querySelector('.delete-header'); + if (!needsDeleteColumn) { + if (existingDeleteTh) + existingDeleteTh.remove(); } else { - const index = currentStatus.indexOf(checkboxValue); - if (index > -1) { - currentStatus.splice(index, 1); + if (!existingDeleteTh) { + const delheader = document.createElement('th'); + delheader.setAttribute('scope', 'col'); + delheader.setAttribute('role', 'columnheader'); + delheader.setAttribute('class', 'delete-header'); + delheader.innerHTML = ` + Delete Action`; + let tableHeaderRow = document.querySelector('.domain-requests__table thead tr'); + tableHeaderRow.appendChild(delheader); } } - // Manage visibility of reset filters button - if (currentStatus.length == 0) { - hideElement(resetFiltersButton); - } else { - showElement(resetFiltersButton); - } + data.domain_requests.forEach(request => { + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + const domainName = request.requested_domain ? request.requested_domain : `New domain request
(${utcDateString(request.created_at)})`; + const actionUrl = request.action_url; + const actionLabel = request.action_label; + const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; + + // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) + // Even if the request is not deletable, we may need these empty strings for the td if the deletable column is displayed + let modalTrigger = ''; - // Disable the auto scroll - scrollToTable = false; + let markupCreatorRow = ''; - // Call loadDomains with updated status - loadDomains(1, 'id', 'asc'); - resetHeaders(); - updateStatusIndicator(); - }); - }); + if (this.portfolioValue) { + markupCreatorRow = ` + + ${request.creator ? request.creator : ''} + + ` + } - // Reset UI and accessibility - function resetHeaders() { - tableHeaders.forEach(header => { - // Unset sort UI in headers - unsetHeader(header); - }); - // Reset the announcement region - tableAnnouncementRegion.innerHTML = ''; - } + // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages + if (request.is_deletable) { + let modalHeading = ''; + let modalDescription = ''; - function resetSearch() { - domainsSearchInput.value = ''; - currentSearchTerm = ''; - hideElement(resetSearchButton); - loadDomains(1, 'id', 'asc'); - resetHeaders(); - } + if (request.requested_domain) { + modalHeading = `Are you sure you want to delete ${request.requested_domain}?`; + modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; + } else { + if (request.created_at) { + modalHeading = 'Are you sure you want to delete this domain request?'; + modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`; + } else { + modalHeading = 'Are you sure you want to delete New domain request?'; + modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; + } + } - if (resetSearchButton) { - resetSearchButton.addEventListener('click', function() { - resetSearch(); - }); - } + modalTrigger = ` + + Delete ${domainName} + ` - function resetFilters() { - currentStatus = []; - statusCheckboxes.forEach(checkbox => { - checkbox.checked = false; - }); - hideElement(resetFiltersButton); + const modalSubmit = ` + + ` - // Disable the auto scroll - scrollToTable = false; + const modal = document.createElement('div'); + modal.setAttribute('class', 'usa-modal'); + modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`); + modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); + modal.setAttribute('aria-describedby', 'Domain will be removed'); + modal.setAttribute('data-force-action', ''); - loadDomains(1, 'id', 'asc'); - resetHeaders(); - updateStatusIndicator(); - // No need to toggle close the filters. The focus shift will trigger that for us. - } + modal.innerHTML = ` +
+
+ +
+ +
+ +
+ +
+ ` - if (resetFiltersButton) { - resetFiltersButton.addEventListener('click', function() { - resetFilters(); - }); - } + this.tableWrapper.appendChild(modal); - function updateStatusIndicator() { - statusIndicator.innerHTML = ''; - // Even if the element is empty, it'll mess up the flex layout unless we set display none - statusIndicator.hideElement(); - if (currentStatus.length) - statusIndicator.innerHTML = '(' + currentStatus.length + ')'; - statusIndicator.showElement(); - } + // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly + if (this.portfolioValue) { + modalTrigger = ` + + Delete ${domainName} + - function closeFilters() { - if (statusToggle.getAttribute("aria-expanded") === "true") { - statusToggle.click(); - } - } +
+
+ +
+ +
+ ` + } + } - // Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter, - // user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button. - // NOTE: We may need to evolve this as we add more filters. - document.addEventListener('focusin', function(event) { - const accordion = document.querySelector('.usa-accordion--select'); - const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); - - if (accordionThatIsOpen && !accordion.contains(event.target)) { - closeFilters(); - } - }); - // Close when user clicks outside - // NOTE: We may need to evolve this as we add more filters. - document.addEventListener('click', function(event) { - const accordion = document.querySelector('.usa-accordion--select'); - const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + const row = document.createElement('tr'); + row.innerHTML = ` + + ${domainName} + + + ${submissionDate} + + ${markupCreatorRow} + + ${request.status} + + + + + ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} + + + ${needsDeleteColumn ? ''+modalTrigger+'' : ''} + `; + tbody.appendChild(row); + }); + + // initialize modals immediately after the DOM content is updated + initializeModals(); + + // Now the DOM and modals are ready, add listeners to the submit buttons + const modals = document.querySelectorAll('.usa-modal__content'); + + modals.forEach(modal => { + const submitButton = modal.querySelector('.usa-modal__submit'); + const closeButton = modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + let pk = submitButton.getAttribute('data-pk'); + // Close the modal to remove the USWDS UI local classes + closeButton.click(); + // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page + let pageToDisplay = data.page; + if (data.total == 1 && data.unfiltered_total > 1) { + pageToDisplay--; + } + this.deleteDomainRequest(pk, pageToDisplay); + }); + }); + + // Do not scroll on first page load + if (scroll) + ScrollToElement('class', 'domain-requests'); + this.scrollToTable = true; + + // update the pagination after the domain requests list is updated + this.updatePagination( + 'domain request', + '#domain-requests-pagination', + '#domain-requests-pagination .usa-pagination__counter', + '#domain-requests', + data.page, + data.num_pages, + data.has_previous, + data.has_next, + data.total, + ); + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; + }) + .catch(error => console.error('Error fetching domain requests:', error)); + } + + /** + * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. + * @param {*} domainRequestPk - the identifier for the request that we're deleting + * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page + */ + deleteDomainRequest(domainRequestPk, pageToDisplay) { + // Use to debug uswds modal issues + //console.log('deleteDomainRequest') - if (accordionThatIsOpen && !accordion.contains(event.target)) { - closeFilters(); + // Get csrf token + const csrfToken = getCsrfToken(); + // Create FormData object and append the CSRF token + const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; + + fetch(`/domain-request/${domainRequestPk}/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRFToken': csrfToken, + }, + body: formData + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + // Update data and UI + this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + }) + .catch(error => console.error('Error fetching domain requests:', error)); + } +} + + +/** + * An IIFE that listens for DOM Content to be loaded, then executes. This function + * initializes the domains list and associated functionality on the home page of the app. + * + */ +document.addEventListener('DOMContentLoaded', function() { + const isDomainsPage = document.querySelector("#domains") + if (isDomainsPage){ + const domainsTable = new DomainsTable(); + if (domainsTable.tableWrapper) { + // Initial load + domainsTable.loadTable(1); + } + } +}); + +/** + * An IIFE that listens for DOM Content to be loaded, then executes. This function + * initializes the domain requests list and associated functionality on the home page of the app. + * + */ +document.addEventListener('DOMContentLoaded', function() { + const domainRequestsSectionWrapper = document.querySelector('.domain-requests'); + if (domainRequestsSectionWrapper) { + const domainRequestsTable = new DomainRequestsTable(); + if (domainRequestsTable.tableWrapper) { + domainRequestsTable.loadTable(1); + } + } + + document.addEventListener('focusin', function(event) { + closeOpenAccordions(event); + }); + + document.addEventListener('click', function(event) { + closeOpenAccordions(event); + }); + + function closeMoreActionMenu(accordionThatIsOpen) { + if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") { + accordionThatIsOpen.click(); + } + } + + function closeOpenAccordions(event) { + const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]'); + openAccordions.forEach((openAccordionButton) => { + // Find the corresponding accordion + const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); + if (accordion && !accordion.contains(event.target)) { + // Close the accordion if the click is outside + closeMoreActionMenu(openAccordionButton); } }); - - // Initial load - loadDomains(1); } }); @@ -1463,458 +1925,6 @@ const utcDateString = (dateString) => { return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`; }; -/** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domain requests list and associated functionality on the home page of the app. - * - */ -document.addEventListener('DOMContentLoaded', function() { - const domainRequestsSectionWrapper = document.querySelector('.domain-requests'); - const domainRequestsWrapper = document.querySelector('.domain-requests__table-wrapper'); - - if (domainRequestsWrapper) { - let currentSortBy = 'id'; - let currentOrder = 'asc'; - const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data'); - const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results'); - let scrollToTable = false; - let currentSearchTerm = ''; - const domainRequestsSearchInput = document.getElementById('domain-requests__search-field'); - const domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit'); - const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]'); - const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region'); - const resetSearchButton = document.querySelector('.domain-requests__reset-search'); - const portfolioElement = document.getElementById('portfolio-js-value'); - const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null; - - /** - * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. - * @param {*} domainRequestPk - the identifier for the request that we're deleting - * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page - */ - function deleteDomainRequest(domainRequestPk,pageToDisplay) { - - // Use to debug uswds modal issues - //console.log('deleteDomainRequest') - - // Get csrf token - const csrfToken = getCsrfToken(); - // Create FormData object and append the CSRF token - const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; - - fetch(`/domain-request/${domainRequestPk}/delete`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - }, - body: formData - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // Update data and UI - loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, scrollToTable, currentSearchTerm); - }) - .catch(error => console.error('Error fetching domain requests:', error)); - } - - // Helper function to get the CSRF token from the cookie - function getCsrfToken() { - return document.querySelector('input[name="csrfmiddlewaretoken"]').value; - } - - /** - * Loads rows in the domain requests list, as well as updates pagination around the domain requests list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} searchTerm - the search term - */ - function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) { - // fetch json of page of domain requests, given params - let baseUrl = document.getElementById("get_domain_requests_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - // fetch json of page of requests, given params - let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}` - if (portfolio) - url += `&portfolio=${portfolio}` - - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no requests exist in the list or search returns no results - updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, currentSearchTerm); - - // identify the DOM element where the domain request list will be inserted into the DOM - const tbody = document.querySelector('.domain-requests__table tbody'); - tbody.innerHTML = ''; - - // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases - // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, - // which will cause bad delete requests to be sent. - const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); - preExistingModalPlaceholders.forEach(element => { - element.remove(); - }); - - // remove any existing modal elements from the DOM so they can be properly re-initialized - // after the DOM content changes and there are new delete modal buttons added - unloadModals(); - - let needsDeleteColumn = false; - - needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); - - // Remove existing delete th and td if they exist - let existingDeleteTh = document.querySelector('.delete-header'); - if (!needsDeleteColumn) { - if (existingDeleteTh) - existingDeleteTh.remove(); - } else { - if (!existingDeleteTh) { - const delheader = document.createElement('th'); - delheader.setAttribute('scope', 'col'); - delheader.setAttribute('role', 'columnheader'); - delheader.setAttribute('class', 'delete-header'); - delheader.innerHTML = ` - Delete Action`; - let tableHeaderRow = document.querySelector('.domain-requests__table thead tr'); - tableHeaderRow.appendChild(delheader); - } - } - - data.domain_requests.forEach(request => { - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - const domainName = request.requested_domain ? request.requested_domain : `New domain request
(${utcDateString(request.created_at)})`; - const actionUrl = request.action_url; - const actionLabel = request.action_label; - const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; - - // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) - // Even if the request is not deletable, we may need these empty strings for the td if the deletable column is displayed - let modalTrigger = ''; - - let markupCreatorRow = ''; - - if (portfolioValue) { - markupCreatorRow = ` - - ${request.creator ? request.creator : ''} - - ` - } - - // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages - if (request.is_deletable) { - let modalHeading = ''; - let modalDescription = ''; - - if (request.requested_domain) { - modalHeading = `Are you sure you want to delete ${request.requested_domain}?`; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } else { - if (request.created_at) { - modalHeading = 'Are you sure you want to delete this domain request?'; - modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`; - } else { - modalHeading = 'Are you sure you want to delete New domain request?'; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } - } - - modalTrigger = ` - - Delete ${domainName} - ` - - const modalSubmit = ` - - ` - - const modal = document.createElement('div'); - modal.setAttribute('class', 'usa-modal'); - modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`); - modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); - modal.setAttribute('aria-describedby', 'Domain will be removed'); - modal.setAttribute('data-force-action', ''); - - modal.innerHTML = ` -
-
- -
- -
- -
- -
- ` - - domainRequestsSectionWrapper.appendChild(modal); - - // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly - if (portfolioValue) { - modalTrigger = ` - - Delete ${domainName} - - -
-
- -
- -
- ` - } - } - - - const row = document.createElement('tr'); - row.innerHTML = ` - - ${domainName} - - - ${submissionDate} - - ${markupCreatorRow} - - ${request.status} - - - - - ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} - - - ${needsDeleteColumn ? ''+modalTrigger+'' : ''} - `; - tbody.appendChild(row); - }); - - // initialize modals immediately after the DOM content is updated - initializeModals(); - - // Now the DOM and modals are ready, add listeners to the submit buttons - const modals = document.querySelectorAll('.usa-modal__content'); - - modals.forEach(modal => { - const submitButton = modal.querySelector('.usa-modal__submit'); - const closeButton = modal.querySelector('.usa-modal__close'); - submitButton.addEventListener('click', function() { - pk = submitButton.getAttribute('data-pk'); - // Close the modal to remove the USWDS UI local classes - closeButton.click(); - // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page - let pageToDisplay = data.page; - if (data.total == 1 && data.unfiltered_total > 1) { - pageToDisplay--; - } - deleteDomainRequest(pk, pageToDisplay); - }); - }); - - // Do not scroll on first page load - if (scroll) - ScrollToElement('class', 'domain-requests'); - scrollToTable = true; - - // update the pagination after the domain requests list is updated - updatePagination( - 'domain request', - '#domain-requests-pagination', - '#domain-requests-pagination .usa-pagination__counter', - '#domain-requests', - loadDomainRequests, - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - currentSearchTerm - ); - currentSortBy = sortBy; - currentOrder = order; - currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domain requests:', error)); - } - - // Add event listeners to table headers for sorting - tableHeaders.forEach(header => { - header.addEventListener('click', function() { - const sortBy = this.getAttribute('data-sortable'); - let order = 'asc'; - // sort order will be ascending, unless the currently sorted column is ascending, and the user - // is selecting the same column to sort in descending order - if (sortBy === currentSortBy) { - order = currentOrder === 'asc' ? 'desc' : 'asc'; - } - loadDomainRequests(1, sortBy, order); - }); - }); - - domainRequestsSearchSubmit.addEventListener('click', function(e) { - e.preventDefault(); - currentSearchTerm = domainRequestsSearchInput.value; - // If the search is blank, we match the resetSearch functionality - if (currentSearchTerm) { - showElement(resetSearchButton); - } else { - hideElement(resetSearchButton); - } - loadDomainRequests(1, 'id', 'asc'); - resetHeaders(); - }); - - // Reset UI and accessibility - function resetHeaders() { - tableHeaders.forEach(header => { - // unset sort UI in headers - unsetHeader(header); - }); - // Reset the announcement region - tableAnnouncementRegion.innerHTML = ''; - } - - function resetSearch() { - domainRequestsSearchInput.value = ''; - currentSearchTerm = ''; - hideElement(resetSearchButton); - loadDomainRequests(1, 'id', 'asc'); - resetHeaders(); - } - - if (resetSearchButton) { - resetSearchButton.addEventListener('click', function() { - resetSearch(); - }); - } - - function closeMoreActionMenu(accordionThatIsOpen) { - if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") { - accordionThatIsOpen.click(); - } - } - - document.addEventListener('focusin', function(event) { - closeOpenAccordions(event); - }); - - document.addEventListener('click', function(event) { - closeOpenAccordions(event); - }); - - function closeOpenAccordions(event) { - const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]'); - openAccordions.forEach((openAccordionButton) => { - // Find the corresponding accordion - const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); - if (accordion && !accordion.contains(event.target)) { - // Close the accordion if the click is outside - closeMoreActionMenu(openAccordionButton); - } - }); - } - - // Initial load - loadDomainRequests(1); - } -}); - /** * An IIFE that displays confirmation modal on the user profile page diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index d431bfa41..2e4469e12 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -1,7 +1,8 @@ @use "uswds-core" as *; @use "cisa_colors" as *; -/* Make "placeholder" links visually obvious */ +// Used on: TODO links +// Used on: NONE a[href$="todo"]::after { background-color: yellow; color: color(blue-80v); @@ -9,10 +10,14 @@ a[href$="todo"]::after { font-style: italic; } +// Used on: profile +// Note: Is this needed? a.usa-link.usa-link--always-blue { color: #{$dhs-blue}; } +// Used on: breadcrumbs +// Note: This could potentially be simplified and use usa-button--with-icon a.breadcrumb__back { display:flex; align-items: center; @@ -28,10 +33,18 @@ a.breadcrumb__back { } } +// Remove anchor buttons' underline a.usa-button { text-decoration: none; } +// Unstyled anchor buttons +a.usa-button--unstyled:visited { + color: color('primary'); +} + +// Disabled anchor buttons +// NOTE: Not used a.usa-button.disabled-link { background-color: #ccc !important; color: #454545 !important @@ -58,6 +71,8 @@ a.usa-button--unstyled.disabled-link:focus { text-decoration: none !important; } +// Disabled buttons +// Used on: Domain managers, disabled logo on profile .usa-button--unstyled.disabled-button, .usa-button--unstyled.disabled-button:hover, .usa-button--unstyled.disabled-button:focus { @@ -66,6 +81,16 @@ a.usa-button--unstyled.disabled-link:focus { text-decoration: none !important; } +// Unstyled variant for reverse out? +// Used on: NONE +.usa-button--unstyled--white, +.usa-button--unstyled--white:hover, +.usa-button--unstyled--white:focus, +.usa-button--unstyled--white:active { + color: color('white'); +} + +// Solid anchor buttons a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { color: color('white'); } @@ -76,7 +101,8 @@ a.usa-button:not(.usa-button--unstyled, .usa-button--outline):focus, a.usa-button:not(.usa-button--unstyled, .usa-button--outline):active { color: color('white'); } - + +// Outline anchor buttons a.usa-button--outline, a.usa-button--outline:visited { box-shadow: inset 0 0 0 2px color('primary'); @@ -94,10 +120,22 @@ a.usa-button--outline:active { color: color('primary-darker'); } +// Used on: Domain request withdraw confirmation a.withdraw { background-color: color('error'); } + +a.withdraw:hover, +a.withdraw:focus { + background-color: color('error-dark'); +} +a.withdraw:active { + background-color: color('error-darker'); +} + +// Used on: Domain request status +//NOTE: Revise to BEM convention usa-button--outline-secondary a.withdraw_outline, a.withdraw_outline:visited { box-shadow: inset 0 0 0 2px color('error'); @@ -115,19 +153,8 @@ a.withdraw_outline:active { color: color('error-darker'); } -a.withdraw:hover, -a.withdraw:focus { - background-color: color('error-dark'); -} - -a.withdraw:active { - background-color: color('error-darker'); -} -a.usa-button--unstyled:visited { - color: color('primary'); -} - +// Used on: Domain request submit .dotgov-button--green { background-color: color('success-dark'); @@ -140,15 +167,8 @@ a.usa-button--unstyled:visited { } } -.usa-button--unstyled--white, -.usa-button--unstyled--white:hover, -.usa-button--unstyled--white:focus, -.usa-button--unstyled--white:active { - color: color('white'); -} - -// Cancel button used on the -// DNSSEC main page +// Cancel button +// Used on: DNSSEC main page // We want to center this button on mobile // and add some extra left margin on tablet+ .usa-button--cancel { @@ -175,6 +195,8 @@ a.usa-button--unstyled:visited { } } +// Used on: Profile page, toggleable fields +// Note: Could potentially be cleaned up by using usa-button--with-icon // We need to deviate from some default USWDS styles here // in this particular case, so we have to override this. .usa-form .usa-button.readonly-edit-button { @@ -186,6 +208,7 @@ a.usa-button--unstyled:visited { } } +//Used on: Domains and Requests tables .usa-button--filter { width: auto; // For mobile stacking @@ -201,6 +224,8 @@ a.usa-button--unstyled:visited { } } +// Buttons with nested icons +// Note: Can be simplified by adding usa-link--icon to anchors in tables .dotgov-table a, .usa-link--icon, .usa-button--with-icon { @@ -224,6 +249,9 @@ a .usa-icon, width: 1.5em; } +// Red, for delete buttons +// Used on: All delete buttons +// Note: Can be simplified by adding text-secondary to delete anchors in tables button.text-secondary, button.text-secondary:hover, .dotgov-table a.text-secondary { diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 0aedfcdba..44c224aad 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -85,7 +85,7 @@ legend.float-left-tablet + button.float-right-tablet { .read-only-label { font-size: size('body', 'sm'); - color: color('primary'); + color: color('primary-dark'); margin-bottom: units(0.5); } diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 96740a15c..03d9e38c6 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -245,7 +245,6 @@ TEMPLATES = [ "registrar.context_processors.is_production", "registrar.context_processors.org_user_status", "registrar.context_processors.add_path_to_context", - "registrar.context_processors.add_has_profile_feature_flag_to_context", "registrar.context_processors.portfolio_permissions", ], }, diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 41046ed1c..3ceb25af1 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -1,5 +1,4 @@ from django.conf import settings -from waffle.decorators import flag_is_active def language_code(request): @@ -54,10 +53,6 @@ def add_path_to_context(request): return {"path": getattr(request, "path", None)} -def add_has_profile_feature_flag_to_context(request): - return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")} - - def portfolio_permissions(request): """Make portfolio permissions for the request user available in global context""" portfolio_context = { diff --git a/src/registrar/migrations/0128_alter_domaininformation_state_territory_and_more.py b/src/registrar/migrations/0128_alter_domaininformation_state_territory_and_more.py new file mode 100644 index 000000000..a6b8d0519 --- /dev/null +++ b/src/registrar/migrations/0128_alter_domaininformation_state_territory_and_more.py @@ -0,0 +1,229 @@ +# Generated by Django 4.2.10 on 2024-09-11 21:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0127_remove_domaininformation_submitter_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="domaininformation", + name="state_territory", + field=models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state, territory, or military post", + ), + ), + migrations.AlterField( + model_name="domainrequest", + name="state_territory", + field=models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state, territory, or military post", + ), + ), + migrations.AlterField( + model_name="portfolio", + name="state_territory", + field=models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state, territory, or military post", + ), + ), + ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 03b8cc047..d04f09c07 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -159,7 +159,7 @@ class DomainInformation(TimeStampedModel): choices=StateTerritoryChoices.choices, null=True, blank=True, - verbose_name="state / territory", + verbose_name="state, territory, or military post", ) zipcode = models.CharField( max_length=10, diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index babc955aa..161d85ae5 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -11,6 +11,7 @@ from registrar.models.federal_agency import FederalAgency from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.utility.constants import BranchChoices +from auditlog.models import LogEntry from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError @@ -422,7 +423,7 @@ class DomainRequest(TimeStampedModel): choices=StateTerritoryChoices.choices, null=True, blank=True, - verbose_name="state / territory", + verbose_name="state, territory, or military post", ) zipcode = models.CharField( max_length=10, @@ -576,11 +577,25 @@ class DomainRequest(TimeStampedModel): verbose_name="last updated on", help_text="Date of the last status update", ) + notes = models.TextField( null=True, blank=True, ) + def get_first_status_set_date(self, status): + """Returns the date when the domain request was first set to the given status.""" + log_entry = ( + LogEntry.objects.filter(content_type__model="domainrequest", object_pk=self.pk, changes__status__1=status) + .order_by("-timestamp") + .first() + ) + return log_entry.timestamp.date() if log_entry else None + + def get_first_status_started_date(self): + """Returns the date when the domain request was put into the status "started" for the first time""" + return self.get_first_status_set_date(DomainRequest.DomainRequestStatus.STARTED) + @classmethod def get_statuses_that_send_emails(cls): """Returns a list of statuses that send an email to the user""" @@ -1138,6 +1153,11 @@ class DomainRequest(TimeStampedModel): data[field.name] = field.value_from_object(self) return data + def get_formatted_cisa_rep_name(self): + """Returns the cisa representatives name in Western order.""" + names = [n for n in [self.cisa_representative_first_name, self.cisa_representative_last_name] if n] + return " ".join(names) if names else "Unknown" + def _is_federal_complete(self): # Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None return not (self.federal_type is None or self.federal_agency is None) diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index fadcf8cac..61d4f7a30 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -89,7 +89,7 @@ class Portfolio(TimeStampedModel): choices=StateTerritoryChoices.choices, null=True, blank=True, - verbose_name="state / territory", + verbose_name="state, territory, or military post", ) zipcode = models.CharField( diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 8c44a3bdd..3cafe87c4 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -3,6 +3,7 @@ import time import logging from urllib.parse import urlparse, urlunparse, urlencode +from django.urls import resolve, Resolver404 logger = logging.getLogger(__name__) @@ -315,3 +316,21 @@ def convert_queryset_to_dict(queryset, is_model=True, key="id"): request_dict = {value[key]: value for value in queryset} return request_dict + + +def get_url_name(path): + """ + Given a URL path, returns the corresponding URL name defined in urls.py. + + Args: + path (str): The URL path to resolve. + + Returns: + str or None: The URL name if it exists, otherwise None. + """ + try: + match = resolve(path) + return match.url_name + except Resolver404: + logger.error(f"No matching URL name found for path: {path}") + return None diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py index d185c2a82..9dd11a3d8 100644 --- a/src/registrar/models/waffle_flag.py +++ b/src/registrar/models/waffle_flag.py @@ -9,7 +9,7 @@ class WaffleFlag(AbstractUserFlag): Custom implementation of django-waffles 'Flag' object. Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html - Use this class when dealing with feature flags, such as profile_feature. + Use this class when dealing with feature flags. """ class Meta: diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 6207591ba..5a75577df 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -72,12 +72,6 @@ class CheckUserProfileMiddleware: """Runs pre-processing logic for each view. Checks for the finished_setup flag on the current user. If they haven't done so, then we redirect them to the finish setup page.""" - # Check that the user is "opted-in" to the profile feature flag - has_profile_feature_flag = flag_is_active(request, "profile_feature") - - # If they aren't, skip this check entirely - if not has_profile_feature_flag: - return None if request.user.is_authenticated: profile_page = self.profile_page diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index dd08004a3..4b6ca6e77 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -77,19 +77,12 @@ {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %} {% else %} {% url 'domain-org-name-address' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %} + {% include "includes/summary_item.html" with title='Organization' value=domain.domain_info address='true' edit_link=url editable=is_editable %} {% url 'domain-senior-official' pk=domain.id as url %} {% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %} {% endif %} - - {# Conditionally display profile #} - {% if not has_profile_feature_flag %} - {% url 'domain-your-contact-information' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=is_editable %} - {% endif %} - {% url 'domain-security-email' pk=domain.id as url %} {% if security_email is not None and security_email not in hidden_security_emails%} {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %} diff --git a/src/registrar/templates/domain_org_name_address.html b/src/registrar/templates/domain_org_name_address.html index a7eb02b59..78baed09e 100644 --- a/src/registrar/templates/domain_org_name_address.html +++ b/src/registrar/templates/domain_org_name_address.html @@ -7,7 +7,7 @@ {# this is right after the messages block in the parent template #} {% include "includes/form_errors.html" with form=form %} -

Organization name and mailing address

+

Organization

The name of your organization will be publicly listed as the domain registrant.

diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index f4319f53d..fd94e0ef1 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -16,11 +16,9 @@

Time to complete the form

If you have all the information you need, completing your domain request might take around 15 minutes.

- {% if has_profile_feature_flag %}

How we’ll reach you

While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review. If the contact information below is not correct, visit your profile to make updates.

{% include "includes/profile_information.html" with user=user%} - {% endif %} {% block form_buttons %} diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index 944371a6a..460f6ae29 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -8,33 +8,30 @@ {% block content %}
- {% comment %} - TODO: Uncomment in #2596 {% if portfolio %} {% url 'domain-requests' as url %} - +

Domain request for {{ DomainRequest.requested_domain.name }}

Status: - {% if DomainRequest.status == 'approved' %} Approved - {% elif DomainRequest.status == 'in review' %} In review - {% elif DomainRequest.status == 'rejected' %} Rejected - {% elif DomainRequest.status == 'submitted' %} Submitted - {% elif DomainRequest.status == 'ineligible' %} Ineligible - {% else %}ERROR Please contact technical support/dev - {% endif %} + {{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}


-

Last updated: {{DomainRequest.updated_at|date:"F j, Y"}}

- + + {% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %} + {% comment %} + These are intentionally seperated this way. + There is some code repetition, but it gives us more flexibility rather than a dense reduction. + Leave it this way until we've solidified our requirements. + {% endcomment %} + {% if DomainRequest.status == statuses.STARTED %} + {% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %} +

+ {% comment %} + A newly created domain request will not have a value for last_status update. + This is because the status never really updated. + However, if this somehow goes back to started we can default to displaying that new date. + {% endcomment %} + Started on: {{last_status_update|default:first_started_date}} +

+ {% endwith %} + {% elif DomainRequest.status == statuses.SUBMITTED %} +

+ Submitted on: {{last_submitted|default:first_submitted }} +

+

+ Last updated on: {{DomainRequest.updated_at|date:"F j, Y"}} +

+ {% elif DomainRequest.status == statuses.ACTION_NEEDED %} +

+ Submitted on: {{last_submitted|default:first_submitted }} +

+

+ Last updated on: {{DomainRequest.updated_at|date:"F j, Y"}} +

+ {% elif DomainRequest.status == statuses.REJECTED %} +

+ Submitted on: {{last_submitted|default:first_submitted }} +

+

+ Rejected on: {{last_status_update}} +

+ {% elif DomainRequest.status == statuses.WITHDRAWN %} +

+ Submitted on: {{last_submitted|default:first_submitted }} +

+

+ Withdrawn on: {{last_status_update}} +

+ {% else %} + {% comment %} Shown for in_review, approved, ineligible {% endcomment %} +

+ Last updated on: {{DomainRequest.updated_at|date:"F j, Y"}} +

+ {% endif %} + {% if DomainRequest.status != 'rejected' %}

{% include "includes/domain_request.html" %}

Withdraw request

{% endif %} + {% endwith %}
@@ -100,7 +143,7 @@ {% endif %} {% if DomainRequest.organization_name %} - {% include "includes/summary_item.html" with title='Organization name and mailing address' value=DomainRequest address='true' heading_level=heading_level %} + {% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %} {% endif %} {% if DomainRequest.about_your_organization %} @@ -130,7 +173,6 @@ {% if DomainRequest.creator %} {% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %} {% endif %} - {% if DomainRequest.other_contacts.all %} {% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %} {% else %} @@ -141,8 +183,8 @@ {% if DomainRequest %}

CISA Regional Representative

+ {% if portfolio %} +
+ Filter by +
+
+ +
+
+

Status

+
+ Select to apply status filter +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ {% endif %}