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.
|
||||
|
||||
/**
|
||||
* Hide element
|
||||
*
|
||||
*/
|
||||
const hideElement = (element) => {
|
||||
element.classList.add('display-none');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show element
|
||||
*
|
||||
*/
|
||||
const showElement = (element) => {
|
||||
element.classList.remove('display-none');
|
||||
};
|
||||
|
||||
/** Makes an element invisible. */
|
||||
function makeHidden(el) {
|
||||
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} 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, 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 paginationCounter = document.querySelector(counterSelector);
|
||||
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
|
||||
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) {
|
||||
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
|
||||
|
@ -1025,13 +1083,21 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
|||
*
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let domainsWrapper = document.querySelector('.domains-wrapper');
|
||||
const domainsWrapper = document.querySelector('.domains__table-wrapper');
|
||||
|
||||
if (domainsWrapper) {
|
||||
let currentSortBy = 'id';
|
||||
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 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
|
||||
|
@ -1040,10 +1106,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
* @param {*} sortBy - the sort column option
|
||||
* @param {*} order - the sort order {asc, desc}
|
||||
* @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(`/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(data => {
|
||||
if (data.error) {
|
||||
|
@ -1051,23 +1118,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
return;
|
||||
}
|
||||
|
||||
// handle the display of proper messaging in the event that no domains exist in the list
|
||||
if (data.domains.length) {
|
||||
domainsWrapper.classList.remove('display-none');
|
||||
noDomainsWrapper.classList.add('display-none');
|
||||
} else {
|
||||
domainsWrapper.classList.add('display-none');
|
||||
noDomainsWrapper.classList.remove('display-none');
|
||||
}
|
||||
// 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, searchTermHolder, currentSearchTerm);
|
||||
|
||||
// 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 = '';
|
||||
|
||||
data.domains.forEach(domain => {
|
||||
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||
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 actionUrl = domain.action_url;
|
||||
|
||||
|
@ -1106,9 +1167,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
// initialize tool tips immediately after the associated DOM elements are added
|
||||
initializeTooltips();
|
||||
|
||||
// Do not scroll on first page load
|
||||
if (loaded)
|
||||
ScrollToElement('id', 'domains-header');
|
||||
|
||||
hasLoaded = true;
|
||||
|
||||
// update pagination
|
||||
|
@ -1122,18 +1184,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
data.num_pages,
|
||||
data.has_previous,
|
||||
data.has_next,
|
||||
data.total
|
||||
data.total,
|
||||
currentSearchTerm
|
||||
);
|
||||
currentSortBy = sortBy;
|
||||
currentOrder = order;
|
||||
currentSearchTerm = searchTerm;
|
||||
})
|
||||
.catch(error => console.error('Error fetching domains:', error));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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() {
|
||||
const sortBy = this.getAttribute('data-sortable');
|
||||
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
|
||||
loadDomains(1);
|
||||
}
|
||||
|
@ -1157,25 +1256,71 @@ const utcDateString = (dateString) => {
|
|||
const utcYear = date.getUTCFullYear();
|
||||
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
|
||||
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');
|
||||
|
||||
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.
|
||||
*
|
||||
*/
|
||||
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) {
|
||||
let currentSortBy = 'id';
|
||||
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 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
|
||||
|
@ -1184,10 +1329,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
* @param {*} sortBy - the sort column option
|
||||
* @param {*} order - the sort order {asc, desc}
|
||||
* @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(`/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(data => {
|
||||
if (data.error) {
|
||||
|
@ -1195,41 +1341,138 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
return;
|
||||
}
|
||||
|
||||
// handle the display of proper messaging in the event that no domain requests exist in the list
|
||||
if (data.domain_requests.length) {
|
||||
domainRequestsWrapper.classList.remove('display-none');
|
||||
noDomainRequestsWrapper.classList.add('display-none');
|
||||
} else {
|
||||
domainRequestsWrapper.classList.add('display-none');
|
||||
noDomainRequestsWrapper.classList.remove('display-none');
|
||||
}
|
||||
// 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, searchTermHolder, currentSearchTerm);
|
||||
|
||||
// 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 = '';
|
||||
|
||||
// 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 = `
|
||||
<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 => {
|
||||
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 actionUrl = request.action_url;
|
||||
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 deleteButton = request.is_deletable ? `
|
||||
<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>` : '';
|
||||
|
||||
// Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed
|
||||
let modalTrigger = '';
|
||||
|
||||
// If the request is deletable, create modal body and insert it
|
||||
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 = `
|
||||
<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');
|
||||
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>
|
||||
</a>
|
||||
</td>
|
||||
<td>${deleteButton}</td>
|
||||
${needsDeleteColumn ? '<td>'+modalTrigger+'</td>' : ''}
|
||||
`;
|
||||
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 (loaded)
|
||||
ScrollToElement('id', 'domain-requests-header');
|
||||
|
||||
hasLoaded = true;
|
||||
|
||||
// update the pagination after the domain requests list is updated
|
||||
|
@ -1272,16 +1536,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
data.num_pages,
|
||||
data.has_previous,
|
||||
data.has_next,
|
||||
data.total
|
||||
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
|
||||
document.querySelectorAll('.dotgov-table__domain-requests th[data-sortable]').forEach(header => {
|
||||
tableHeaders.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const sortBy = this.getAttribute('data-sortable');
|
||||
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
|
||||
loadDomainRequests(1);
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
}
|
||||
}
|
||||
@media (min-width: 1040px){
|
||||
.dotgov-table__domain-requests {
|
||||
.domain-requests__table {
|
||||
th:nth-of-type(1) {
|
||||
width: 200px;
|
||||
}
|
||||
|
@ -122,7 +122,7 @@
|
|||
}
|
||||
|
||||
@media (min-width: 1040px){
|
||||
.dotgov-table__registered-domains {
|
||||
.domains__table {
|
||||
th:nth-of-type(1) {
|
||||
width: 200px;
|
||||
}
|
||||
|
|
|
@ -23,10 +23,39 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
<section class="section--outlined">
|
||||
<h2 id="domains-header">Domains</h2>
|
||||
<div class="domains-wrapper display-none">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__registered-domains">
|
||||
<section class="section--outlined domains">
|
||||
<div class="grid-row">
|
||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||
<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>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -50,7 +79,7 @@
|
|||
aria-live="polite"
|
||||
></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 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">
|
||||
|
@ -61,6 +90,9 @@
|
|||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="domains__no-search-results display-none">
|
||||
<p>No results found for "<span class="domains__search-term"></span>"</p>
|
||||
</div>
|
||||
</section>
|
||||
<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">
|
||||
|
@ -71,10 +103,39 @@
|
|||
</ul>
|
||||
</nav>
|
||||
|
||||
<section class="section--outlined">
|
||||
<h2 id="domain-requests-header">Domain requests</h2>
|
||||
<div class="domain-requests-wrapper display-none">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__domain-requests">
|
||||
<section class="section--outlined domain-requests">
|
||||
<div class="grid-row">
|
||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||
<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>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -82,7 +143,7 @@
|
|||
<th data-sortable="submission_date" scope="col" role="columnheader">Date submitted</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">Delete Action</span></th>
|
||||
<!-- AJAX will conditionally add a th for delete actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="domain-requests-tbody">
|
||||
|
@ -93,45 +154,13 @@
|
|||
class="usa-sr-only usa-table__announcement-region"
|
||||
aria-live="polite"
|
||||
></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 class="no-domain-requests-wrapper display-none">
|
||||
<div class="domain-requests__no-data display-none">
|
||||
<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>
|
||||
<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">
|
||||
|
|
|
@ -384,15 +384,15 @@ class HomeTests(TestWithUser):
|
|||
)
|
||||
domain_request_2.other_contacts.set([contact_shared])
|
||||
|
||||
# Ensure that igorville.gov exists on the page
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
igorville = DomainRequest.objects.filter(requested_domain__name="igorville.gov")
|
||||
self.assertTrue(igorville.exists())
|
||||
|
||||
# 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
|
||||
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
|
||||
orphan = Contact.objects.filter(id=contact.id)
|
||||
|
@ -456,13 +456,14 @@ class HomeTests(TestWithUser):
|
|||
)
|
||||
domain_request_2.other_contacts.set([contact_shared])
|
||||
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "teaville.gov")
|
||||
teaville = DomainRequest.objects.filter(requested_domain__name="teaville.gov")
|
||||
self.assertTrue(teaville.exists())
|
||||
|
||||
# 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
|
||||
orphan = Contact.objects.filter(id=contact_shared.id)
|
||||
|
|
|
@ -102,6 +102,35 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
)
|
||||
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):
|
||||
"""Test that pagination is correct in the response"""
|
||||
response = self.app.get(reverse("get_domains_json"), {"page": 1})
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from registrar.models import DomainRequest
|
||||
from django.urls import reverse
|
||||
|
||||
from registrar.models.draft_domain import DraftDomain
|
||||
from .test_views import TestWithUser
|
||||
from django_webtest import WebTest # type: ignore
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
@ -10,32 +12,37 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
super().setUp()
|
||||
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
|
||||
self.domain_requests = [
|
||||
DomainRequest.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=None,
|
||||
requested_domain=lamb_chops,
|
||||
submission_date="2024-01-01",
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
created_at="2024-01-01",
|
||||
),
|
||||
DomainRequest.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=None,
|
||||
requested_domain=short_ribs,
|
||||
submission_date="2024-02-01",
|
||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
created_at="2024-02-01",
|
||||
),
|
||||
DomainRequest.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=None,
|
||||
requested_domain=beef_chuck,
|
||||
submission_date="2024-03-01",
|
||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||
created_at="2024-03-01",
|
||||
),
|
||||
DomainRequest.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=None,
|
||||
requested_domain=stew_beef,
|
||||
submission_date="2024-04-01",
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
created_at="2024-04-01",
|
||||
|
@ -195,6 +202,61 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
)
|
||||
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):
|
||||
"""Test that pagination works properly. There are 11 total non-approved requests and
|
||||
a page size of 10"""
|
||||
|
|
|
@ -798,7 +798,8 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
|||
contacts_to_delete, duplicates = self._get_orphaned_contacts(domain_request)
|
||||
|
||||
# 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
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -4,6 +4,7 @@ from registrar.models import DomainRequest
|
|||
from django.utils.dateformat import format
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -14,9 +15,27 @@ def get_domain_requests_json(request):
|
|||
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
|
||||
status=DomainRequest.DomainRequestStatus.APPROVED
|
||||
)
|
||||
unfiltered_total = domain_requests.count()
|
||||
|
||||
# Handle sorting
|
||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||
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":
|
||||
sort_by = f"-{sort_by}"
|
||||
domain_requests = domain_requests.order_by(sort_by)
|
||||
|
@ -75,5 +94,6 @@ def get_domain_requests_json(request):
|
|||
"page": page_obj.number,
|
||||
"num_pages": paginator.num_pages,
|
||||
"total": paginator.count,
|
||||
"unfiltered_total": unfiltered_total,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.core.paginator import Paginator
|
|||
from registrar.models import UserDomainRole, Domain
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -14,10 +15,15 @@ def get_domains_json(request):
|
|||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||
|
||||
objects = Domain.objects.filter(id__in=domain_ids)
|
||||
unfiltered_total = objects.count()
|
||||
|
||||
# Handle sorting
|
||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||
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":
|
||||
# Fetch the objects and sort them in Python
|
||||
|
@ -56,5 +62,6 @@ def get_domains_json(request):
|
|||
"has_previous": page_obj.has_previous(),
|
||||
"has_next": page_obj.has_next(),
|
||||
"total": paginator.count,
|
||||
"unfiltered_total": unfiltered_total,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.shortcuts import render
|
||||
from registrar.models import DomainRequest
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
|
||||
|
@ -8,46 +7,7 @@ def index(request):
|
|||
context = {}
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
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