mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-31 09:43:54 +02:00
resolved merge
This commit is contained in:
commit
59c3a1e535
46 changed files with 1759 additions and 1134 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
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
@use "uswds-core" as *;
|
||||
@use "cisa_colors" as *;
|
||||
|
||||
/* Make "placeholder" links visually obvious */
|
||||
// Used on: TODO links
|
||||
// Used on: NONE
|
||||
a[href$="todo"]::after {
|
||||
background-color: yellow;
|
||||
color: color(blue-80v);
|
||||
|
@ -9,10 +10,14 @@ a[href$="todo"]::after {
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
// Used on: profile
|
||||
// Note: Is this needed?
|
||||
a.usa-link.usa-link--always-blue {
|
||||
color: #{$dhs-blue};
|
||||
}
|
||||
|
||||
// Used on: breadcrumbs
|
||||
// Note: This could potentially be simplified and use usa-button--with-icon
|
||||
a.breadcrumb__back {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
|
@ -28,10 +33,18 @@ a.breadcrumb__back {
|
|||
}
|
||||
}
|
||||
|
||||
// Remove anchor buttons' underline
|
||||
a.usa-button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Unstyled anchor buttons
|
||||
a.usa-button--unstyled:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
// Disabled anchor buttons
|
||||
// NOTE: Not used
|
||||
a.usa-button.disabled-link {
|
||||
background-color: #ccc !important;
|
||||
color: #454545 !important
|
||||
|
@ -58,6 +71,8 @@ a.usa-button--unstyled.disabled-link:focus {
|
|||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
// Disabled buttons
|
||||
// Used on: Domain managers, disabled logo on profile
|
||||
.usa-button--unstyled.disabled-button,
|
||||
.usa-button--unstyled.disabled-button:hover,
|
||||
.usa-button--unstyled.disabled-button:focus {
|
||||
|
@ -66,6 +81,16 @@ a.usa-button--unstyled.disabled-link:focus {
|
|||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
// Unstyled variant for reverse out?
|
||||
// Used on: NONE
|
||||
.usa-button--unstyled--white,
|
||||
.usa-button--unstyled--white:hover,
|
||||
.usa-button--unstyled--white:focus,
|
||||
.usa-button--unstyled--white:active {
|
||||
color: color('white');
|
||||
}
|
||||
|
||||
// Solid anchor buttons
|
||||
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
|
||||
color: color('white');
|
||||
}
|
||||
|
@ -77,6 +102,7 @@ a.usa-button:not(.usa-button--unstyled, .usa-button--outline):active {
|
|||
color: color('white');
|
||||
}
|
||||
|
||||
// Outline anchor buttons
|
||||
a.usa-button--outline,
|
||||
a.usa-button--outline:visited {
|
||||
box-shadow: inset 0 0 0 2px color('primary');
|
||||
|
@ -94,10 +120,22 @@ a.usa-button--outline:active {
|
|||
color: color('primary-darker');
|
||||
}
|
||||
|
||||
// Used on: Domain request withdraw confirmation
|
||||
a.withdraw {
|
||||
background-color: color('error');
|
||||
}
|
||||
|
||||
a.withdraw:hover,
|
||||
a.withdraw:focus {
|
||||
background-color: color('error-dark');
|
||||
}
|
||||
|
||||
a.withdraw:active {
|
||||
background-color: color('error-darker');
|
||||
}
|
||||
|
||||
// Used on: Domain request status
|
||||
//NOTE: Revise to BEM convention usa-button--outline-secondary
|
||||
a.withdraw_outline,
|
||||
a.withdraw_outline:visited {
|
||||
box-shadow: inset 0 0 0 2px color('error');
|
||||
|
@ -115,19 +153,8 @@ a.withdraw_outline:active {
|
|||
color: color('error-darker');
|
||||
}
|
||||
|
||||
a.withdraw:hover,
|
||||
a.withdraw:focus {
|
||||
background-color: color('error-dark');
|
||||
}
|
||||
|
||||
a.withdraw:active {
|
||||
background-color: color('error-darker');
|
||||
}
|
||||
|
||||
a.usa-button--unstyled:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
// Used on: Domain request submit
|
||||
.dotgov-button--green {
|
||||
background-color: color('success-dark');
|
||||
|
||||
|
@ -140,15 +167,8 @@ a.usa-button--unstyled:visited {
|
|||
}
|
||||
}
|
||||
|
||||
.usa-button--unstyled--white,
|
||||
.usa-button--unstyled--white:hover,
|
||||
.usa-button--unstyled--white:focus,
|
||||
.usa-button--unstyled--white:active {
|
||||
color: color('white');
|
||||
}
|
||||
|
||||
// Cancel button used on the
|
||||
// DNSSEC main page
|
||||
// Cancel button
|
||||
// Used on: DNSSEC main page
|
||||
// We want to center this button on mobile
|
||||
// and add some extra left margin on tablet+
|
||||
.usa-button--cancel {
|
||||
|
@ -175,6 +195,8 @@ a.usa-button--unstyled:visited {
|
|||
}
|
||||
}
|
||||
|
||||
// Used on: Profile page, toggleable fields
|
||||
// Note: Could potentially be cleaned up by using usa-button--with-icon
|
||||
// We need to deviate from some default USWDS styles here
|
||||
// in this particular case, so we have to override this.
|
||||
.usa-form .usa-button.readonly-edit-button {
|
||||
|
@ -186,6 +208,7 @@ a.usa-button--unstyled:visited {
|
|||
}
|
||||
}
|
||||
|
||||
//Used on: Domains and Requests tables
|
||||
.usa-button--filter {
|
||||
width: auto;
|
||||
// For mobile stacking
|
||||
|
@ -201,6 +224,8 @@ a.usa-button--unstyled:visited {
|
|||
}
|
||||
}
|
||||
|
||||
// Buttons with nested icons
|
||||
// Note: Can be simplified by adding usa-link--icon to anchors in tables
|
||||
.dotgov-table a,
|
||||
.usa-link--icon,
|
||||
.usa-button--with-icon {
|
||||
|
@ -232,6 +257,9 @@ a .usa-icon,
|
|||
width: 1.5em;
|
||||
}
|
||||
|
||||
// Red, for delete buttons
|
||||
// Used on: All delete buttons
|
||||
// Note: Can be simplified by adding text-secondary to delete anchors in tables
|
||||
button.text-secondary,
|
||||
button.text-secondary:hover,
|
||||
.dotgov-table a.text-secondary {
|
||||
|
|
|
@ -85,7 +85,7 @@ legend.float-left-tablet + button.float-right-tablet {
|
|||
|
||||
.read-only-label {
|
||||
font-size: size('body', 'sm');
|
||||
color: color('primary');
|
||||
color: color('primary-dark');
|
||||
margin-bottom: units(0.5);
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
"registrar.context_processors.is_widescreen_mode",
|
||||
],
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
# Generated by Django 4.2.10 on 2024-09-11 21:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0127_remove_domaininformation_submitter_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
max_length=2,
|
||||
null=True,
|
||||
verbose_name="state, territory, or military post",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
max_length=2,
|
||||
null=True,
|
||||
verbose_name="state, territory, or military post",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolio",
|
||||
name="state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
max_length=2,
|
||||
null=True,
|
||||
verbose_name="state, territory, or military post",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -159,7 +159,7 @@ class DomainInformation(TimeStampedModel):
|
|||
choices=StateTerritoryChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="state / territory",
|
||||
verbose_name="state, territory, or military post",
|
||||
)
|
||||
zipcode = models.CharField(
|
||||
max_length=10,
|
||||
|
|
|
@ -11,6 +11,7 @@ from registrar.models.federal_agency import FederalAgency
|
|||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from auditlog.models import LogEntry
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
|
@ -422,7 +423,7 @@ class DomainRequest(TimeStampedModel):
|
|||
choices=StateTerritoryChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="state / territory",
|
||||
verbose_name="state, territory, or military post",
|
||||
)
|
||||
zipcode = models.CharField(
|
||||
max_length=10,
|
||||
|
@ -576,11 +577,25 @@ class DomainRequest(TimeStampedModel):
|
|||
verbose_name="last updated on",
|
||||
help_text="Date of the last status update",
|
||||
)
|
||||
|
||||
notes = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def get_first_status_set_date(self, status):
|
||||
"""Returns the date when the domain request was first set to the given status."""
|
||||
log_entry = (
|
||||
LogEntry.objects.filter(content_type__model="domainrequest", object_pk=self.pk, changes__status__1=status)
|
||||
.order_by("-timestamp")
|
||||
.first()
|
||||
)
|
||||
return log_entry.timestamp.date() if log_entry else None
|
||||
|
||||
def get_first_status_started_date(self):
|
||||
"""Returns the date when the domain request was put into the status "started" for the first time"""
|
||||
return self.get_first_status_set_date(DomainRequest.DomainRequestStatus.STARTED)
|
||||
|
||||
@classmethod
|
||||
def get_statuses_that_send_emails(cls):
|
||||
"""Returns a list of statuses that send an email to the user"""
|
||||
|
@ -1138,6 +1153,11 @@ class DomainRequest(TimeStampedModel):
|
|||
data[field.name] = field.value_from_object(self)
|
||||
return data
|
||||
|
||||
def get_formatted_cisa_rep_name(self):
|
||||
"""Returns the cisa representatives name in Western order."""
|
||||
names = [n for n in [self.cisa_representative_first_name, self.cisa_representative_last_name] if n]
|
||||
return " ".join(names) if names else "Unknown"
|
||||
|
||||
def _is_federal_complete(self):
|
||||
# Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None
|
||||
return not (self.federal_type is None or self.federal_agency is None)
|
||||
|
|
|
@ -89,7 +89,7 @@ class Portfolio(TimeStampedModel):
|
|||
choices=StateTerritoryChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="state / territory",
|
||||
verbose_name="state, territory, or military post",
|
||||
)
|
||||
|
||||
zipcode = models.CharField(
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import time
|
||||
import logging
|
||||
from urllib.parse import urlparse, urlunparse, urlencode
|
||||
from django.urls import resolve, Resolver404
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -315,3 +316,21 @@ def convert_queryset_to_dict(queryset, is_model=True, key="id"):
|
|||
request_dict = {value[key]: value for value in queryset}
|
||||
|
||||
return request_dict
|
||||
|
||||
|
||||
def get_url_name(path):
|
||||
"""
|
||||
Given a URL path, returns the corresponding URL name defined in urls.py.
|
||||
|
||||
Args:
|
||||
path (str): The URL path to resolve.
|
||||
|
||||
Returns:
|
||||
str or None: The URL name if it exists, otherwise None.
|
||||
"""
|
||||
try:
|
||||
match = resolve(path)
|
||||
return match.url_name
|
||||
except Resolver404:
|
||||
logger.error(f"No matching URL name found for path: {path}")
|
||||
return None
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -77,19 +77,12 @@
|
|||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %}
|
||||
{% else %}
|
||||
{% url 'domain-org-name-address' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
||||
|
||||
{% url 'domain-senior-official' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# Conditionally display profile #}
|
||||
{% if not has_profile_feature_flag %}
|
||||
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
{% if security_email is not None and security_email not in hidden_security_emails%}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{# this is right after the messages block in the parent template #}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
|
||||
<h1>Organization name and mailing address </h1>
|
||||
<h1>Organization</h1>
|
||||
|
||||
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -8,33 +8,30 @@
|
|||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
{% comment %}
|
||||
TODO: Uncomment in #2596
|
||||
{% if portfolio %}
|
||||
{% url 'domain-requests' as url %}
|
||||
{% else %}
|
||||
{% url 'home' as url %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
{% if portfolio %}
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||
{% else %}
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>{{ DomainRequest.requested_domain.name }}</span
|
||||
>
|
||||
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
|
||||
<span>New domain request</span>
|
||||
{% else %}
|
||||
<span>{{ DomainRequest.requested_domain.name }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% else %}{% endcomment %}
|
||||
{% url 'home' as url %}
|
||||
<a href="{{ url }}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||
</svg>
|
||||
|
||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||
Back to manage your domains
|
||||
</p>
|
||||
</a>
|
||||
{% comment %} {% endif %}{% endcomment %}
|
||||
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||
|
@ -48,18 +45,63 @@
|
|||
<span class="text-bold text-primary-darker">
|
||||
Status:
|
||||
</span>
|
||||
{% if DomainRequest.status == 'approved' %} Approved
|
||||
{% elif DomainRequest.status == 'in review' %} In review
|
||||
{% elif DomainRequest.status == 'rejected' %} Rejected
|
||||
{% elif DomainRequest.status == 'submitted' %} Submitted
|
||||
{% elif DomainRequest.status == 'ineligible' %} Ineligible
|
||||
{% else %}ERROR Please contact technical support/dev
|
||||
{% endif %}
|
||||
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<p><b class="review__step__name">Last updated:</b> {{DomainRequest.updated_at|date:"F j, Y"}}</p>
|
||||
|
||||
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
|
||||
{% comment %}
|
||||
These are intentionally seperated this way.
|
||||
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
|
||||
Leave it this way until we've solidified our requirements.
|
||||
{% endcomment %}
|
||||
{% if DomainRequest.status == statuses.STARTED %}
|
||||
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
||||
<p class="margin-top-1">
|
||||
{% comment %}
|
||||
A newly created domain request will not have a value for last_status update.
|
||||
This is because the status never really updated.
|
||||
However, if this somehow goes back to started we can default to displaying that new date.
|
||||
{% endcomment %}
|
||||
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
|
||||
</p>
|
||||
{% endwith %}
|
||||
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.REJECTED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
|
||||
</p>
|
||||
{% else %}
|
||||
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.status != 'rejected' %}
|
||||
<p>{% include "includes/domain_request.html" %}</p>
|
||||
|
@ -67,6 +109,7 @@
|
|||
Withdraw request</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
|
@ -100,7 +143,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if DomainRequest.organization_name %}
|
||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=DomainRequest address='true' heading_level=heading_level %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.about_your_organization %}
|
||||
|
@ -130,7 +173,6 @@
|
|||
{% if DomainRequest.creator %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
|
@ -141,8 +183,8 @@
|
|||
{% if DomainRequest %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if domain_request.cisa_representative_first_name %}
|
||||
{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}
|
||||
{% if DomainRequest.cisa_representative_first_name %}
|
||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
{% if not portfolio %}
|
||||
{% with url_name="domain-org-name-address" %}
|
||||
{% include "includes/domain_sidenav_item.html" with item_text="Organization name and mailing address" %}
|
||||
{% include "includes/domain_sidenav_item.html" with item_text="Organization" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ State-recognized tribe
|
|||
Election office:
|
||||
{{ domain_request.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||
{% endif %}
|
||||
Organization name and mailing address:
|
||||
Organization:
|
||||
{% spaceless %}{{ domain_request.federal_agency }}
|
||||
{{ domain_request.organization_name }}
|
||||
{{ domain_request.address_line1 }}{% if domain_request.address_line2 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
@ -42,7 +40,7 @@
|
|||
{% else %}
|
||||
{% url 'no-portfolio-domains' as url %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
|
||||
<a href="{{ url }}" class="usa-nav-link{% if path|is_domain_subpage %} usa-current{% endif %}">
|
||||
Domains
|
||||
</a>
|
||||
</li>
|
||||
|
@ -59,7 +57,7 @@
|
|||
{% url 'domain-requests' as url %}
|
||||
<button
|
||||
type="button"
|
||||
class="usa-accordion__button usa-nav__link{% if 'request'|in_path:request.path %} usa-current{% endif %}"
|
||||
class="usa-accordion__button usa-nav__link{% if path|is_domain_request_subpage %} usa-current{% endif %}"
|
||||
aria-expanded="false"
|
||||
aria-controls="basic-nav-section-two"
|
||||
>
|
||||
|
@ -80,13 +78,13 @@
|
|||
<!-- user has view but no edit permissions -->
|
||||
{% elif has_any_requests_portfolio_permission %}
|
||||
{% url 'domain-requests' as url %}
|
||||
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
|
||||
<a href="{{ url }}" class="usa-nav-link{% if path|is_domain_request_subpage %} usa-current{% endif %}">
|
||||
Domain requests
|
||||
</a>
|
||||
<!-- user does not have permissions -->
|
||||
{% else %}
|
||||
{% url 'no-portfolio-requests' as url %}
|
||||
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
|
||||
<a href="{{ url }}" class="usa-nav-link{% if path|is_domain_request_subpage %} usa-current{% endif %}">
|
||||
Domain requests
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -104,7 +102,7 @@
|
|||
<li class="usa-nav__primary-item">
|
||||
{% url 'organization' as url %}
|
||||
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
|
||||
<a href="{{ url }}" class="usa-nav-link padding-y-0 {% if request.path == '/organization/' %} usa-current{% endif %}">
|
||||
<a href="{{ url }}" class="usa-nav-link padding-y-0 {% if path|is_portfolio_subpage %} usa-current{% endif %}">
|
||||
<span class="ellipsis ellipsis--23 ellipsis--desktop-50 padding-y-1 desktop:padding-y-2">
|
||||
{{ portfolio.organization_name }}
|
||||
</span>
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
<div class="usa-alert">
|
||||
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %} {% if is_widescreen_mode %}usa-alert__body--widescreen{% 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>
|
||||
|
|
|
@ -16,15 +16,7 @@
|
|||
|
||||
{% if can_edit %}
|
||||
{% include "includes/required_fields.html" %}
|
||||
{% else %}
|
||||
<p>
|
||||
The senior official for your organization can’t be updated here.
|
||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit %}
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{% input_with_errors form.first_name %}
|
||||
{% input_with_errors form.last_name %}
|
||||
|
@ -33,8 +25,16 @@
|
|||
<button type="submit" class="usa-button">Save</button>
|
||||
</form>
|
||||
{% elif not form.full_name.value and not form.title.value and not form.email.value %}
|
||||
<h4>No senior official was found.</h4>
|
||||
<p>
|
||||
Your senior official is a person within your organization who can authorize domain requests.
|
||||
We don't have information about your organization's senior official. To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
The senior official for your organization can’t be updated here.
|
||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
<div class="desktop:margin-top-4">
|
||||
{% if form.full_name.value is not None %}
|
||||
{% include "includes/input_read_only.html" with field=form.full_name %}
|
||||
{% endif %}
|
||||
|
@ -46,4 +46,5 @@
|
|||
{% if form.email.value is not None %}
|
||||
{% include "includes/input_read_only.html" with field=form.email %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<div id="main-content">
|
||||
<h1 id="domains-header">Domains</h1>
|
||||
{% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}
|
||||
|
|
|
@ -5,6 +5,12 @@
|
|||
{% block title %} Domains | {% endblock %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
<div id="main-content">
|
||||
<h1 id="domains-header">Domains</h1>
|
||||
<section class="section-outlined">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'portfolio_base.html' %}
|
||||
{% load static field_helpers%}
|
||||
|
||||
{% block title %}Organization mailing address | {{ portfolio.name }}{% endblock %}
|
||||
{% block title %}Organization name and mailing address | {{ portfolio.name }}{% endblock %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
|
@ -19,21 +19,25 @@
|
|||
|
||||
<div class="tablet:grid-col-9" id="main-content">
|
||||
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<h1>Organization</h1>
|
||||
|
||||
<p>The name of your federal agency will be publicly listed as the domain registrant.</p>
|
||||
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
||||
|
||||
{% if has_edit_org_portfolio_permission %}
|
||||
<p>
|
||||
The federal agency for your organization can’t be updated here.
|
||||
Your organization name can’t be updated here.
|
||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% include "includes/required_fields.html" %}
|
||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<h4 class="read-only-label">Federal agency</h4>
|
||||
<h4 class="read-only-label">Organization name</h4>
|
||||
<p class="read-only-value">
|
||||
{{ portfolio.federal_agency }}
|
||||
</p>
|
||||
|
@ -49,7 +53,7 @@
|
|||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<h4 class="read-only-label">Federal agency</h4>
|
||||
<h4 class="read-only-label">Organization name</h4>
|
||||
<p class="read-only-value">
|
||||
{{ portfolio.federal_agency }}
|
||||
</p>
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<div id="main-content">
|
||||
<h1 id="domain-requests-header">Domain requests</h1>
|
||||
<div class="grid-row grid-gap">
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
{% load static %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-3">
|
||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||
|
|
|
@ -3,6 +3,9 @@ from django import template
|
|||
import re
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from registrar.views.domain_request import DomainRequestWizard
|
||||
|
||||
from registrar.models.utility.generic_helper import get_url_name
|
||||
|
||||
register = template.Library()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -174,3 +177,65 @@ def has_contact_info(user):
|
|||
@register.filter
|
||||
def model_name_lowercase(instance):
|
||||
return instance.__class__.__name__.lower()
|
||||
|
||||
|
||||
@register.filter(name="is_domain_subpage")
|
||||
def is_domain_subpage(path):
|
||||
"""Checks if the given page is a subpage of domains.
|
||||
Takes a path name, like '/domains/'."""
|
||||
# Since our pages aren't unified under a common path, we need this approach for now.
|
||||
url_names = [
|
||||
"domains",
|
||||
"no-portfolio-domains",
|
||||
"domain",
|
||||
"domain-users",
|
||||
"domain-dns",
|
||||
"domain-dns-nameservers",
|
||||
"domain-dns-dnssec",
|
||||
"domain-dns-dnssec-dsdata",
|
||||
"domain-your-contact-information",
|
||||
"domain-org-name-address",
|
||||
"domain-senior-official",
|
||||
"domain-security-email",
|
||||
"domain-users-add",
|
||||
"domain-request-delete",
|
||||
"domain-user-delete",
|
||||
"invitation-delete",
|
||||
]
|
||||
return get_url_name(path) in url_names
|
||||
|
||||
|
||||
@register.filter(name="is_domain_request_subpage")
|
||||
def is_domain_request_subpage(path):
|
||||
"""Checks if the given page is a subpage of domain requests.
|
||||
Takes a path name, like '/requests/'."""
|
||||
# Since our pages aren't unified under a common path, we need this approach for now.
|
||||
url_names = [
|
||||
"domain-requests",
|
||||
"no-portfolio-requests",
|
||||
"domain-request-status",
|
||||
"domain-request-withdraw-confirmation",
|
||||
"domain-request-withdrawn",
|
||||
"domain-request-delete",
|
||||
]
|
||||
|
||||
# The domain request wizard pages don't have a defined path,
|
||||
# so we need to check directly on it.
|
||||
wizard_paths = [
|
||||
DomainRequestWizard.EDIT_URL_NAME,
|
||||
DomainRequestWizard.URL_NAMESPACE,
|
||||
DomainRequestWizard.NEW_URL_NAME,
|
||||
]
|
||||
return get_url_name(path) in url_names or any(wizard in path for wizard in wizard_paths)
|
||||
|
||||
|
||||
@register.filter(name="is_portfolio_subpage")
|
||||
def is_portfolio_subpage(path):
|
||||
"""Checks if the given page is a subpage of portfolio.
|
||||
Takes a path name, like '/organization/'."""
|
||||
# Since our pages aren't unified under a common path, we need this approach for now.
|
||||
url_names = [
|
||||
"organization",
|
||||
"senior-official",
|
||||
]
|
||||
return get_url_name(path) in url_names
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -9,6 +9,9 @@ from registrar.templatetags.custom_filters import (
|
|||
find_index,
|
||||
slice_after,
|
||||
contains_checkbox,
|
||||
is_domain_request_subpage,
|
||||
is_domain_subpage,
|
||||
is_portfolio_subpage,
|
||||
)
|
||||
|
||||
|
||||
|
@ -90,3 +93,18 @@ class CustomFiltersTestCase(TestCase):
|
|||
]
|
||||
result = contains_checkbox(html_list)
|
||||
self.assertFalse(result) # Expecting False
|
||||
|
||||
def test_is_domain_subpage(self):
|
||||
"""Tests if the path is recognized as a domain subpage."""
|
||||
self.assertTrue(is_domain_subpage("/domains/"))
|
||||
self.assertFalse(is_domain_subpage("/"))
|
||||
|
||||
def test_is_domain_request_subpage(self):
|
||||
"""Tests if the path is recognized as a domain request subpage."""
|
||||
self.assertTrue(is_domain_request_subpage("/requests/"))
|
||||
self.assertFalse(is_domain_request_subpage("/"))
|
||||
|
||||
def test_is_portfolio_subpage(self):
|
||||
"""Tests if the path is recognized as a portfolio subpage."""
|
||||
self.assertTrue(is_portfolio_subpage("/organization/"))
|
||||
self.assertFalse(is_portfolio_subpage("/"))
|
||||
|
|
|
@ -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
|
||||
|
@ -1298,7 +1306,9 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
"""Can load domain's org name and mailing address page."""
|
||||
page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
# once on the sidebar, once in the page title, once as H1
|
||||
self.assertContains(page, "Organization name and mailing address", count=4)
|
||||
self.assertContains(page, "/org-name-address")
|
||||
self.assertContains(page, "Organization name and mailing address")
|
||||
self.assertContains(page, "Organization</h1>")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_org_name_address_content(self):
|
||||
|
@ -1607,7 +1617,7 @@ class TestDomainSuborganization(TestDomainOverview):
|
|||
|
||||
# Test for the title change
|
||||
self.assertContains(page, "Suborganization")
|
||||
self.assertNotContains(page, "Organization name")
|
||||
self.assertNotContains(page, "Organization")
|
||||
|
||||
# Test for the good value
|
||||
self.assertContains(page, "Ice Cream")
|
||||
|
|
|
@ -200,7 +200,7 @@ class TestPortfolio(WebTest):
|
|||
# Assert the response is a 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# The label for Federal agency will always be a h4
|
||||
self.assertContains(response, '<h4 class="read-only-label">Federal agency</h4>')
|
||||
self.assertContains(response, '<h4 class="read-only-label">Organization name</h4>')
|
||||
# The read only label for city will be a h4
|
||||
self.assertContains(response, '<h4 class="read-only-label">City</h4>')
|
||||
self.assertNotContains(response, 'for="id_city"')
|
||||
|
@ -225,10 +225,10 @@ class TestPortfolio(WebTest):
|
|||
# Assert the response is a 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# The label for Federal agency will always be a h4
|
||||
self.assertContains(response, '<h4 class="read-only-label">Federal agency</h4>')
|
||||
self.assertContains(response, '<h4 class="read-only-label">Organization name</h4>')
|
||||
# The read only label for city will be a h4
|
||||
self.assertNotContains(response, '<h4 class="read-only-label">City</h4>')
|
||||
self.assertNotContains(response, '<p class="read-only-value">Los Angeles</p>>')
|
||||
self.assertNotContains(response, '<p class="read-only-value">Los Angeles</p>')
|
||||
self.assertContains(response, 'for="id_city"')
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -342,9 +342,7 @@ class TestPortfolio(WebTest):
|
|||
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
|
||||
)
|
||||
page = self.app.get(reverse("organization"))
|
||||
self.assertContains(
|
||||
page, "The name of your federal agency will be publicly listed as the domain registrant."
|
||||
)
|
||||
self.assertContains(page, "The name of your organization will be publicly listed as the domain registrant.")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_org_name_address_content(self):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from unittest import skip
|
||||
from unittest.mock import Mock
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
|
@ -56,6 +57,46 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
intro_page = self.app.get(reverse("domain-request:"))
|
||||
self.assertContains(intro_page, "You’re about to start your .gov domain request")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_template_status_display(self):
|
||||
"""Tests the display of status-related information in the template."""
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||
domain_request.last_submitted_date = datetime.now()
|
||||
domain_request.save()
|
||||
response = self.app.get(f"/domain-request/{domain_request.id}")
|
||||
self.assertContains(response, "Submitted on:")
|
||||
self.assertContains(response, domain_request.last_submitted_date.strftime("%B %-d, %Y"))
|
||||
|
||||
@patch.object(DomainRequest, "get_first_status_set_date")
|
||||
def test_get_first_status_started_date(self, mock_get_first_status_set_date):
|
||||
"""Tests retrieval of the first date the status was set to 'started'."""
|
||||
|
||||
# Set the mock to return a fixed date
|
||||
fixed_date = timezone.datetime(2023, 1, 1).date()
|
||||
mock_get_first_status_set_date.return_value = fixed_date
|
||||
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
|
||||
domain_request.last_status_update = None
|
||||
domain_request.save()
|
||||
|
||||
response = self.app.get(f"/domain-request/{domain_request.id}")
|
||||
# Ensure that the date is still set to None
|
||||
self.assertIsNone(domain_request.last_status_update)
|
||||
print(response)
|
||||
# We should still grab a date for this field in this event - but it should come from the audit log instead
|
||||
self.assertContains(response, "Started on:")
|
||||
self.assertContains(response, fixed_date.strftime("%B %-d, %Y"))
|
||||
|
||||
# If a status date is set, we display that instead
|
||||
domain_request.last_status_update = datetime.now()
|
||||
domain_request.save()
|
||||
|
||||
response = self.app.get(f"/domain-request/{domain_request.id}")
|
||||
|
||||
# We should still grab a date for this field in this event - but it should come from the audit log instead
|
||||
self.assertContains(response, "Started on:")
|
||||
self.assertContains(response, domain_request.last_status_update.strftime("%B %-d, %Y"))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_form_intro_is_skipped_when_edit_access(self):
|
||||
"""Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'"""
|
||||
|
@ -2206,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()
|
||||
|
|
|
@ -204,7 +204,7 @@ class DomainView(DomainBaseView):
|
|||
|
||||
|
||||
class DomainOrgNameAddressView(DomainFormBaseView):
|
||||
"""Organization name and mailing address view"""
|
||||
"""Organization view"""
|
||||
|
||||
model = Domain
|
||||
template_name = "domain_org_name_address.html"
|
||||
|
|
|
@ -82,7 +82,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
Step.TRIBAL_GOVERNMENT: _("Tribal government"),
|
||||
Step.ORGANIZATION_FEDERAL: _("Federal government branch"),
|
||||
Step.ORGANIZATION_ELECTION: _("Election office"),
|
||||
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
|
||||
Step.ORGANIZATION_CONTACT: _("Organization"),
|
||||
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
|
||||
Step.SENIOR_OFFICIAL: _("Senior official"),
|
||||
Step.CURRENT_SITES: _("Current websites"),
|
||||
|
|
|
@ -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