mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-12 15:34:50 +02:00
Merge branch 'main' into za/2596-view-only-domain-request-page
This commit is contained in:
commit
8644b0d456
24 changed files with 1164 additions and 1024 deletions
|
@ -9,10 +9,10 @@ We use [django-waffle](https://waffle.readthedocs.io/en/stable/) for our feature
|
|||
3. Click `Add waffle flag`.
|
||||
4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them.
|
||||
|
||||
### Enabling the profile_feature flag
|
||||
### Enabling a feature flag
|
||||
1. On the app, navigate to `\admin`.
|
||||
2. Under models, click `Waffle flags`.
|
||||
3. Click the `profile_feature` record. This should exist by default, if not - create one with that name.
|
||||
3. Click the featue flag record. This should exist by default, if not - create one with that name.
|
||||
4. (Important) Set the field `Everyone` to `Unknown`. This field overrides all other settings when set to anything else.
|
||||
5. Configure the settings as you see fit.
|
||||
|
||||
|
|
|
@ -126,7 +126,15 @@ class AvailableAPITest(MockEppLib):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = get_user_model().objects.create(username="username")
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
title = "title"
|
||||
phone = "8080102431"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone
|
||||
)
|
||||
|
||||
def test_available_get(self):
|
||||
self.client.force_login(self.user)
|
||||
|
|
|
@ -1977,11 +1977,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
so we should display that information using this function.
|
||||
|
||||
"""
|
||||
|
||||
if hasattr(obj, "creator"):
|
||||
recipient = obj.creator
|
||||
else:
|
||||
recipient = None
|
||||
|
||||
# Displays a warning in admin when an email cannot be sent
|
||||
if recipient and recipient.email:
|
||||
|
@ -2186,7 +2182,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
||||
emails = self.get_all_action_needed_reason_emails(obj)
|
||||
extra_context["action_needed_reason_emails"] = json.dumps(emails)
|
||||
extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||
|
||||
# Denote if an action needed email was sent or not
|
||||
email_sent = request.session.get("action_needed_email_sent", False)
|
||||
|
|
|
@ -33,6 +33,14 @@ const showElement = (element) => {
|
|||
element.classList.remove('display-none');
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get the CSRF token from the cookie
|
||||
*
|
||||
*/
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that scrolls to an element
|
||||
* @param {string} attributeName - The string "class" or "id"
|
||||
|
@ -994,40 +1002,79 @@ function unloadModals() {
|
|||
window.modal.off();
|
||||
}
|
||||
|
||||
class LoadTableBase {
|
||||
constructor(tableSelector, tableWrapperSelector, searchFieldId, searchSubmitId, resetSearchBtn, resetFiltersBtn, noDataDisplay, noSearchresultsDisplay) {
|
||||
this.tableWrapper = document.querySelector(tableWrapperSelector);
|
||||
this.tableHeaders = document.querySelectorAll(`${tableSelector} th[data-sortable]`);
|
||||
this.currentSortBy = 'id';
|
||||
this.currentOrder = 'asc';
|
||||
this.currentStatus = [];
|
||||
this.currentSearchTerm = '';
|
||||
this.scrollToTable = false;
|
||||
this.searchInput = document.querySelector(searchFieldId);
|
||||
this.searchSubmit = document.querySelector(searchSubmitId);
|
||||
this.tableAnnouncementRegion = document.querySelector(`${tableWrapperSelector} .usa-table__announcement-region`);
|
||||
this.resetSearchButton = document.querySelector(resetSearchBtn);
|
||||
this.resetFiltersButton = document.querySelector(resetFiltersBtn);
|
||||
// NOTE: these 3 can't be used if filters are active on a page with more than 1 table
|
||||
this.statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
|
||||
this.statusIndicator = document.querySelector('.filter-indicator');
|
||||
this.statusToggle = document.querySelector('.usa-button--filter');
|
||||
this.noTableWrapper = document.querySelector(noDataDisplay);
|
||||
this.noSearchResultsWrapper = document.querySelector(noSearchresultsDisplay);
|
||||
this.portfolioElement = document.getElementById('portfolio-js-value');
|
||||
this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null;
|
||||
this.initializeTableHeaders();
|
||||
this.initializeSearchHandler();
|
||||
this.initializeStatusToggleHandler();
|
||||
this.initializeFilterCheckboxes();
|
||||
this.initializeResetSearchButton();
|
||||
this.initializeResetFiltersButton();
|
||||
this.initializeAccordionAccessibilityListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generalized function to update pagination for a list.
|
||||
* @param {string} itemName - The name displayed in the counter
|
||||
* @param {string} paginationSelector - CSS selector for the pagination container.
|
||||
* @param {string} counterSelector - CSS selector for the pagination counter.
|
||||
* @param {string} linkAnchor - CSS selector for the header element to anchor the links to.
|
||||
* @param {Function} loadPageFunction - Function to call when a page link is clicked.
|
||||
* @param {string} tableSelector - CSS selector for the header element to anchor the links to.
|
||||
* @param {number} currentPage - The current page number (starting with 1).
|
||||
* @param {number} numPages - The total number of pages.
|
||||
* @param {boolean} hasPrevious - Whether there is a page before the current page.
|
||||
* @param {boolean} hasNext - Whether there is a page after the current page.
|
||||
* @param {number} totalItems - The total number of items.
|
||||
* @param {string} searchTerm - The search term
|
||||
* @param {number} total - The total number of items.
|
||||
*/
|
||||
function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
|
||||
const paginationContainer = document.querySelector(paginationSelector);
|
||||
const paginationCounter = document.querySelector(counterSelector);
|
||||
updatePagination(
|
||||
itemName,
|
||||
paginationSelector,
|
||||
counterSelector,
|
||||
parentTableSelector,
|
||||
currentPage,
|
||||
numPages,
|
||||
hasPrevious,
|
||||
hasNext,
|
||||
totalItems,
|
||||
) {
|
||||
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
||||
paginationCounter.innerHTML = '';
|
||||
const counterSelectorEl = document.querySelector(counterSelector);
|
||||
const paginationSelectorEl = document.querySelector(paginationSelector);
|
||||
counterSelectorEl.innerHTML = '';
|
||||
paginationButtons.innerHTML = '';
|
||||
|
||||
// Buttons should only be displayed if there are more than one pages of results
|
||||
paginationButtons.classList.toggle('display-none', numPages <= 1);
|
||||
|
||||
// Counter should only be displayed if there is more than 1 item
|
||||
paginationContainer.classList.toggle('display-none', totalItems < 1);
|
||||
paginationSelectorEl.classList.toggle('display-none', totalItems < 1);
|
||||
|
||||
paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${searchTerm ? ' for ' + '"' + searchTerm + '"' : ''}`;
|
||||
counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`;
|
||||
|
||||
if (hasPrevious) {
|
||||
const prevPageItem = document.createElement('li');
|
||||
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||
prevPageItem.innerHTML = `
|
||||
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
||||
<a href="${parentTableSelector}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
||||
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||
<use xlink:href="/public/img/sprite.svg#navigate_before"></use>
|
||||
</svg>
|
||||
|
@ -1036,32 +1083,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
|||
`;
|
||||
prevPageItem.querySelector('a').addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
loadPageFunction(currentPage - 1);
|
||||
this.loadTable(currentPage - 1);
|
||||
});
|
||||
paginationButtons.appendChild(prevPageItem);
|
||||
}
|
||||
|
||||
// Helper function to create a page item
|
||||
function createPageItem(page) {
|
||||
const pageItem = document.createElement('li');
|
||||
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
||||
pageItem.innerHTML = `
|
||||
<a href="${linkAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
||||
`;
|
||||
if (page === currentPage) {
|
||||
pageItem.querySelector('a').classList.add('usa-current');
|
||||
pageItem.querySelector('a').setAttribute('aria-current', 'page');
|
||||
}
|
||||
pageItem.querySelector('a').addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
loadPageFunction(page);
|
||||
});
|
||||
return pageItem;
|
||||
}
|
||||
|
||||
// Add first page and ellipsis if necessary
|
||||
if (currentPage > 2) {
|
||||
paginationButtons.appendChild(createPageItem(1));
|
||||
paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage));
|
||||
if (currentPage > 3) {
|
||||
const ellipsis = document.createElement('li');
|
||||
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
||||
|
@ -1073,7 +1102,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
|||
|
||||
// Add pages around the current page
|
||||
for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) {
|
||||
paginationButtons.appendChild(createPageItem(i));
|
||||
paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage));
|
||||
}
|
||||
|
||||
// Add last page and ellipsis if necessary
|
||||
|
@ -1085,14 +1114,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
|||
ellipsis.innerHTML = '<span>…</span>';
|
||||
paginationButtons.appendChild(ellipsis);
|
||||
}
|
||||
paginationButtons.appendChild(createPageItem(numPages));
|
||||
paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage));
|
||||
}
|
||||
|
||||
if (hasNext) {
|
||||
const nextPageItem = document.createElement('li');
|
||||
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||
nextPageItem.innerHTML = `
|
||||
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
||||
<a href="${parentTableSelector}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
||||
<span class="usa-pagination__link-text">Next</span>
|
||||
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||
<use xlink:href="/public/img/sprite.svg#navigate_next"></use>
|
||||
|
@ -1101,7 +1130,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
|||
`;
|
||||
nextPageItem.querySelector('a').addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
loadPageFunction(currentPage + 1);
|
||||
this.loadTable(currentPage + 1);
|
||||
});
|
||||
paginationButtons.appendChild(nextPageItem);
|
||||
}
|
||||
|
@ -1111,7 +1140,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
|||
* A helper that toggles content/ no content/ no search results
|
||||
*
|
||||
*/
|
||||
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
||||
updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
||||
const { unfiltered_total, total } = data;
|
||||
if (unfiltered_total) {
|
||||
if (total) {
|
||||
|
@ -1130,11 +1159,29 @@ const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper)
|
|||
}
|
||||
};
|
||||
|
||||
// Helper function to create a page item
|
||||
createPageItem(page, parentTableSelector, currentPage) {
|
||||
const pageItem = document.createElement('li');
|
||||
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
||||
pageItem.innerHTML = `
|
||||
<a href="${parentTableSelector}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
||||
`;
|
||||
if (page === currentPage) {
|
||||
pageItem.querySelector('a').classList.add('usa-current');
|
||||
pageItem.querySelector('a').setAttribute('aria-current', 'page');
|
||||
}
|
||||
pageItem.querySelector('a').addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
this.loadTable(page);
|
||||
});
|
||||
return pageItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that resets sortable table headers
|
||||
*
|
||||
*/
|
||||
const unsetHeader = (header) => {
|
||||
unsetHeader = (header) => {
|
||||
header.removeAttribute('aria-sort');
|
||||
let headerName = header.innerText;
|
||||
const headerLabel = `${headerName}, sortable column, currently unsorted"`;
|
||||
|
@ -1143,34 +1190,181 @@ const unsetHeader = (header) => {
|
|||
header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel);
|
||||
};
|
||||
|
||||
/**
|
||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||
* initializes the domains list and associated functionality on the home page of the app.
|
||||
*
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const domainsWrapper = document.querySelector('.domains__table-wrapper');
|
||||
// Abstract method (to be implemented in the child class)
|
||||
loadTable(page, sortBy, order) {
|
||||
throw new Error('loadData() must be implemented in a subclass');
|
||||
}
|
||||
|
||||
if (domainsWrapper) {
|
||||
let currentSortBy = 'id';
|
||||
let currentOrder = 'asc';
|
||||
const noDomainsWrapper = document.querySelector('.domains__no-data');
|
||||
const noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
|
||||
let scrollToTable = false;
|
||||
let currentStatus = [];
|
||||
let currentSearchTerm = '';
|
||||
const domainsSearchInput = document.getElementById('domains__search-field');
|
||||
const domainsSearchSubmit = document.getElementById('domains__search-field-submit');
|
||||
const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]');
|
||||
const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region');
|
||||
const resetSearchButton = document.querySelector('.domains__reset-search');
|
||||
const resetFiltersButton = document.querySelector('.domains__reset-filters');
|
||||
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
|
||||
const statusIndicator = document.querySelector('.domain__filter-indicator');
|
||||
const statusToggle = document.querySelector('.usa-button--filter');
|
||||
const portfolioElement = document.getElementById('portfolio-js-value');
|
||||
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
|
||||
// Add event listeners to table headers for sorting
|
||||
initializeTableHeaders() {
|
||||
this.tableHeaders.forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const sortBy = header.getAttribute('data-sortable');
|
||||
let order = 'asc';
|
||||
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||
// is selecting the same column to sort in descending order
|
||||
if (sortBy === this.currentSortBy) {
|
||||
order = this.currentOrder === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
// load the results with the updated sort
|
||||
this.loadTable(1, sortBy, order);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initializeSearchHandler() {
|
||||
this.searchSubmit.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.currentSearchTerm = this.searchInput.value;
|
||||
// If the search is blank, we match the resetSearch functionality
|
||||
if (this.currentSearchTerm) {
|
||||
showElement(this.resetSearchButton);
|
||||
} else {
|
||||
hideElement(this.resetSearchButton);
|
||||
}
|
||||
this.loadTable(1, 'id', 'asc');
|
||||
this.resetHeaders();
|
||||
});
|
||||
}
|
||||
|
||||
initializeStatusToggleHandler() {
|
||||
if (this.statusToggle) {
|
||||
this.statusToggle.addEventListener('click', () => {
|
||||
toggleCaret(this.statusToggle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listeners to status filter checkboxes
|
||||
initializeFilterCheckboxes() {
|
||||
this.statusCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', () => {
|
||||
const checkboxValue = checkbox.value;
|
||||
|
||||
// Update currentStatus array based on checkbox state
|
||||
if (checkbox.checked) {
|
||||
this.currentStatus.push(checkboxValue);
|
||||
} else {
|
||||
const index = this.currentStatus.indexOf(checkboxValue);
|
||||
if (index > -1) {
|
||||
this.currentStatus.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Manage visibility of reset filters button
|
||||
if (this.currentStatus.length == 0) {
|
||||
hideElement(this.resetFiltersButton);
|
||||
} else {
|
||||
showElement(this.resetFiltersButton);
|
||||
}
|
||||
|
||||
// Disable the auto scroll
|
||||
this.scrollToTable = false;
|
||||
|
||||
// Call loadTable with updated status
|
||||
this.loadTable(1, 'id', 'asc');
|
||||
this.resetHeaders();
|
||||
this.updateStatusIndicator();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Reset UI and accessibility
|
||||
resetHeaders() {
|
||||
this.tableHeaders.forEach(header => {
|
||||
// Unset sort UI in headers
|
||||
this.unsetHeader(header);
|
||||
});
|
||||
// Reset the announcement region
|
||||
this.tableAnnouncementRegion.innerHTML = '';
|
||||
}
|
||||
|
||||
resetSearch() {
|
||||
this.searchInput.value = '';
|
||||
this.currentSearchTerm = '';
|
||||
hideElement(this.resetSearchButton);
|
||||
this.loadTable(1, 'id', 'asc');
|
||||
this.resetHeaders();
|
||||
}
|
||||
|
||||
initializeResetSearchButton() {
|
||||
if (this.resetSearchButton) {
|
||||
this.resetSearchButton.addEventListener('click', () => {
|
||||
this.resetSearch();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resetFilters() {
|
||||
this.currentStatus = [];
|
||||
this.statusCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
hideElement(this.resetFiltersButton);
|
||||
|
||||
// Disable the auto scroll
|
||||
this.scrollToTable = false;
|
||||
|
||||
this.loadTable(1, 'id', 'asc');
|
||||
this.resetHeaders();
|
||||
this.updateStatusIndicator();
|
||||
// No need to toggle close the filters. The focus shift will trigger that for us.
|
||||
}
|
||||
|
||||
initializeResetFiltersButton() {
|
||||
if (this.resetFiltersButton) {
|
||||
this.resetFiltersButton.addEventListener('click', () => {
|
||||
this.resetFilters();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusIndicator() {
|
||||
this.statusIndicator.innerHTML = '';
|
||||
// Even if the element is empty, it'll mess up the flex layout unless we set display none
|
||||
hideElement(this.statusIndicator);
|
||||
if (this.currentStatus.length)
|
||||
this.statusIndicator.innerHTML = '(' + this.currentStatus.length + ')';
|
||||
showElement(this.statusIndicator);
|
||||
}
|
||||
|
||||
closeFilters() {
|
||||
if (this.statusToggle.getAttribute("aria-expanded") === "true") {
|
||||
this.statusToggle.click();
|
||||
}
|
||||
}
|
||||
|
||||
initializeAccordionAccessibilityListeners() {
|
||||
// Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter,
|
||||
// user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button.
|
||||
// NOTE: We may need to evolve this as we add more filters.
|
||||
document.addEventListener('focusin', (event) => {
|
||||
const accordion = document.querySelector('.usa-accordion--select');
|
||||
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
|
||||
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
||||
this.closeFilters();
|
||||
}
|
||||
});
|
||||
|
||||
// Close when user clicks outside
|
||||
// NOTE: We may need to evolve this as we add more filters.
|
||||
document.addEventListener('click', (event) => {
|
||||
const accordion = document.querySelector('.usa-accordion--select');
|
||||
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
|
||||
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
||||
this.closeFilters();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DomainsTable extends LoadTableBase {
|
||||
|
||||
constructor() {
|
||||
super('.domains__table', '.domains__table-wrapper', '#domains__search-field', '#domains__search-field-submit', '.domains__reset-search', '.domains__reset-filters', '.domains__no-data', '.domains__no-search-results');
|
||||
}
|
||||
/**
|
||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||
* based on the supplied attributes.
|
||||
|
@ -1178,10 +1372,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
* @param {*} sortBy - the sort column option
|
||||
* @param {*} order - the sort order {asc, desc}
|
||||
* @param {*} scroll - control for the scrollToElement functionality
|
||||
* @param {*} status - control for the status filter
|
||||
* @param {*} searchTerm - the search term
|
||||
* @param {*} portfolio - the portfolio id
|
||||
*/
|
||||
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
|
||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||
|
||||
// fetch json of page of domais, given params
|
||||
let baseUrl = document.getElementById("get_domains_json_url");
|
||||
if (!baseUrl) {
|
||||
|
@ -1194,10 +1390,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
|
||||
// fetch json of page of domains, given params
|
||||
let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`
|
||||
let searchParams = new URLSearchParams(
|
||||
{
|
||||
"page": page,
|
||||
"sort_by": sortBy,
|
||||
"order": order,
|
||||
"status": status,
|
||||
"search_term": searchTerm
|
||||
}
|
||||
);
|
||||
if (portfolio)
|
||||
url += `&portfolio=${portfolio}`
|
||||
searchParams.append("portfolio", portfolio)
|
||||
|
||||
let url = `${baseUrlValue}?${searchParams.toString()}`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
|
@ -1207,7 +1412,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
|
||||
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results
|
||||
updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, currentSearchTerm);
|
||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||
|
||||
// identify the DOM element where the domain list will be inserted into the DOM
|
||||
const domainList = document.querySelector('.domains__table tbody');
|
||||
|
@ -1225,7 +1430,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
let markupForSuborganizationRow = '';
|
||||
|
||||
if (portfolioValue) {
|
||||
if (this.portfolioValue) {
|
||||
markupForSuborganizationRow = `
|
||||
<td>
|
||||
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
||||
|
@ -1271,271 +1476,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// Do not scroll on first page load
|
||||
if (scroll)
|
||||
ScrollToElement('class', 'domains');
|
||||
scrollToTable = true;
|
||||
this.scrollToTable = true;
|
||||
|
||||
// update pagination
|
||||
updatePagination(
|
||||
this.updatePagination(
|
||||
'domain',
|
||||
'#domains-pagination',
|
||||
'#domains-pagination .usa-pagination__counter',
|
||||
'#domains',
|
||||
loadDomains,
|
||||
data.page,
|
||||
data.num_pages,
|
||||
data.has_previous,
|
||||
data.has_next,
|
||||
data.total,
|
||||
currentSearchTerm
|
||||
);
|
||||
currentSortBy = sortBy;
|
||||
currentOrder = order;
|
||||
currentSearchTerm = searchTerm;
|
||||
this.currentSortBy = sortBy;
|
||||
this.currentOrder = order;
|
||||
this.currentSearchTerm = searchTerm;
|
||||
})
|
||||
.catch(error => console.error('Error fetching domains:', error));
|
||||
}
|
||||
|
||||
// Add event listeners to table headers for sorting
|
||||
tableHeaders.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const sortBy = this.getAttribute('data-sortable');
|
||||
let order = 'asc';
|
||||
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||
// is selecting the same column to sort in descending order
|
||||
if (sortBy === currentSortBy) {
|
||||
order = currentOrder === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
// load the results with the updated sort
|
||||
loadDomains(1, sortBy, order);
|
||||
});
|
||||
});
|
||||
|
||||
domainsSearchSubmit.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
currentSearchTerm = domainsSearchInput.value;
|
||||
// If the search is blank, we match the resetSearch functionality
|
||||
if (currentSearchTerm) {
|
||||
showElement(resetSearchButton);
|
||||
} else {
|
||||
hideElement(resetSearchButton);
|
||||
}
|
||||
loadDomains(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
});
|
||||
|
||||
if (statusToggle) {
|
||||
statusToggle.addEventListener('click', function() {
|
||||
toggleCaret(statusToggle);
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listeners to status filter checkboxes
|
||||
statusCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const checkboxValue = this.value;
|
||||
|
||||
// Update currentStatus array based on checkbox state
|
||||
if (this.checked) {
|
||||
currentStatus.push(checkboxValue);
|
||||
} else {
|
||||
const index = currentStatus.indexOf(checkboxValue);
|
||||
if (index > -1) {
|
||||
currentStatus.splice(index, 1);
|
||||
class DomainRequestsTable extends LoadTableBase {
|
||||
|
||||
constructor() {
|
||||
super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results');
|
||||
}
|
||||
}
|
||||
|
||||
// Manage visibility of reset filters button
|
||||
if (currentStatus.length == 0) {
|
||||
hideElement(resetFiltersButton);
|
||||
} else {
|
||||
showElement(resetFiltersButton);
|
||||
}
|
||||
|
||||
// Disable the auto scroll
|
||||
scrollToTable = false;
|
||||
|
||||
// Call loadDomains with updated status
|
||||
loadDomains(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
updateStatusIndicator();
|
||||
});
|
||||
});
|
||||
|
||||
// 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(resetSearchButton);
|
||||
loadDomains(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
}
|
||||
|
||||
if (resetSearchButton) {
|
||||
resetSearchButton.addEventListener('click', function() {
|
||||
resetSearch();
|
||||
});
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
currentStatus = [];
|
||||
statusCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
hideElement(resetFiltersButton);
|
||||
|
||||
// Disable the auto scroll
|
||||
scrollToTable = false;
|
||||
|
||||
loadDomains(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
updateStatusIndicator();
|
||||
// No need to toggle close the filters. The focus shift will trigger that for us.
|
||||
}
|
||||
|
||||
if (resetFiltersButton) {
|
||||
resetFiltersButton.addEventListener('click', function() {
|
||||
resetFilters();
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatusIndicator() {
|
||||
statusIndicator.innerHTML = '';
|
||||
// Even if the element is empty, it'll mess up the flex layout unless we set display none
|
||||
statusIndicator.hideElement();
|
||||
if (currentStatus.length)
|
||||
statusIndicator.innerHTML = '(' + currentStatus.length + ')';
|
||||
statusIndicator.showElement();
|
||||
}
|
||||
|
||||
function closeFilters() {
|
||||
if (statusToggle.getAttribute("aria-expanded") === "true") {
|
||||
statusToggle.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter,
|
||||
// user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button.
|
||||
// NOTE: We may need to evolve this as we add more filters.
|
||||
document.addEventListener('focusin', function(event) {
|
||||
const accordion = document.querySelector('.usa-accordion--select');
|
||||
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
|
||||
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
||||
closeFilters();
|
||||
}
|
||||
});
|
||||
|
||||
// Close when user clicks outside
|
||||
// NOTE: We may need to evolve this as we add more filters.
|
||||
document.addEventListener('click', function(event) {
|
||||
const accordion = document.querySelector('.usa-accordion--select');
|
||||
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
|
||||
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
||||
closeFilters();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadDomains(1);
|
||||
}
|
||||
});
|
||||
|
||||
const utcDateString = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const utcYear = date.getUTCFullYear();
|
||||
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
|
||||
const utcDay = date.getUTCDate().toString().padStart(2, '0');
|
||||
let utcHours = date.getUTCHours();
|
||||
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
|
||||
|
||||
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
|
||||
* initializes the domain requests list and associated functionality on the home page of the app.
|
||||
*
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const domainRequestsSectionWrapper = document.querySelector('.domain-requests');
|
||||
const domainRequestsWrapper = document.querySelector('.domain-requests__table-wrapper');
|
||||
|
||||
if (domainRequestsWrapper) {
|
||||
let currentSortBy = 'id';
|
||||
let currentOrder = 'asc';
|
||||
const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data');
|
||||
const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results');
|
||||
let scrollToTable = false;
|
||||
let currentSearchTerm = '';
|
||||
const domainRequestsSearchInput = document.getElementById('domain-requests__search-field');
|
||||
const domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit');
|
||||
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
|
||||
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
|
||||
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
|
||||
const portfolioElement = document.getElementById('portfolio-js-value');
|
||||
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
|
||||
|
||||
/**
|
||||
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
|
||||
* @param {*} domainRequestPk - the identifier for the request that we're deleting
|
||||
* @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page
|
||||
*/
|
||||
function deleteDomainRequest(domainRequestPk,pageToDisplay) {
|
||||
|
||||
// Use to debug uswds modal issues
|
||||
//console.log('deleteDomainRequest')
|
||||
|
||||
// Get csrf token
|
||||
const csrfToken = getCsrfToken();
|
||||
// Create FormData object and append the CSRF token
|
||||
const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`;
|
||||
|
||||
fetch(`/domain-request/${domainRequestPk}/delete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
// Update data and UI
|
||||
loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, scrollToTable, currentSearchTerm);
|
||||
})
|
||||
.catch(error => console.error('Error fetching domain requests:', error));
|
||||
}
|
||||
|
||||
// Helper function to get the CSRF token from the cookie
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads rows in the domain requests list, as well as updates pagination around the domain requests list
|
||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||
* based on the supplied attributes.
|
||||
* @param {*} page - the page number of the results (starts with 1)
|
||||
* @param {*} sortBy - the sort column option
|
||||
* @param {*} order - the sort order {asc, desc}
|
||||
* @param {*} scroll - control for the scrollToElement functionality
|
||||
* @param {*} status - control for the status filter
|
||||
* @param {*} searchTerm - the search term
|
||||
* @param {*} portfolio - the portfolio id
|
||||
*/
|
||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
|
||||
// fetch json of page of domain requests, given params
|
||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
||||
if (!baseUrl) {
|
||||
return;
|
||||
|
@ -1546,11 +1526,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
return;
|
||||
}
|
||||
|
||||
// fetch json of page of requests, given params
|
||||
let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`
|
||||
// add searchParams
|
||||
let searchParams = new URLSearchParams(
|
||||
{
|
||||
"page": page,
|
||||
"sort_by": sortBy,
|
||||
"order": order,
|
||||
"status": status,
|
||||
"search_term": searchTerm
|
||||
}
|
||||
);
|
||||
if (portfolio)
|
||||
url += `&portfolio=${portfolio}`
|
||||
searchParams.append("portfolio", portfolio)
|
||||
|
||||
let url = `${baseUrlValue}?${searchParams.toString()}`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
|
@ -1560,7 +1549,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
|
||||
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
||||
updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, currentSearchTerm);
|
||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||
|
||||
// identify the DOM element where the domain request list will be inserted into the DOM
|
||||
const tbody = document.querySelector('.domain-requests__table tbody');
|
||||
|
@ -1613,7 +1602,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
let markupCreatorRow = '';
|
||||
|
||||
if (portfolioValue) {
|
||||
if (this.portfolioValue) {
|
||||
markupCreatorRow = `
|
||||
<td>
|
||||
<span class="text-wrap break-word">${request.creator ? request.creator : ''}</span>
|
||||
|
@ -1708,10 +1697,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
</div>
|
||||
`
|
||||
|
||||
domainRequestsSectionWrapper.appendChild(modal);
|
||||
this.tableWrapper.appendChild(modal);
|
||||
|
||||
// Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly
|
||||
if (portfolioValue) {
|
||||
if (this.portfolioValue) {
|
||||
modalTrigger = `
|
||||
<a
|
||||
role="button"
|
||||
|
@ -1794,8 +1783,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
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');
|
||||
submitButton.addEventListener('click', () => {
|
||||
let pk = submitButton.getAttribute('data-pk');
|
||||
// Close the modal to remove the USWDS UI local classes
|
||||
closeButton.click();
|
||||
// If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page
|
||||
|
@ -1803,90 +1792,95 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (data.total == 1 && data.unfiltered_total > 1) {
|
||||
pageToDisplay--;
|
||||
}
|
||||
deleteDomainRequest(pk, pageToDisplay);
|
||||
this.deleteDomainRequest(pk, pageToDisplay);
|
||||
});
|
||||
});
|
||||
|
||||
// Do not scroll on first page load
|
||||
if (scroll)
|
||||
ScrollToElement('class', 'domain-requests');
|
||||
scrollToTable = true;
|
||||
this.scrollToTable = true;
|
||||
|
||||
// update the pagination after the domain requests list is updated
|
||||
updatePagination(
|
||||
this.updatePagination(
|
||||
'domain request',
|
||||
'#domain-requests-pagination',
|
||||
'#domain-requests-pagination .usa-pagination__counter',
|
||||
'#domain-requests',
|
||||
loadDomainRequests,
|
||||
data.page,
|
||||
data.num_pages,
|
||||
data.has_previous,
|
||||
data.has_next,
|
||||
data.total,
|
||||
currentSearchTerm
|
||||
);
|
||||
currentSortBy = sortBy;
|
||||
currentOrder = order;
|
||||
currentSearchTerm = searchTerm;
|
||||
this.currentSortBy = sortBy;
|
||||
this.currentOrder = order;
|
||||
this.currentSearchTerm = searchTerm;
|
||||
})
|
||||
.catch(error => console.error('Error fetching domain requests:', error));
|
||||
}
|
||||
|
||||
// Add event listeners to table headers for sorting
|
||||
tableHeaders.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const sortBy = this.getAttribute('data-sortable');
|
||||
let order = 'asc';
|
||||
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||
// is selecting the same column to sort in descending order
|
||||
if (sortBy === currentSortBy) {
|
||||
order = currentOrder === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
loadDomainRequests(1, sortBy, order);
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
|
||||
* @param {*} domainRequestPk - the identifier for the request that we're deleting
|
||||
* @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page
|
||||
*/
|
||||
deleteDomainRequest(domainRequestPk, pageToDisplay) {
|
||||
// Use to debug uswds modal issues
|
||||
//console.log('deleteDomainRequest')
|
||||
|
||||
domainRequestsSearchSubmit.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
currentSearchTerm = domainRequestsSearchInput.value;
|
||||
// If the search is blank, we match the resetSearch functionality
|
||||
if (currentSearchTerm) {
|
||||
showElement(resetSearchButton);
|
||||
} else {
|
||||
hideElement(resetSearchButton);
|
||||
}
|
||||
loadDomainRequests(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
});
|
||||
// Get csrf token
|
||||
const csrfToken = getCsrfToken();
|
||||
// Create FormData object and append the CSRF token
|
||||
const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`;
|
||||
|
||||
// Reset UI and accessibility
|
||||
function resetHeaders() {
|
||||
tableHeaders.forEach(header => {
|
||||
// unset sort UI in headers
|
||||
unsetHeader(header);
|
||||
});
|
||||
// Reset the announcement region
|
||||
tableAnnouncementRegion.innerHTML = '';
|
||||
fetch(`/domain-request/${domainRequestPk}/delete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
// Update data and UI
|
||||
this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm);
|
||||
})
|
||||
.catch(error => console.error('Error fetching domain requests:', error));
|
||||
}
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
domainRequestsSearchInput.value = '';
|
||||
currentSearchTerm = '';
|
||||
hideElement(resetSearchButton);
|
||||
loadDomainRequests(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
}
|
||||
|
||||
if (resetSearchButton) {
|
||||
resetSearchButton.addEventListener('click', function() {
|
||||
resetSearch();
|
||||
/**
|
||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||
* initializes the domains list and associated functionality on the home page of the app.
|
||||
*
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const isDomainsPage = document.querySelector("#domains")
|
||||
if (isDomainsPage){
|
||||
const domainsTable = new DomainsTable();
|
||||
if (domainsTable.tableWrapper) {
|
||||
// Initial load
|
||||
domainsTable.loadTable(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeMoreActionMenu(accordionThatIsOpen) {
|
||||
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
|
||||
accordionThatIsOpen.click();
|
||||
/**
|
||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||
* initializes the domain requests list and associated functionality on the home page of the app.
|
||||
*
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const domainRequestsSectionWrapper = document.querySelector('.domain-requests');
|
||||
if (domainRequestsSectionWrapper) {
|
||||
const domainRequestsTable = new DomainRequestsTable();
|
||||
if (domainRequestsTable.tableWrapper) {
|
||||
domainRequestsTable.loadTable(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1898,6 +1892,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
closeOpenAccordions(event);
|
||||
});
|
||||
|
||||
function closeMoreActionMenu(accordionThatIsOpen) {
|
||||
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
|
||||
accordionThatIsOpen.click();
|
||||
}
|
||||
}
|
||||
|
||||
function closeOpenAccordions(event) {
|
||||
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
|
||||
openAccordions.forEach((openAccordionButton) => {
|
||||
|
@ -1909,12 +1909,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial load
|
||||
loadDomainRequests(1);
|
||||
}
|
||||
});
|
||||
|
||||
const utcDateString = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const utcYear = date.getUTCFullYear();
|
||||
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
|
||||
const utcDay = date.getUTCDate().toString().padStart(2, '0');
|
||||
let utcHours = date.getUTCHours();
|
||||
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
|
||||
|
||||
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 displays confirmation modal on the user profile page
|
||||
|
|
|
@ -245,7 +245,6 @@ TEMPLATES = [
|
|||
"registrar.context_processors.is_production",
|
||||
"registrar.context_processors.org_user_status",
|
||||
"registrar.context_processors.add_path_to_context",
|
||||
"registrar.context_processors.add_has_profile_feature_flag_to_context",
|
||||
"registrar.context_processors.portfolio_permissions",
|
||||
],
|
||||
},
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.conf import settings
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
|
||||
def language_code(request):
|
||||
|
@ -54,10 +53,6 @@ def add_path_to_context(request):
|
|||
return {"path": getattr(request, "path", None)}
|
||||
|
||||
|
||||
def add_has_profile_feature_flag_to_context(request):
|
||||
return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")}
|
||||
|
||||
|
||||
def portfolio_permissions(request):
|
||||
"""Make portfolio permissions for the request user available in global context"""
|
||||
portfolio_context = {
|
||||
|
|
|
@ -9,7 +9,7 @@ class WaffleFlag(AbstractUserFlag):
|
|||
Custom implementation of django-waffles 'Flag' object.
|
||||
Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html
|
||||
|
||||
Use this class when dealing with feature flags, such as profile_feature.
|
||||
Use this class when dealing with feature flags.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -72,12 +72,6 @@ class CheckUserProfileMiddleware:
|
|||
"""Runs pre-processing logic for each view. Checks for the
|
||||
finished_setup flag on the current user. If they haven't done so,
|
||||
then we redirect them to the finish setup page."""
|
||||
# Check that the user is "opted-in" to the profile feature flag
|
||||
has_profile_feature_flag = flag_is_active(request, "profile_feature")
|
||||
|
||||
# If they aren't, skip this check entirely
|
||||
if not has_profile_feature_flag:
|
||||
return None
|
||||
|
||||
if request.user.is_authenticated:
|
||||
profile_page = self.profile_page
|
||||
|
|
|
@ -83,13 +83,6 @@
|
|||
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# Conditionally display profile #}
|
||||
{% if not has_profile_feature_flag %}
|
||||
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
{% if security_email is not None and security_email not in hidden_security_emails%}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
|
||||
|
|
|
@ -16,11 +16,9 @@
|
|||
<h2>Time to complete the form</h2>
|
||||
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
|
||||
completing your domain request might take around 15 minutes.</p>
|
||||
{% if has_profile_feature_flag %}
|
||||
<h2>How we’ll reach you</h2>
|
||||
<p>While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review. If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:" class="usa-link">your profile</a> to make updates.</p>
|
||||
{% include "includes/profile_information.html" with user=user%}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% block form_buttons %}
|
||||
|
|
|
@ -23,13 +23,21 @@
|
|||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
{% if portfolio %}
|
||||
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name or creator</label>
|
||||
{% else %}
|
||||
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
||||
{% endif %}
|
||||
<input
|
||||
class="usa-input"
|
||||
id="domain-requests__search-field"
|
||||
type="search"
|
||||
name="search"
|
||||
{% if portfolio %}
|
||||
placeholder="Search by domain name or creator"
|
||||
{% else %}
|
||||
placeholder="Search by domain name"
|
||||
{% endif %}
|
||||
/>
|
||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
||||
<img
|
||||
|
@ -42,6 +50,125 @@
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% if portfolio %}
|
||||
<div class="display-flex flex-align-center">
|
||||
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
|
||||
<div class="usa-accordion usa-accordion--select margin-right-2">
|
||||
<div class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--small padding--8-8-9 usa-button--outline usa-button--filter usa-accordion__button"
|
||||
aria-expanded="false"
|
||||
aria-controls="filter-status"
|
||||
>
|
||||
<span class="filter-indicator text-bold display-none"></span> Status
|
||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="filter-status" class="usa-accordion__content usa-prose shadow-1">
|
||||
<h2>Status</h2>
|
||||
<fieldset class="usa-fieldset margin-top-0">
|
||||
<legend class="usa-legend">Select to apply <span class="sr-only">status</span> filter</legend>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-started"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="started"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-started"
|
||||
>Started</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-submitted"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="submitted"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-submitted"
|
||||
>Submitted</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-in-review"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="in review"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-in-review"
|
||||
>In review</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-action-needed"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="action needed"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-action-needed"
|
||||
>Action needed</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-rejected"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="rejected"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-rejected"
|
||||
>Rejected</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-withdrawn"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="withdrawn"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-withdrawn"
|
||||
>Withdrawn</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-ineligible"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="ineligible"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-ineligible"
|
||||
>Ineligible</label
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--small padding--8-12-9-12 usa-button--outline usa-button--filter domain-requests__reset-filters display-none"
|
||||
>
|
||||
Clear filters
|
||||
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
|
||||
<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>
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
aria-expanded="false"
|
||||
aria-controls="filter-status"
|
||||
>
|
||||
<span class="domain__filter-indicator text-bold display-none"></span> Status
|
||||
<span class="filter-indicator text-bold display-none"></span> Status
|
||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||
</svg>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
{% if user.is_authenticated %}
|
||||
<span class="usa-nav__username ellipsis">{{ user.email }}</span>
|
||||
</li>
|
||||
{% if has_profile_feature_flag %}
|
||||
<li class="usa-nav__primary-item">
|
||||
{% url 'user-profile' as user_profile_url %}
|
||||
{% url 'finish-user-profile-setup' as finish_setup_url %}
|
||||
|
@ -24,7 +23,6 @@
|
|||
<span class="text-primary">Your profile</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="usa-nav__primary-item">
|
||||
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
|
||||
{% else %}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
{% if user.is_authenticated %}
|
||||
<span class="ellipsis usa-nav__username">{{ user.email }}</span>
|
||||
</li>
|
||||
{% if has_profile_feature_flag %}
|
||||
<li class="usa-nav__secondary-item">
|
||||
{% url 'user-profile' as user_profile_url %}
|
||||
{% url 'finish-user-profile-setup' as finish_setup_url %}
|
||||
|
@ -26,7 +25,6 @@
|
|||
Your profile
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="usa-nav__secondary-item">
|
||||
<a class="usa-nav-link" href="{% url 'logout' %}">Sign out</a>
|
||||
{% else %}
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
<div class="usa-alert">
|
||||
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
|
||||
<b>Attention:</b> You are on a test site.
|
||||
{% if has_profile_feature_flag %}
|
||||
The profile_feature flag is active.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -535,8 +535,10 @@ class MockDb(TestCase):
|
|||
first_name = "First"
|
||||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
title = "title"
|
||||
phone = "8080102431"
|
||||
cls.user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, email=email
|
||||
username=username, first_name=first_name, last_name=last_name, email=email, title=title, phone=phone
|
||||
)
|
||||
|
||||
current_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||
|
@ -845,6 +847,7 @@ def create_superuser():
|
|||
last_name="last",
|
||||
is_staff=True,
|
||||
password=p,
|
||||
phone="8003111234",
|
||||
)
|
||||
# Retrieve the group or create it if it doesn't exist
|
||||
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
|
||||
|
@ -862,7 +865,9 @@ def create_user():
|
|||
first_name="first",
|
||||
last_name="last",
|
||||
is_staff=True,
|
||||
title="title",
|
||||
password=p,
|
||||
phone="8003111234",
|
||||
)
|
||||
# Retrieve the group or create it if it doesn't exist
|
||||
group, _ = UserGroup.objects.get_or_create(name="cisa_analysts_group")
|
||||
|
@ -879,7 +884,12 @@ def create_test_user():
|
|||
phone = "8003111234"
|
||||
title = "test title"
|
||||
user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, title=title
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
title=title,
|
||||
)
|
||||
return user
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ from .common import (
|
|||
GenericTestHelper,
|
||||
)
|
||||
from unittest.mock import patch
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from django.conf import settings
|
||||
import boto3_mocking # type: ignore
|
||||
|
@ -957,7 +956,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
@override_flag("profile_feature", True)
|
||||
@less_console_noise_decorator
|
||||
def test_save_model_sends_approved_email(self):
|
||||
"""When transitioning to approved on a domain request,
|
||||
|
|
|
@ -256,19 +256,9 @@ class TestDomainRequest(TestCase):
|
|||
|
||||
email_allowed.delete()
|
||||
|
||||
@override_flag("profile_feature", active=False)
|
||||
@less_console_noise_decorator
|
||||
def test_submit_from_started_sends_email(self):
|
||||
msg = "Create a domain request and submit it and see if email was sent."
|
||||
domain_request = completed_domain_request(user=self.dummy_user_2)
|
||||
self.check_email_sent(
|
||||
domain_request, msg, "submit", 1, expected_content="Lava", expected_email=self.dummy_user_2.email
|
||||
)
|
||||
|
||||
@override_flag("profile_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_submit_from_started_sends_email_to_creator(self):
|
||||
"""Tests if, when the profile feature flag is on, we send an email to the creator"""
|
||||
"""tests that we send an email to the creator"""
|
||||
msg = "Create a domain request and submit it and see if email was sent when the feature flag is on."
|
||||
domain_request = completed_domain_request(user=self.dummy_user_2)
|
||||
self.check_email_sent(
|
||||
|
|
|
@ -494,7 +494,13 @@ class HomeTests(TestWithUser):
|
|||
phone = "8003111234"
|
||||
status = User.RESTRICTED
|
||||
restricted_user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, status=status
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
status=status,
|
||||
title="title",
|
||||
)
|
||||
self.client.force_login(restricted_user)
|
||||
response = self.client.get("/request/", follow=True)
|
||||
|
@ -546,7 +552,6 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
return page.follow() if follow else page
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("profile_feature", active=True)
|
||||
def test_full_name_initial_value(self):
|
||||
"""Test that full_name initial value is empty when first_name or last_name is empty.
|
||||
This will later be displayed as "unknown" using javascript."""
|
||||
|
@ -600,8 +605,8 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
incomplete_regular_user.delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_with_profile_feature_on(self):
|
||||
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
|
||||
def test_new_user(self):
|
||||
"""Tests that a new user is redirected to the profile setup page"""
|
||||
username_regular_incomplete = "test_regular_user_incomplete"
|
||||
first_name_2 = "Incomplete"
|
||||
email_2 = "unicorn@igorville.com"
|
||||
|
@ -614,12 +619,10 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
)
|
||||
|
||||
self.app.set_user(incomplete_regular_user.username)
|
||||
with override_flag("profile_feature", active=True):
|
||||
# This will redirect the user to the setup page.
|
||||
# Follow implicity checks if our redirect is working.
|
||||
finish_setup_page = self.app.get(reverse("home")).follow()
|
||||
self._set_session_cookie()
|
||||
|
||||
# Assert that we're on the right page
|
||||
self.assertContains(finish_setup_page, "Finish setting up your profile")
|
||||
|
||||
|
@ -663,7 +666,6 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
verification_type=User.VerificationTypeChoices.REGULAR,
|
||||
)
|
||||
self.app.set_user(incomplete_regular_user.username)
|
||||
with override_flag("profile_feature", active=True):
|
||||
# This will redirect the user to the setup page.
|
||||
# Follow implicity checks if our redirect is working.
|
||||
finish_setup_page = self.app.get(reverse("home")).follow()
|
||||
|
@ -701,8 +703,8 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
incomplete_regular_user.delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
|
||||
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
|
||||
def test_new_user_goes_to_domain_request(self):
|
||||
"""Tests that a new user is redirected to the domain request page"""
|
||||
username_regular_incomplete = "test_regular_user_incomplete"
|
||||
first_name_2 = "Incomplete"
|
||||
email_2 = "unicorn@igorville.com"
|
||||
|
@ -714,7 +716,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
verification_type=User.VerificationTypeChoices.REGULAR,
|
||||
)
|
||||
self.app.set_user(incomplete_regular_user.username)
|
||||
with override_flag("profile_feature", active=True):
|
||||
with override_flag("", active=True):
|
||||
# This will redirect the user to the setup page
|
||||
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
|
||||
self._set_session_cookie()
|
||||
|
@ -758,25 +760,6 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
self.assertContains(completed_setup_page, "You’re about to start your .gov domain request")
|
||||
incomplete_regular_user.delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_with_profile_feature_off(self):
|
||||
"""Tests that a new user is not redirected to the profile setup page when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/")
|
||||
self.assertNotContains(response, "Finish setting up your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_goes_to_domain_request_with_profile_feature_off(self):
|
||||
"""Tests that a new user is redirected to the domain request page
|
||||
when profile_feature is off but not the setup page"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/request/")
|
||||
|
||||
self.assertNotContains(response, "Finish setting up your profile")
|
||||
self.assertNotContains(response, "What contact information should we use to reach you?")
|
||||
|
||||
self.assertContains(response, "You’re about to start your .gov domain request")
|
||||
|
||||
|
||||
class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
||||
"""A series of tests that target the user profile page intercept for incomplete IAL1 user profiles."""
|
||||
|
@ -816,8 +799,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
|||
return page.follow() if follow else page
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_with_profile_feature_on(self):
|
||||
"""Tests that a new user is redirected to the profile setup page when profile_feature is on,
|
||||
def test_new_user(self):
|
||||
"""Tests that a new user is redirected to the profile setup page,
|
||||
and testing that the confirmation modal is present"""
|
||||
username_other_incomplete = "test_other_user_incomplete"
|
||||
first_name_2 = "Incomplete"
|
||||
|
@ -831,7 +814,6 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
|||
verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF,
|
||||
)
|
||||
self.app.set_user(incomplete_other_user.username)
|
||||
with override_flag("profile_feature", active=True):
|
||||
# This will redirect the user to the user profile page.
|
||||
# Follow implicity checks if our redirect is working.
|
||||
user_profile_page = self.app.get(reverse("home")).follow()
|
||||
|
@ -881,9 +863,7 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
|||
# NOTE: "anage" is not a typo. It is to accomodate the fact that the "m" is uppercase in one
|
||||
# instance and lowercase in the other.
|
||||
self.assertContains(save_page, "anage your domains", count=2)
|
||||
self.assertNotContains(
|
||||
save_page, "Before you can manage your domains, we need you to add contact information"
|
||||
)
|
||||
self.assertNotContains(save_page, "Before you can manage your domains, we need you to add contact information")
|
||||
# Assert that modal does not appear on subsequent submits
|
||||
self.assertNotContains(save_page, "domain registrants must maintain accurate contact information")
|
||||
|
||||
|
@ -915,113 +895,59 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
DomainInformation.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def error_500_main_nav_with_profile_feature_turned_on(self):
|
||||
"""test that Your profile is in main nav of 500 error page when profile_feature is on.
|
||||
def error_500_main_nav(self):
|
||||
"""test that Your profile is in main nav of 500 error page.
|
||||
|
||||
Our treatment of 401 and 403 error page handling with that waffle feature is similar, so we
|
||||
assume that the same test results hold true for 401 and 403."""
|
||||
with override_flag("profile_feature", active=True):
|
||||
with self.assertRaises(Exception):
|
||||
response = self.client.get(reverse("home"), follow=True)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def error_500_main_nav_with_profile_feature_turned_off(self):
|
||||
"""test that Your profile is not in main nav of 500 error page when profile_feature is off.
|
||||
|
||||
Our treatment of 401 and 403 error page handling with that waffle feature is similar, so we
|
||||
assume that the same test results hold true for 401 and 403."""
|
||||
with override_flag("profile_feature", active=False):
|
||||
with self.assertRaises(Exception):
|
||||
response = self.client.get(reverse("home"), follow=True)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_home_page_main_nav_with_profile_feature_on(self):
|
||||
"""test that Your profile is in main nav of home page when profile_feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
def test_home_page_main_nav(self):
|
||||
"""test that Your profile is in main nav of home page"""
|
||||
response = self.client.get("/", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_home_page_main_nav_with_profile_feature_off(self):
|
||||
"""test that Your profile is not in main nav of home page when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/", follow=True)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_request_main_nav_with_profile_feature_on(self):
|
||||
"""test that Your profile is in main nav of new request when profile_feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
def test_new_request_main_nav(self):
|
||||
"""test that Your profile is in main nav of new request"""
|
||||
response = self.client.get("/request/", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_request_main_nav_with_profile_feature_off(self):
|
||||
"""test that Your profile is not in main nav of new request when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/request/", follow=True)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_profile_main_nav_with_profile_feature_on(self):
|
||||
"""test that Your profile is in main nav of user profile when profile_feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
def test_user_profile_main_nav(self):
|
||||
"""test that Your profile is in main nav of user profile"""
|
||||
response = self.client.get("/user-profile", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_profile_returns_404_when_feature_off(self):
|
||||
"""test that Your profile returns 404 when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/user-profile", follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_profile_back_button_when_coming_from_domain_request(self):
|
||||
"""tests user profile when profile_feature is on,
|
||||
"""tests user profile,
|
||||
and when they are redirected from the domain request page"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get("/user-profile?redirect=domain-request:")
|
||||
self.assertContains(response, "Your profile")
|
||||
self.assertContains(response, "Go back to your domain request")
|
||||
self.assertNotContains(response, "Back to manage your domains")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_detail_profile_feature_on(self):
|
||||
"""test that domain detail view when profile_feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
def test_domain_detail_contains_your_profile(self):
|
||||
"""Tests that the domain detail view contains 'your profile' rather than 'your contact information'"""
|
||||
response = self.client.get(reverse("domain", args=[self.domain.pk]))
|
||||
self.assertContains(response, "Your profile")
|
||||
self.assertNotContains(response, "Your contact information")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_request_when_profile_feature_on(self):
|
||||
"""test that Your profile is in request page when profile feature is on"""
|
||||
|
||||
contact_user, _ = Contact.objects.get_or_create(
|
||||
first_name="Hank",
|
||||
last_name="McFakerson",
|
||||
)
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
domain_request = DomainRequest.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
senior_official=contact_user,
|
||||
)
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
def test_domain_your_contact_information(self):
|
||||
"""test that your contact information is not accessible"""
|
||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_request_when_profile_feature_off(self):
|
||||
"""test that Your profile is not in request page when profile feature is off"""
|
||||
def test_profile_request_page(self):
|
||||
"""test that your profile is in request"""
|
||||
|
||||
contact_user, _ = Contact.objects.get_or_create(
|
||||
first_name="Hank",
|
||||
|
@ -1034,20 +960,16 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
senior_official=contact_user,
|
||||
)
|
||||
with override_flag("profile_feature", active=False):
|
||||
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
self.assertContains(response, "Your profile")
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
# cleanup
|
||||
domain_request.delete()
|
||||
site.delete()
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_profile_form_submission(self):
|
||||
"""test user profile form submission"""
|
||||
self.app.set_user(self.user.username)
|
||||
with override_flag("profile_feature", active=True):
|
||||
profile_page = self.app.get(reverse("user-profile"))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
|
|
@ -723,7 +723,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
email_address = "mayor@igorville.gov"
|
||||
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
|
||||
|
||||
other_user = User()
|
||||
other_user = create_user()
|
||||
other_user.save()
|
||||
self.client.force_login(other_user)
|
||||
mock_client = MagicMock()
|
||||
|
@ -737,6 +737,12 @@ class TestDomainManagers(TestDomainOverview):
|
|||
def test_domain_invitation_flow(self):
|
||||
"""Send an invitation to a new user, log in and load the dashboard."""
|
||||
email_address = "mayor@igorville.gov"
|
||||
username = "mayor"
|
||||
first_name = "First"
|
||||
last_name = "Last"
|
||||
title = "title"
|
||||
phone = "8080102431"
|
||||
title = "title"
|
||||
User.objects.filter(email=email_address).delete()
|
||||
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
|
@ -752,7 +758,9 @@ class TestDomainManagers(TestDomainOverview):
|
|||
add_page.form.submit()
|
||||
|
||||
# user was invited, create them
|
||||
new_user = User.objects.create(username=email_address, email=email_address)
|
||||
new_user = User.objects.create(
|
||||
username=username, email=email_address, first_name=first_name, last_name=last_name, title=title, phone=phone
|
||||
)
|
||||
# log them in to `self.app`
|
||||
self.app.set_user(new_user.username)
|
||||
# and manually call the on each login callback
|
||||
|
|
|
@ -2247,7 +2247,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
senior_official = domain_request.senior_official
|
||||
self.assertEquals("Testy2", senior_official.first_name)
|
||||
|
||||
@override_flag("profile_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_edit_creator_in_place(self):
|
||||
"""When you:
|
||||
|
|
|
@ -458,3 +458,81 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
# Ensure no approved requests are included
|
||||
for domain_request in data["domain_requests"]:
|
||||
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
|
||||
|
||||
def test_search(self):
|
||||
"""Tests our search functionality. We expect that search filters on creator only when we are in a portfolio"""
|
||||
# Test search for domain name
|
||||
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "lamb"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
self.assertEqual(len(data["domain_requests"]), 1)
|
||||
|
||||
requested_domain = data["domain_requests"][0]["requested_domain"]
|
||||
self.assertEqual(requested_domain, "lamb-chops.gov")
|
||||
|
||||
# Test search for 'New domain request'
|
||||
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "new domain"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
self.assertTrue(any(req["requested_domain"] is None for req in data["domain_requests"]))
|
||||
|
||||
# Test search with portfolio (including creator search)
|
||||
self.client.force_login(self.user)
|
||||
with override_flag("organization_feature", active=True), override_flag("organization_requests", active=True):
|
||||
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
)
|
||||
response = self.app.get(
|
||||
reverse("get_domain_requests_json"), {"search_term": "info", "portfolio": self.portfolio.id}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
self.assertTrue(any(req["creator"].startswith("info") for req in data["domain_requests"]))
|
||||
|
||||
# Test search without portfolio (should not search on creator)
|
||||
with override_flag("organization_feature", active=False), override_flag("organization_requests", active=False):
|
||||
user_perm.delete()
|
||||
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "info"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
self.assertEqual(len(data["domain_requests"]), 0)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
def test_status_filter(self):
|
||||
"""Test that status filtering works properly"""
|
||||
# Test a single status
|
||||
response = self.app.get(reverse("get_domain_requests_json"), {"status": "started"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
|
||||
|
||||
# Test an invalid status
|
||||
response = self.app.get(reverse("get_domain_requests_json"), {"status": "approved"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
self.assertEqual(len(data["domain_requests"]), 0)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
def test_combined_filtering_and_sorting(self):
|
||||
"""Test that combining filters and sorting works properly"""
|
||||
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
response = self.app.get(
|
||||
reverse("get_domain_requests_json"),
|
||||
{"search_term": "beef", "status": "started", "portfolio": self.portfolio.id},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
self.assertTrue(all("beef" in req["requested_domain"] for req in data["domain_requests"]))
|
||||
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
|
||||
created_at_dates = [req["created_at"] for req in data["domain_requests"]]
|
||||
self.assertEqual(created_at_dates, sorted(created_at_dates, reverse=True))
|
||||
user_perm.delete()
|
||||
|
|
|
@ -20,6 +20,7 @@ def get_domain_requests_json(request):
|
|||
unfiltered_total = objects.count()
|
||||
|
||||
objects = apply_search(objects, request)
|
||||
objects = apply_status_filter(objects, request)
|
||||
objects = apply_sorting(objects, request)
|
||||
|
||||
paginator = Paginator(objects, 10)
|
||||
|
@ -63,6 +64,7 @@ def get_domain_request_ids_from_request(request):
|
|||
|
||||
def apply_search(queryset, request):
|
||||
search_term = request.GET.get("search_term")
|
||||
is_portfolio = request.GET.get("portfolio")
|
||||
|
||||
if search_term:
|
||||
search_term_lower = search_term.lower()
|
||||
|
@ -75,11 +77,34 @@ def apply_search(queryset, request):
|
|||
queryset = queryset.filter(
|
||||
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
||||
)
|
||||
elif is_portfolio:
|
||||
queryset = queryset.filter(
|
||||
Q(requested_domain__name__icontains=search_term)
|
||||
| Q(creator__first_name__icontains=search_term)
|
||||
| Q(creator__last_name__icontains=search_term)
|
||||
| Q(creator__email__icontains=search_term)
|
||||
)
|
||||
# For non org users
|
||||
else:
|
||||
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
||||
return queryset
|
||||
|
||||
|
||||
def apply_status_filter(queryset, request):
|
||||
status_param = request.GET.get("status")
|
||||
if status_param:
|
||||
status_list = status_param.split(",")
|
||||
statuses = [status for status in status_list if status in DomainRequest.DomainRequestStatus.values]
|
||||
# Construct Q objects for statuses that can be queried through ORM
|
||||
status_query = Q()
|
||||
if statuses:
|
||||
status_query |= Q(status__in=statuses)
|
||||
# Apply the combined query
|
||||
queryset = queryset.filter(status_query)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
def apply_sorting(queryset, request):
|
||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||
|
|
|
@ -11,7 +11,6 @@ from django.urls import NoReverseMatch, reverse
|
|||
from registrar.models.user import User
|
||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||
from registrar.views.utility.permission_views import UserProfilePermissionView
|
||||
from waffle.decorators import waffle_flag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -46,7 +45,6 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
|||
|
||||
return self.render_to_response(context)
|
||||
|
||||
@waffle_flag("profile_feature") # type: ignore
|
||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue