mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 18:39:21 +02:00
Merge pull request #2267 from cisagov/rjm/2179-search-bar
Issue #2179: Search bar on domain and request tables, home page
This commit is contained in:
commit
c0201f5fa3
10 changed files with 572 additions and 159 deletions
|
@ -17,6 +17,22 @@ var SUCCESS = "success";
|
||||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
// Helper functions.
|
// Helper functions.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide element
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const hideElement = (element) => {
|
||||||
|
element.classList.add('display-none');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show element
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const showElement = (element) => {
|
||||||
|
element.classList.remove('display-none');
|
||||||
|
};
|
||||||
|
|
||||||
/** Makes an element invisible. */
|
/** Makes an element invisible. */
|
||||||
function makeHidden(el) {
|
function makeHidden(el) {
|
||||||
el.style.position = "absolute";
|
el.style.position = "absolute";
|
||||||
|
@ -918,8 +934,9 @@ function ScrollToElement(attributeName, attributeValue) {
|
||||||
* @param {boolean} hasPrevious - Whether there is a page before the current page.
|
* @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 {boolean} hasNext - Whether there is a page after the current page.
|
||||||
* @param {number} totalItems - The total number of items.
|
* @param {number} totalItems - The total number of items.
|
||||||
|
* @param {string} searchTerm - The search term
|
||||||
*/
|
*/
|
||||||
function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems) {
|
function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
|
||||||
const paginationContainer = document.querySelector(paginationSelector);
|
const paginationContainer = document.querySelector(paginationSelector);
|
||||||
const paginationCounter = document.querySelector(counterSelector);
|
const paginationCounter = document.querySelector(counterSelector);
|
||||||
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
||||||
|
@ -932,7 +949,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
||||||
// Counter should only be displayed if there is more than 1 item
|
// Counter should only be displayed if there is more than 1 item
|
||||||
paginationContainer.classList.toggle('display-none', totalItems < 1);
|
paginationContainer.classList.toggle('display-none', totalItems < 1);
|
||||||
|
|
||||||
paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}`;
|
paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${searchTerm ? ' for ' + '"' + searchTerm + '"' : ''}`;
|
||||||
|
|
||||||
if (hasPrevious) {
|
if (hasPrevious) {
|
||||||
const prevPageItem = document.createElement('li');
|
const prevPageItem = document.createElement('li');
|
||||||
|
@ -1018,6 +1035,47 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper that toggles content/ no content/ no search results
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm) => {
|
||||||
|
const { unfiltered_total, total } = data;
|
||||||
|
|
||||||
|
if (searchTermHolder)
|
||||||
|
searchTermHolder.innerHTML = '';
|
||||||
|
|
||||||
|
if (unfiltered_total) {
|
||||||
|
if (total) {
|
||||||
|
showElement(dataWrapper);
|
||||||
|
hideElement(noSearchResultsWrapper);
|
||||||
|
hideElement(noDataWrapper);
|
||||||
|
} else {
|
||||||
|
if (searchTermHolder)
|
||||||
|
searchTermHolder.innerHTML = currentSearchTerm;
|
||||||
|
hideElement(dataWrapper);
|
||||||
|
showElement(noSearchResultsWrapper);
|
||||||
|
hideElement(noDataWrapper);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hideElement(dataWrapper);
|
||||||
|
hideElement(noSearchResultsWrapper);
|
||||||
|
showElement(noDataWrapper);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||||
|
@ -1025,13 +1083,21 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
let domainsWrapper = document.querySelector('.domains-wrapper');
|
const domainsWrapper = document.querySelector('.domains__table-wrapper');
|
||||||
|
|
||||||
if (domainsWrapper) {
|
if (domainsWrapper) {
|
||||||
let currentSortBy = 'id';
|
let currentSortBy = 'id';
|
||||||
let currentOrder = 'asc';
|
let currentOrder = 'asc';
|
||||||
let noDomainsWrapper = document.querySelector('.no-domains-wrapper');
|
const noDomainsWrapper = document.querySelector('.domains__no-data');
|
||||||
|
const noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
|
||||||
let hasLoaded = false;
|
let hasLoaded = false;
|
||||||
|
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 searchTermHolder = document.querySelector('.domains__search-term');
|
||||||
|
const resetButton = document.querySelector('.domains__reset-button');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||||
|
@ -1040,10 +1106,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
* @param {*} sortBy - the sort column option
|
* @param {*} sortBy - the sort column option
|
||||||
* @param {*} order - the sort order {asc, desc}
|
* @param {*} order - the sort order {asc, desc}
|
||||||
* @param {*} loaded - control for the scrollToElement functionality
|
* @param {*} loaded - control for the scrollToElement functionality
|
||||||
|
* @param {*} searchTerm - the search term
|
||||||
*/
|
*/
|
||||||
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded) {
|
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) {
|
||||||
//fetch json of page of domains, given page # and sort
|
//fetch json of page of domains, given page # and sort
|
||||||
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}`)
|
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
|
@ -1051,23 +1118,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no domains exist in the list
|
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results
|
||||||
if (data.domains.length) {
|
updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm);
|
||||||
domainsWrapper.classList.remove('display-none');
|
|
||||||
noDomainsWrapper.classList.add('display-none');
|
|
||||||
} else {
|
|
||||||
domainsWrapper.classList.add('display-none');
|
|
||||||
noDomainsWrapper.classList.remove('display-none');
|
|
||||||
}
|
|
||||||
|
|
||||||
// identify the DOM element where the domain list will be inserted into the DOM
|
// identify the DOM element where the domain list will be inserted into the DOM
|
||||||
const domainList = document.querySelector('.dotgov-table__registered-domains tbody');
|
const domainList = document.querySelector('.domains__table tbody');
|
||||||
domainList.innerHTML = '';
|
domainList.innerHTML = '';
|
||||||
|
|
||||||
data.domains.forEach(domain => {
|
data.domains.forEach(domain => {
|
||||||
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||||
const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null;
|
const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null;
|
||||||
const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : null;
|
const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : '';
|
||||||
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
|
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
|
||||||
const actionUrl = domain.action_url;
|
const actionUrl = domain.action_url;
|
||||||
|
|
||||||
|
@ -1106,9 +1167,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
});
|
});
|
||||||
// initialize tool tips immediately after the associated DOM elements are added
|
// initialize tool tips immediately after the associated DOM elements are added
|
||||||
initializeTooltips();
|
initializeTooltips();
|
||||||
|
|
||||||
|
// Do not scroll on first page load
|
||||||
if (loaded)
|
if (loaded)
|
||||||
ScrollToElement('id', 'domains-header');
|
ScrollToElement('id', 'domains-header');
|
||||||
|
|
||||||
hasLoaded = true;
|
hasLoaded = true;
|
||||||
|
|
||||||
// update pagination
|
// update pagination
|
||||||
|
@ -1122,18 +1184,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
data.num_pages,
|
data.num_pages,
|
||||||
data.has_previous,
|
data.has_previous,
|
||||||
data.has_next,
|
data.has_next,
|
||||||
data.total
|
data.total,
|
||||||
|
currentSearchTerm
|
||||||
);
|
);
|
||||||
currentSortBy = sortBy;
|
currentSortBy = sortBy;
|
||||||
currentOrder = order;
|
currentOrder = order;
|
||||||
|
currentSearchTerm = searchTerm;
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching domains:', error));
|
.catch(error => console.error('Error fetching domains:', error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Add event listeners to table headers for sorting
|
// Add event listeners to table headers for sorting
|
||||||
document.querySelectorAll('.dotgov-table__registered-domains th[data-sortable]').forEach(header => {
|
tableHeaders.forEach(header => {
|
||||||
header.addEventListener('click', function() {
|
header.addEventListener('click', function() {
|
||||||
const sortBy = this.getAttribute('data-sortable');
|
const sortBy = this.getAttribute('data-sortable');
|
||||||
let order = 'asc';
|
let order = 'asc';
|
||||||
|
@ -1147,6 +1209,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
domainsSearchSubmit.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
currentSearchTerm = domainsSearchInput.value;
|
||||||
|
// If the search is blank, we match the resetSearch functionality
|
||||||
|
if (currentSearchTerm) {
|
||||||
|
showElement(resetButton);
|
||||||
|
} else {
|
||||||
|
hideElement(resetButton);
|
||||||
|
}
|
||||||
|
loadDomains(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() {
|
||||||
|
domainsSearchInput.value = '';
|
||||||
|
currentSearchTerm = '';
|
||||||
|
hideElement(resetButton);
|
||||||
|
loadDomains(1, 'id', 'asc', hasLoaded, '');
|
||||||
|
resetHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetButton) {
|
||||||
|
resetButton.addEventListener('click', function() {
|
||||||
|
resetSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Load the first page initially
|
// Load the first page initially
|
||||||
loadDomains(1);
|
loadDomains(1);
|
||||||
}
|
}
|
||||||
|
@ -1157,25 +1256,71 @@ const utcDateString = (dateString) => {
|
||||||
const utcYear = date.getUTCFullYear();
|
const utcYear = date.getUTCFullYear();
|
||||||
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
|
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
|
||||||
const utcDay = date.getUTCDate().toString().padStart(2, '0');
|
const utcDay = date.getUTCDate().toString().padStart(2, '0');
|
||||||
const utcHours = date.getUTCHours().toString().padStart(2, '0');
|
let utcHours = date.getUTCHours();
|
||||||
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
|
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} UTC`;
|
const ampm = utcHours >= 12 ? 'PM' : 'AM';
|
||||||
|
utcHours = utcHours % 12 || 12; // Convert to 12-hour format, '0' hours should be '12'
|
||||||
|
|
||||||
|
return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
* 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.
|
* initializes the domain requests list and associated functionality on the home page of the app.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
let domainRequestsWrapper = document.querySelector('.domain-requests-wrapper');
|
const domainRequestsSectionWrapper = document.querySelector('.domain-requests');
|
||||||
|
const domainRequestsWrapper = document.querySelector('.domain-requests__table-wrapper');
|
||||||
|
|
||||||
if (domainRequestsWrapper) {
|
if (domainRequestsWrapper) {
|
||||||
let currentSortBy = 'id';
|
let currentSortBy = 'id';
|
||||||
let currentOrder = 'asc';
|
let currentOrder = 'asc';
|
||||||
let noDomainRequestsWrapper = document.querySelector('.no-domain-requests-wrapper');
|
const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data');
|
||||||
|
const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results');
|
||||||
let hasLoaded = false;
|
let hasLoaded = 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 searchTermHolder = document.querySelector('.domain-requests__search-term');
|
||||||
|
const resetButton = document.querySelector('.domain-requests__reset-button');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
// 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, hasLoaded, 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
|
* Loads rows in the domain requests list, as well as updates pagination around the domain requests list
|
||||||
|
@ -1184,10 +1329,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
* @param {*} sortBy - the sort column option
|
* @param {*} sortBy - the sort column option
|
||||||
* @param {*} order - the sort order {asc, desc}
|
* @param {*} order - the sort order {asc, desc}
|
||||||
* @param {*} loaded - control for the scrollToElement functionality
|
* @param {*} loaded - control for the scrollToElement functionality
|
||||||
|
* @param {*} searchTerm - the search term
|
||||||
*/
|
*/
|
||||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded) {
|
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) {
|
||||||
//fetch json of page of domain requests, given page # and sort
|
//fetch json of page of domain requests, given page # and sort
|
||||||
fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}`)
|
fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
|
@ -1195,41 +1341,138 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no domain requests exist in the list
|
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
||||||
if (data.domain_requests.length) {
|
updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm);
|
||||||
domainRequestsWrapper.classList.remove('display-none');
|
|
||||||
noDomainRequestsWrapper.classList.add('display-none');
|
|
||||||
} else {
|
|
||||||
domainRequestsWrapper.classList.add('display-none');
|
|
||||||
noDomainRequestsWrapper.classList.remove('display-none');
|
|
||||||
}
|
|
||||||
|
|
||||||
// identify the DOM element where the domain request list will be inserted into the DOM
|
// identify the DOM element where the domain request list will be inserted into the DOM
|
||||||
const tbody = document.querySelector('.dotgov-table__domain-requests tbody');
|
const tbody = document.querySelector('.domain-requests__table tbody');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
// remove any existing modal elements from the DOM so they can be properly re-initialized
|
// 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
|
// after the DOM content changes and there are new delete modal buttons added
|
||||||
unloadModals();
|
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 = `
|
||||||
|
<span class="usa-sr-only">Delete Action</span>`;
|
||||||
|
let tableHeaderRow = document.querySelector('.domain-requests__table thead tr');
|
||||||
|
tableHeaderRow.appendChild(delheader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data.domain_requests.forEach(request => {
|
data.domain_requests.forEach(request => {
|
||||||
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||||
const domainName = request.requested_domain ? request.requested_domain : `New domain request <br><span class="text-base font-body-xs">(${utcDateString(request.created_at)})</span>`;
|
const domainName = request.requested_domain ? request.requested_domain : `New domain request <br><span class="text-base font-body-xs">(${utcDateString(request.created_at)})</span>`;
|
||||||
const actionUrl = request.action_url;
|
const actionUrl = request.action_url;
|
||||||
const actionLabel = request.action_label;
|
const actionLabel = request.action_label;
|
||||||
const submissionDate = request.submission_date ? new Date(request.submission_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
|
const submissionDate = request.submission_date ? new Date(request.submission_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
|
||||||
const deleteButton = request.is_deletable ? `
|
|
||||||
<a
|
// Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed
|
||||||
role="button"
|
let modalTrigger = '';
|
||||||
id="button-toggle-delete-domain-alert-${request.id}"
|
|
||||||
href="#toggle-delete-domain-alert-${request.id}"
|
// If the request is deletable, create modal body and insert it
|
||||||
class="usa-button--unstyled text-no-underline late-loading-modal-trigger"
|
if (request.is_deletable) {
|
||||||
aria-controls="toggle-delete-domain-alert-${request.id}"
|
let modalHeading = '';
|
||||||
data-open-modal
|
let modalDescription = '';
|
||||||
>
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
if (request.requested_domain) {
|
||||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
modalHeading = `Are you sure you want to delete ${request.requested_domain}?`;
|
||||||
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.';
|
||||||
</a>` : '';
|
} 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 = `
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
id="button-toggle-delete-domain-alert-${request.id}"
|
||||||
|
href="#toggle-delete-domain-alert-${request.id}"
|
||||||
|
class="usa-button--unstyled text-no-underline late-loading-modal-trigger"
|
||||||
|
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||||
|
data-open-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
|
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||||
|
</a>`
|
||||||
|
|
||||||
|
const modalSubmit = `
|
||||||
|
<button type="button"
|
||||||
|
class="usa-button usa-button--secondary usa-modal__submit"
|
||||||
|
data-pk = ${request.id}
|
||||||
|
name="delete-domain-request">Yes, delete request</button>
|
||||||
|
`
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
|
${modalHeading}
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p id="modal-1-description">
|
||||||
|
${modalDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
${modalSubmit}
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
domainRequestsSectionWrapper.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
|
@ -1250,15 +1493,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
${actionLabel} <span class="usa-sr-only">${request.requested_domain ? request.requested_domain : 'New domain request'}</span>
|
${actionLabel} <span class="usa-sr-only">${request.requested_domain ? request.requested_domain : 'New domain request'}</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>${deleteButton}</td>
|
${needsDeleteColumn ? '<td>'+modalTrigger+'</td>' : ''}
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
// initialize modals immediately after the DOM content is updated
|
// initialize modals immediately after the DOM content is updated
|
||||||
initializeModals();
|
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 (loaded)
|
if (loaded)
|
||||||
ScrollToElement('id', 'domain-requests-header');
|
ScrollToElement('id', 'domain-requests-header');
|
||||||
|
|
||||||
hasLoaded = true;
|
hasLoaded = true;
|
||||||
|
|
||||||
// update the pagination after the domain requests list is updated
|
// update the pagination after the domain requests list is updated
|
||||||
|
@ -1272,16 +1536,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
data.num_pages,
|
data.num_pages,
|
||||||
data.has_previous,
|
data.has_previous,
|
||||||
data.has_next,
|
data.has_next,
|
||||||
data.total
|
data.total,
|
||||||
|
currentSearchTerm
|
||||||
);
|
);
|
||||||
currentSortBy = sortBy;
|
currentSortBy = sortBy;
|
||||||
currentOrder = order;
|
currentOrder = order;
|
||||||
|
currentSearchTerm = searchTerm;
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching domain requests:', error));
|
.catch(error => console.error('Error fetching domain requests:', error));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners to table headers for sorting
|
// Add event listeners to table headers for sorting
|
||||||
document.querySelectorAll('.dotgov-table__domain-requests th[data-sortable]').forEach(header => {
|
tableHeaders.forEach(header => {
|
||||||
header.addEventListener('click', function() {
|
header.addEventListener('click', function() {
|
||||||
const sortBy = this.getAttribute('data-sortable');
|
const sortBy = this.getAttribute('data-sortable');
|
||||||
let order = 'asc';
|
let order = 'asc';
|
||||||
|
@ -1294,6 +1560,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
domainRequestsSearchSubmit.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
currentSearchTerm = domainRequestsSearchInput.value;
|
||||||
|
// If the search is blank, we match the resetSearch functionality
|
||||||
|
if (currentSearchTerm) {
|
||||||
|
showElement(resetButton);
|
||||||
|
} else {
|
||||||
|
hideElement(resetButton);
|
||||||
|
}
|
||||||
|
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(resetButton);
|
||||||
|
loadDomainRequests(1, 'id', 'asc', hasLoaded, '');
|
||||||
|
resetHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetButton) {
|
||||||
|
resetButton.addEventListener('click', function() {
|
||||||
|
resetSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Load the first page initially
|
// Load the first page initially
|
||||||
loadDomainRequests(1);
|
loadDomainRequests(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1040px){
|
@media (min-width: 1040px){
|
||||||
.dotgov-table__domain-requests {
|
.domain-requests__table {
|
||||||
th:nth-of-type(1) {
|
th:nth-of-type(1) {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1040px){
|
@media (min-width: 1040px){
|
||||||
.dotgov-table__registered-domains {
|
.domains__table {
|
||||||
th:nth-of-type(1) {
|
th:nth-of-type(1) {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,39 @@
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section class="section--outlined">
|
<section class="section--outlined domains">
|
||||||
<h2 id="domains-header">Domains</h2>
|
<div class="grid-row">
|
||||||
<div class="domains-wrapper display-none">
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__registered-domains">
|
<h2 id="domains-header" class="flex-6">Domains</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
|
<section aria-label="Domains search component" class="flex-6 margin-y-2">
|
||||||
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-button display-none" type="button">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<label class="usa-sr-only" for="domains__search-field">Search</label>
|
||||||
|
<input
|
||||||
|
class="usa-input"
|
||||||
|
id="domains__search-field"
|
||||||
|
type="search"
|
||||||
|
name="search"
|
||||||
|
placeholder="Search by domain name"
|
||||||
|
/>
|
||||||
|
<button class="usa-button" type="submit" id="domains__search-field-submit">
|
||||||
|
<img
|
||||||
|
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||||
|
class="usa-search__submit-icon"
|
||||||
|
alt="Search"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="domains__table-wrapper display-none">
|
||||||
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domains__table">
|
||||||
<caption class="sr-only">Your registered domains</caption>
|
<caption class="sr-only">Your registered domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -50,7 +79,7 @@
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="no-domains-wrapper display-none">
|
<div class="domains__no-data display-none">
|
||||||
<p>You don't have any registered domains.</p>
|
<p>You don't have any registered domains.</p>
|
||||||
<p class="maxw-none clearfix">
|
<p class="maxw-none clearfix">
|
||||||
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet display-flex flex-align-start usa-link" target="_blank">
|
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet display-flex flex-align-start usa-link" target="_blank">
|
||||||
|
@ -61,6 +90,9 @@
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="domains__no-search-results display-none">
|
||||||
|
<p>No results found for "<span class="domains__search-term"></span>"</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">
|
||||||
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||||
|
@ -71,10 +103,39 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section class="section--outlined">
|
<section class="section--outlined domain-requests">
|
||||||
<h2 id="domain-requests-header">Domain requests</h2>
|
<div class="grid-row">
|
||||||
<div class="domain-requests-wrapper display-none">
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__domain-requests">
|
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
|
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
||||||
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-button display-none" type="button">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<label class="usa-sr-only" for="domain-requests__search-field">Search</label>
|
||||||
|
<input
|
||||||
|
class="usa-input"
|
||||||
|
id="domain-requests__search-field"
|
||||||
|
type="search"
|
||||||
|
name="search"
|
||||||
|
placeholder="Search by domain name"
|
||||||
|
/>
|
||||||
|
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
||||||
|
<img
|
||||||
|
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||||
|
class="usa-search__submit-icon"
|
||||||
|
alt="Search"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="domain-requests__table-wrapper display-none">
|
||||||
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -82,7 +143,7 @@
|
||||||
<th data-sortable="submission_date" scope="col" role="columnheader">Date submitted</th>
|
<th data-sortable="submission_date" scope="col" role="columnheader">Date submitted</th>
|
||||||
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Delete Action</span></th>
|
<!-- AJAX will conditionally add a th for delete actions -->
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="domain-requests-tbody">
|
<tbody id="domain-requests-tbody">
|
||||||
|
@ -93,45 +154,13 @@
|
||||||
class="usa-sr-only usa-table__announcement-region"
|
class="usa-sr-only usa-table__announcement-region"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
{% for domain_request in domain_requests %}
|
|
||||||
{% if has_deletable_domain_requests %}
|
|
||||||
{% if domain_request.status == domain_request.DomainRequestStatus.STARTED or domain_request.status == domain_request.DomainRequestStatus.WITHDRAWN %}
|
|
||||||
<div
|
|
||||||
class="usa-modal"
|
|
||||||
id="toggle-delete-domain-alert-{{ domain_request.id }}"
|
|
||||||
aria-labelledby="Are you sure you want to continue?"
|
|
||||||
aria-describedby="Domain will be removed"
|
|
||||||
data-force-action
|
|
||||||
>
|
|
||||||
<form method="POST" action="{% url "domain-request-delete" pk=domain_request.id %}">
|
|
||||||
{% if domain_request.requested_domain is None %}
|
|
||||||
{% if domain_request.created_at %}
|
|
||||||
{% with prefix="(created " %}
|
|
||||||
{% with formatted_date=domain_request.created_at|date:"DATETIME_FORMAT" %}
|
|
||||||
{% with modal_content=prefix|add:formatted_date|add:" UTC)" %}
|
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this domain request?" modal_description="This will remove the domain request "|add:modal_content|add:" from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% else %}
|
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete New domain request?" modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% with modal_heading_value=domain_request.requested_domain.name|add:"?" %}
|
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete" heading_value=modal_heading_value modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="no-domain-requests-wrapper display-none">
|
<div class="domain-requests__no-data display-none">
|
||||||
<p>You haven't requested any domains.</p>
|
<p>You haven't requested any domains.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="domain-requests__no-search-results display-none">
|
||||||
|
<p>No results found for "<span class="domain-requests__search-term"></span>"</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
||||||
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||||
|
|
|
@ -384,15 +384,15 @@ class HomeTests(TestWithUser):
|
||||||
)
|
)
|
||||||
domain_request_2.other_contacts.set([contact_shared])
|
domain_request_2.other_contacts.set([contact_shared])
|
||||||
|
|
||||||
# Ensure that igorville.gov exists on the page
|
igorville = DomainRequest.objects.filter(requested_domain__name="igorville.gov")
|
||||||
home_page = self.client.get("/")
|
self.assertTrue(igorville.exists())
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
|
|
||||||
# Trigger the delete logic
|
# Trigger the delete logic
|
||||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}))
|
||||||
|
|
||||||
# igorville is now deleted
|
# igorville is now deleted
|
||||||
self.assertNotContains(response, "igorville.gov")
|
igorville = DomainRequest.objects.filter(requested_domain__name="igorville.gov")
|
||||||
|
self.assertFalse(igorville.exists())
|
||||||
|
|
||||||
# Check if the orphaned contact was deleted
|
# Check if the orphaned contact was deleted
|
||||||
orphan = Contact.objects.filter(id=contact.id)
|
orphan = Contact.objects.filter(id=contact.id)
|
||||||
|
@ -456,13 +456,14 @@ class HomeTests(TestWithUser):
|
||||||
)
|
)
|
||||||
domain_request_2.other_contacts.set([contact_shared])
|
domain_request_2.other_contacts.set([contact_shared])
|
||||||
|
|
||||||
home_page = self.client.get("/")
|
teaville = DomainRequest.objects.filter(requested_domain__name="teaville.gov")
|
||||||
self.assertContains(home_page, "teaville.gov")
|
self.assertTrue(teaville.exists())
|
||||||
|
|
||||||
# Trigger the delete logic
|
# Trigger the delete logic
|
||||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request_2.pk}), follow=True)
|
self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request_2.pk}))
|
||||||
|
|
||||||
self.assertNotContains(response, "teaville.gov")
|
teaville = DomainRequest.objects.filter(requested_domain__name="teaville.gov")
|
||||||
|
self.assertFalse(teaville.exists())
|
||||||
|
|
||||||
# Check if the orphaned contact was deleted
|
# Check if the orphaned contact was deleted
|
||||||
orphan = Contact.objects.filter(id=contact_shared.id)
|
orphan = Contact.objects.filter(id=contact_shared.id)
|
||||||
|
|
|
@ -102,6 +102,35 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
)
|
)
|
||||||
self.assertEqual(svg_icon_expected, svg_icons[i])
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
|
def test_get_domains_json_search(self):
|
||||||
|
"""Test search."""
|
||||||
|
# Define your URL variables as a dictionary
|
||||||
|
url_vars = {"search_term": "e2"}
|
||||||
|
|
||||||
|
# Use the params parameter to include URL variables
|
||||||
|
response = self.app.get(reverse("get_domains_json"), params=url_vars)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
self.assertEqual(data["total"], 1)
|
||||||
|
self.assertEqual(data["unfiltered_total"], 3)
|
||||||
|
|
||||||
|
# Check the number of domain requests
|
||||||
|
self.assertEqual(len(data["domains"]), 1)
|
||||||
|
|
||||||
|
# Extract fields from response
|
||||||
|
domains = [request["name"] for request in data["domains"]]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.domain2.name,
|
||||||
|
domains[0],
|
||||||
|
)
|
||||||
|
|
||||||
def test_pagination(self):
|
def test_pagination(self):
|
||||||
"""Test that pagination is correct in the response"""
|
"""Test that pagination is correct in the response"""
|
||||||
response = self.app.get(reverse("get_domains_json"), {"page": 1})
|
response = self.app.get(reverse("get_domains_json"), {"page": 1})
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from registrar.models import DomainRequest
|
from registrar.models import DomainRequest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from registrar.models.draft_domain import DraftDomain
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
|
@ -10,32 +12,37 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
lamb_chops, _ = DraftDomain.objects.get_or_create(name="lamb-chops.gov")
|
||||||
|
short_ribs, _ = DraftDomain.objects.get_or_create(name="short-ribs.gov")
|
||||||
|
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
|
||||||
|
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
|
||||||
|
|
||||||
# Create domain requests for the user
|
# Create domain requests for the user
|
||||||
self.domain_requests = [
|
self.domain_requests = [
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
requested_domain=None,
|
requested_domain=lamb_chops,
|
||||||
submission_date="2024-01-01",
|
submission_date="2024-01-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-01-01",
|
created_at="2024-01-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
requested_domain=None,
|
requested_domain=short_ribs,
|
||||||
submission_date="2024-02-01",
|
submission_date="2024-02-01",
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
created_at="2024-02-01",
|
created_at="2024-02-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
requested_domain=None,
|
requested_domain=beef_chuck,
|
||||||
submission_date="2024-03-01",
|
submission_date="2024-03-01",
|
||||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
created_at="2024-03-01",
|
created_at="2024-03-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
requested_domain=None,
|
requested_domain=stew_beef,
|
||||||
submission_date="2024-04-01",
|
submission_date="2024-04-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-04-01",
|
created_at="2024-04-01",
|
||||||
|
@ -195,6 +202,61 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
)
|
)
|
||||||
self.assertEqual(svg_icon_expected, svg_icons[i])
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
|
def test_get_domain_requests_json_search(self):
|
||||||
|
"""Test search."""
|
||||||
|
# Define your URL variables as a dictionary
|
||||||
|
url_vars = {"search_term": "lamb"}
|
||||||
|
|
||||||
|
# Use the params parameter to include URL variables
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), params=url_vars)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
self.assertEqual(data["total"], 1)
|
||||||
|
self.assertEqual(data["unfiltered_total"], 12)
|
||||||
|
|
||||||
|
# Check the number of domain requests
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 1)
|
||||||
|
|
||||||
|
# Extract fields from response
|
||||||
|
requested_domains = [request["requested_domain"] for request in data["domain_requests"]]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.domain_requests[0].requested_domain.name,
|
||||||
|
requested_domains[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_domain_requests_json_search_new_domains(self):
|
||||||
|
"""Test search when looking up New domain requests"""
|
||||||
|
# Define your URL variables as a dictionary
|
||||||
|
url_vars = {"search_term": "ew"}
|
||||||
|
|
||||||
|
# Use the params parameter to include URL variables
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), params=url_vars)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
pagination_fields = ["page", "has_next", "has_previous", "num_pages", "total", "unfiltered_total"]
|
||||||
|
expected_pagination_values = [1, False, False, 1, 9, 12]
|
||||||
|
for field, expected_value in zip(pagination_fields, expected_pagination_values):
|
||||||
|
self.assertEqual(data[field], expected_value)
|
||||||
|
|
||||||
|
# Check the number of domain requests
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 9)
|
||||||
|
|
||||||
|
# Extract fields from response
|
||||||
|
requested_domains = [request.get("requested_domain") for request in data["domain_requests"]]
|
||||||
|
|
||||||
|
expected_domain_values = ["stew-beef.gov"] + [None] * 8
|
||||||
|
for expected_value, actual_value in zip(expected_domain_values, requested_domains):
|
||||||
|
self.assertEqual(expected_value, actual_value)
|
||||||
|
|
||||||
def test_pagination(self):
|
def test_pagination(self):
|
||||||
"""Test that pagination works properly. There are 11 total non-approved requests and
|
"""Test that pagination works properly. There are 11 total non-approved requests and
|
||||||
a page size of 10"""
|
a page size of 10"""
|
||||||
|
|
|
@ -798,7 +798,8 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
contacts_to_delete, duplicates = self._get_orphaned_contacts(domain_request)
|
contacts_to_delete, duplicates = self._get_orphaned_contacts(domain_request)
|
||||||
|
|
||||||
# Delete the DomainRequest
|
# Delete the DomainRequest
|
||||||
response = super().post(request, *args, **kwargs)
|
self.object = self.get_object()
|
||||||
|
self.object.delete()
|
||||||
|
|
||||||
# Delete orphaned contacts - but only for if they are not associated with a user
|
# Delete orphaned contacts - but only for if they are not associated with a user
|
||||||
Contact.objects.filter(id__in=contacts_to_delete, user=None).delete()
|
Contact.objects.filter(id__in=contacts_to_delete, user=None).delete()
|
||||||
|
@ -810,7 +811,8 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
|
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
|
||||||
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()
|
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()
|
||||||
|
|
||||||
return response
|
# Return a 200 response with an empty body
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
def _get_orphaned_contacts(self, domain_request: DomainRequest, check_db=False):
|
def _get_orphaned_contacts(self, domain_request: DomainRequest, check_db=False):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -4,6 +4,7 @@ from registrar.models import DomainRequest
|
||||||
from django.utils.dateformat import format
|
from django.utils.dateformat import format
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -14,9 +15,27 @@ def get_domain_requests_json(request):
|
||||||
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
|
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED
|
status=DomainRequest.DomainRequestStatus.APPROVED
|
||||||
)
|
)
|
||||||
|
unfiltered_total = domain_requests.count()
|
||||||
|
|
||||||
# Handle sorting
|
# Handle sorting
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
search_term = request.GET.get("search_term")
|
||||||
|
|
||||||
|
if search_term:
|
||||||
|
search_term_lower = search_term.lower()
|
||||||
|
new_domain_request_text = "new domain request"
|
||||||
|
|
||||||
|
# Check if the search term is a substring of 'New domain request'
|
||||||
|
# If yes, we should return domain requests that do not have a
|
||||||
|
# requested_domain (those display as New domain request in the UI)
|
||||||
|
if search_term_lower in new_domain_request_text:
|
||||||
|
domain_requests = domain_requests.filter(
|
||||||
|
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
domain_requests = domain_requests.filter(Q(requested_domain__name__icontains=search_term))
|
||||||
|
|
||||||
if order == "desc":
|
if order == "desc":
|
||||||
sort_by = f"-{sort_by}"
|
sort_by = f"-{sort_by}"
|
||||||
domain_requests = domain_requests.order_by(sort_by)
|
domain_requests = domain_requests.order_by(sort_by)
|
||||||
|
@ -75,5 +94,6 @@ def get_domain_requests_json(request):
|
||||||
"page": page_obj.number,
|
"page": page_obj.number,
|
||||||
"num_pages": paginator.num_pages,
|
"num_pages": paginator.num_pages,
|
||||||
"total": paginator.count,
|
"total": paginator.count,
|
||||||
|
"unfiltered_total": unfiltered_total,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.core.paginator import Paginator
|
||||||
from registrar.models import UserDomainRole, Domain
|
from registrar.models import UserDomainRole, Domain
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -14,10 +15,15 @@ def get_domains_json(request):
|
||||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||||
|
|
||||||
objects = Domain.objects.filter(id__in=domain_ids)
|
objects = Domain.objects.filter(id__in=domain_ids)
|
||||||
|
unfiltered_total = objects.count()
|
||||||
|
|
||||||
# Handle sorting
|
# Handle sorting
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
search_term = request.GET.get("search_term")
|
||||||
|
|
||||||
|
if search_term:
|
||||||
|
objects = objects.filter(Q(name__icontains=search_term))
|
||||||
|
|
||||||
if sort_by == "state_display":
|
if sort_by == "state_display":
|
||||||
# Fetch the objects and sort them in Python
|
# Fetch the objects and sort them in Python
|
||||||
|
@ -56,5 +62,6 @@ def get_domains_json(request):
|
||||||
"has_previous": page_obj.has_previous(),
|
"has_previous": page_obj.has_previous(),
|
||||||
"has_next": page_obj.has_next(),
|
"has_next": page_obj.has_next(),
|
||||||
"total": paginator.count,
|
"total": paginator.count,
|
||||||
|
"unfiltered_total": unfiltered_total,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from registrar.models import DomainRequest
|
|
||||||
from waffle.decorators import flag_is_active
|
from waffle.decorators import flag_is_active
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,46 +7,7 @@ def index(request):
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
# Get all domain requests the user has access to
|
|
||||||
domain_requests, deletable_domain_requests = _get_domain_requests(request)
|
|
||||||
|
|
||||||
context["domain_requests"] = domain_requests
|
|
||||||
|
|
||||||
# Determine if the user will see domain requests that they can delete
|
|
||||||
has_deletable_domain_requests = deletable_domain_requests.exists()
|
|
||||||
context["has_deletable_domain_requests"] = has_deletable_domain_requests
|
|
||||||
|
|
||||||
# This is a django waffle flag which toggles features based off of the "flag" table
|
# This is a django waffle flag which toggles features based off of the "flag" table
|
||||||
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||||
|
|
||||||
# If they can delete domain requests, add the delete button to the context
|
|
||||||
if has_deletable_domain_requests:
|
|
||||||
# Add the delete modal button to the context
|
|
||||||
modal_button = (
|
|
||||||
'<button type="submit" '
|
|
||||||
'class="usa-button usa-button--secondary" '
|
|
||||||
'name="delete-domain-request">Yes, delete request</button>'
|
|
||||||
)
|
|
||||||
context["modal_button"] = modal_button
|
|
||||||
|
|
||||||
return render(request, "home.html", context)
|
return render(request, "home.html", context)
|
||||||
|
|
||||||
|
|
||||||
def _get_domain_requests(request):
|
|
||||||
"""Given the current request,
|
|
||||||
get all DomainRequests that are associated with the UserDomainRole object.
|
|
||||||
|
|
||||||
Returns a tuple of all domain requests, and those that are deletable by the user.
|
|
||||||
"""
|
|
||||||
# Let's exclude the approved domain requests since our
|
|
||||||
# domain_requests context will be used to populate
|
|
||||||
# the active domain requests table
|
|
||||||
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
|
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a placeholder DraftDomain for each incomplete draft
|
|
||||||
valid_statuses = [DomainRequest.DomainRequestStatus.STARTED, DomainRequest.DomainRequestStatus.WITHDRAWN]
|
|
||||||
deletable_domain_requests = domain_requests.filter(status__in=valid_statuses)
|
|
||||||
|
|
||||||
return (domain_requests, deletable_domain_requests)
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue