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:
dave-kennedy-ecs 2024-06-17 11:28:40 -04:00 committed by GitHub
commit c0201f5fa3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 572 additions and 159 deletions

View file

@ -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);
} }

View file

@ -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;
} }

View file

@ -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">

View file

@ -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)

View file

@ -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})

View file

@ -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"""

View file

@ -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):
""" """

View file

@ -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,
} }
) )

View file

@ -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,
} }
) )

View file

@ -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)