mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 18:09:25 +02:00
Merge remote-tracking branch 'origin/main' into nl/2300-Senior-Official-Table
This commit is contained in:
commit
ae4e435e49
22 changed files with 593 additions and 103 deletions
|
@ -67,8 +67,8 @@ services:
|
||||||
# command: "python"
|
# command: "python"
|
||||||
command: >
|
command: >
|
||||||
bash -c " python manage.py migrate &&
|
bash -c " python manage.py migrate &&
|
||||||
python manage.py load &&
|
|
||||||
python manage.py createcachetable &&
|
python manage.py createcachetable &&
|
||||||
|
python manage.py load &&
|
||||||
python manage.py runserver 0.0.0.0:8080"
|
python manage.py runserver 0.0.0.0:8080"
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
|
|
@ -598,6 +598,27 @@ class UserContactInline(admin.StackedInline):
|
||||||
|
|
||||||
model = models.Contact
|
model = models.Contact
|
||||||
|
|
||||||
|
# Read only that we'll leverage for CISA Analysts
|
||||||
|
analyst_readonly_fields = [
|
||||||
|
"user",
|
||||||
|
"email",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
"""Set the read-only state on form elements.
|
||||||
|
We have 1 conditions that determine which fields are read-only:
|
||||||
|
admin user permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
readonly_fields = list(self.readonly_fields)
|
||||||
|
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for analysts and
|
||||||
|
# users who might not belong to groups
|
||||||
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
|
return readonly_fields # Read-only fields for analysts
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
@ -645,7 +666,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
None,
|
None,
|
||||||
{"fields": ("username", "password", "status", "verification_type")},
|
{"fields": ("username", "password", "status", "verification_type")},
|
||||||
),
|
),
|
||||||
("Personal info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||||
(
|
(
|
||||||
"Permissions",
|
"Permissions",
|
||||||
{
|
{
|
||||||
|
@ -676,7 +697,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||||
(
|
(
|
||||||
"Permissions",
|
"Permissions",
|
||||||
{
|
{
|
||||||
|
@ -700,7 +721,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
# NOT all fields are readonly for admin, otherwise we would have
|
# NOT all fields are readonly for admin, otherwise we would have
|
||||||
# set this at the permissions level. The exception is 'status'
|
# set this at the permissions level. The exception is 'status'
|
||||||
analyst_readonly_fields = [
|
analyst_readonly_fields = [
|
||||||
"Personal Info",
|
"User profile",
|
||||||
"first_name",
|
"first_name",
|
||||||
"middle_name",
|
"middle_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
|
@ -937,6 +958,7 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
# Read only that we'll leverage for CISA Analysts
|
# Read only that we'll leverage for CISA Analysts
|
||||||
analyst_readonly_fields = [
|
analyst_readonly_fields = [
|
||||||
"user",
|
"user",
|
||||||
|
"email",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
@ -1246,7 +1268,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
search_help_text = "Search by domain."
|
search_help_text = "Search by domain."
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {"fields": ["portfolio", "creator", "submitter", "domain_request", "notes"]}),
|
(None, {"fields": ["portfolio", "sub_organization", "creator", "submitter", "domain_request", "notes"]}),
|
||||||
(".gov domain", {"fields": ["domain"]}),
|
(".gov domain", {"fields": ["domain"]}),
|
||||||
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
|
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||||
("Background info", {"fields": ["anything_else"]}),
|
("Background info", {"fields": ["anything_else"]}),
|
||||||
|
@ -1325,6 +1347,8 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"senior_official",
|
"senior_official",
|
||||||
"domain",
|
"domain",
|
||||||
"submitter",
|
"submitter",
|
||||||
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Table ordering
|
# Table ordering
|
||||||
|
@ -1334,6 +1358,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
superuser_only_fields = [
|
superuser_only_fields = [
|
||||||
"portfolio",
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
]
|
]
|
||||||
|
|
||||||
# DEVELOPER's NOTE:
|
# DEVELOPER's NOTE:
|
||||||
|
@ -1522,6 +1547,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
"portfolio",
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
|
@ -1629,11 +1655,14 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"creator",
|
"creator",
|
||||||
"senior_official",
|
"senior_official",
|
||||||
"investigator",
|
"investigator",
|
||||||
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
]
|
]
|
||||||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
||||||
|
|
||||||
superuser_only_fields = [
|
superuser_only_fields = [
|
||||||
"portfolio",
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
]
|
]
|
||||||
|
|
||||||
# DEVELOPER's NOTE:
|
# DEVELOPER's NOTE:
|
||||||
|
@ -2048,14 +2077,7 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
fieldsets = DomainInformationAdmin.fieldsets
|
fieldsets = DomainInformationAdmin.fieldsets
|
||||||
readonly_fields = DomainInformationAdmin.readonly_fields
|
readonly_fields = DomainInformationAdmin.readonly_fields
|
||||||
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
||||||
|
autocomplete_fields = DomainInformationAdmin.autocomplete_fields
|
||||||
autocomplete_fields = [
|
|
||||||
"creator",
|
|
||||||
"domain_request",
|
|
||||||
"senior_official",
|
|
||||||
"domain",
|
|
||||||
"submitter",
|
|
||||||
]
|
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
"""Custom has_change_permission override so that we can specify that
|
"""Custom has_change_permission override so that we can specify that
|
||||||
|
@ -2167,8 +2189,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# this ordering effects the ordering of results
|
# this ordering effects the ordering of results in autocomplete_fields for domain
|
||||||
# in autocomplete_fields for domain
|
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def generic_org_type(self, obj):
|
def generic_org_type(self, obj):
|
||||||
|
@ -2652,6 +2673,11 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
# readonly_fields = [
|
# readonly_fields = [
|
||||||
# "requestor",
|
# "requestor",
|
||||||
# ]
|
# ]
|
||||||
|
# Creates select2 fields (with search bars)
|
||||||
|
autocomplete_fields = [
|
||||||
|
"creator",
|
||||||
|
"federal_agency",
|
||||||
|
]
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
|
|
||||||
|
@ -2735,6 +2761,10 @@ class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
list_display = ["name", "portfolio"]
|
list_display = ["name", "portfolio"]
|
||||||
|
autocomplete_fields = [
|
||||||
|
"portfolio",
|
||||||
|
]
|
||||||
|
search_fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
admin.site.unregister(LogEntry) # Unregister the default registration
|
admin.site.unregister(LogEntry) # Unregister the default registration
|
||||||
|
|
|
@ -46,7 +46,7 @@ function ScrollToElement(attributeName, attributeValue) {
|
||||||
} else if (attributeName === 'id') {
|
} else if (attributeName === 'id') {
|
||||||
targetEl = document.getElementById(attributeValue);
|
targetEl = document.getElementById(attributeValue);
|
||||||
} else {
|
} else {
|
||||||
console.log('Error: unknown attribute name provided.');
|
console.error('Error: unknown attribute name provided.');
|
||||||
return; // Exit the function if an invalid attributeName is provided
|
return; // Exit the function if an invalid attributeName is provided
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +78,50 @@ function makeVisible(el) {
|
||||||
el.style.visibility = "visible";
|
el.style.visibility = "visible";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles expand_more / expand_more svgs in buttons or anchors
|
||||||
|
* @param {Element} element - DOM element
|
||||||
|
*/
|
||||||
|
function toggleCaret(element) {
|
||||||
|
// Get a reference to the use element inside the button
|
||||||
|
const useElement = element.querySelector('use');
|
||||||
|
// Check if the span element text is 'Hide'
|
||||||
|
if (useElement.getAttribute('xlink:href') === '/public/img/sprite.svg#expand_more') {
|
||||||
|
// Update the xlink:href attribute to expand_more
|
||||||
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
||||||
|
} else {
|
||||||
|
// Update the xlink:href attribute to expand_less
|
||||||
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that scrolls to an element
|
||||||
|
* @param {string} attributeName - The string "class" or "id"
|
||||||
|
* @param {string} attributeValue - The class or id name
|
||||||
|
*/
|
||||||
|
function ScrollToElement(attributeName, attributeValue) {
|
||||||
|
let targetEl = null;
|
||||||
|
|
||||||
|
if (attributeName === 'class') {
|
||||||
|
targetEl = document.getElementsByClassName(attributeValue)[0];
|
||||||
|
} else if (attributeName === 'id') {
|
||||||
|
targetEl = document.getElementById(attributeValue);
|
||||||
|
} else {
|
||||||
|
console.error('Error: unknown attribute name provided.');
|
||||||
|
return; // Exit the function if an invalid attributeName is provided
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetEl) {
|
||||||
|
const rect = targetEl.getBoundingClientRect();
|
||||||
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||||
|
window.scrollTo({
|
||||||
|
top: rect.top + scrollTop,
|
||||||
|
behavior: 'smooth' // Optional: for smooth scrolling
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Creates and returns a live region element. */
|
/** Creates and returns a live region element. */
|
||||||
function createLiveRegion(id) {
|
function createLiveRegion(id) {
|
||||||
const liveRegion = document.createElement("div");
|
const liveRegion = document.createElement("div");
|
||||||
|
@ -927,7 +971,7 @@ function unloadModals() {
|
||||||
* @param {string} itemName - The name displayed in the counter
|
* @param {string} itemName - The name displayed in the counter
|
||||||
* @param {string} paginationSelector - CSS selector for the pagination container.
|
* @param {string} paginationSelector - CSS selector for the pagination container.
|
||||||
* @param {string} counterSelector - CSS selector for the pagination counter.
|
* @param {string} counterSelector - CSS selector for the pagination counter.
|
||||||
* @param {string} headerAnchor - CSS selector for the header element to anchor the links to.
|
* @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 {Function} loadPageFunction - Function to call when a page link is clicked.
|
||||||
* @param {number} currentPage - The current page number (starting with 1).
|
* @param {number} currentPage - The current page number (starting with 1).
|
||||||
* @param {number} numPages - The total number of pages.
|
* @param {number} numPages - The total number of pages.
|
||||||
|
@ -936,7 +980,7 @@ function unloadModals() {
|
||||||
* @param {number} totalItems - The total number of items.
|
* @param {number} totalItems - The total number of items.
|
||||||
* @param {string} searchTerm - The search term
|
* @param {string} searchTerm - The search term
|
||||||
*/
|
*/
|
||||||
function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
|
function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
|
||||||
const paginationContainer = document.querySelector(paginationSelector);
|
const paginationContainer = document.querySelector(paginationSelector);
|
||||||
const paginationCounter = document.querySelector(counterSelector);
|
const paginationCounter = document.querySelector(counterSelector);
|
||||||
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
||||||
|
@ -955,7 +999,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
||||||
const prevPageItem = document.createElement('li');
|
const prevPageItem = document.createElement('li');
|
||||||
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||||
prevPageItem.innerHTML = `
|
prevPageItem.innerHTML = `
|
||||||
<a href="${headerAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
||||||
<svg class="usa-icon" aria-hidden="true" role="img">
|
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||||
<use xlink:href="/public/img/sprite.svg#navigate_before"></use>
|
<use xlink:href="/public/img/sprite.svg#navigate_before"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -974,7 +1018,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
||||||
const pageItem = document.createElement('li');
|
const pageItem = document.createElement('li');
|
||||||
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
||||||
pageItem.innerHTML = `
|
pageItem.innerHTML = `
|
||||||
<a href="${headerAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
<a href="${linkAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
||||||
`;
|
`;
|
||||||
if (page === currentPage) {
|
if (page === currentPage) {
|
||||||
pageItem.querySelector('a').classList.add('usa-current');
|
pageItem.querySelector('a').classList.add('usa-current');
|
||||||
|
@ -1020,7 +1064,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
||||||
const nextPageItem = document.createElement('li');
|
const nextPageItem = document.createElement('li');
|
||||||
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||||
nextPageItem.innerHTML = `
|
nextPageItem.innerHTML = `
|
||||||
<a href="${headerAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
||||||
<span class="usa-pagination__link-text">Next</span>
|
<span class="usa-pagination__link-text">Next</span>
|
||||||
<svg class="usa-icon" aria-hidden="true" role="img">
|
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||||
<use xlink:href="/public/img/sprite.svg#navigate_next"></use>
|
<use xlink:href="/public/img/sprite.svg#navigate_next"></use>
|
||||||
|
@ -1039,20 +1083,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
||||||
* A helper that toggles content/ no content/ no search results
|
* A helper that toggles content/ no content/ no search results
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm) => {
|
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
||||||
const { unfiltered_total, total } = data;
|
const { unfiltered_total, total } = data;
|
||||||
|
|
||||||
if (searchTermHolder)
|
|
||||||
searchTermHolder.innerHTML = '';
|
|
||||||
|
|
||||||
if (unfiltered_total) {
|
if (unfiltered_total) {
|
||||||
if (total) {
|
if (total) {
|
||||||
showElement(dataWrapper);
|
showElement(dataWrapper);
|
||||||
hideElement(noSearchResultsWrapper);
|
hideElement(noSearchResultsWrapper);
|
||||||
hideElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
} else {
|
} else {
|
||||||
if (searchTermHolder)
|
|
||||||
searchTermHolder.innerHTML = currentSearchTerm;
|
|
||||||
hideElement(dataWrapper);
|
hideElement(dataWrapper);
|
||||||
showElement(noSearchResultsWrapper);
|
showElement(noSearchResultsWrapper);
|
||||||
hideElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
|
@ -1090,14 +1128,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
let currentOrder = 'asc';
|
let currentOrder = 'asc';
|
||||||
const noDomainsWrapper = document.querySelector('.domains__no-data');
|
const noDomainsWrapper = document.querySelector('.domains__no-data');
|
||||||
const noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
|
const noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
|
||||||
let hasLoaded = false;
|
let scrollToTable = false;
|
||||||
let currentSearchTerm = ''
|
let currentStatus = [];
|
||||||
|
let currentSearchTerm = '';
|
||||||
const domainsSearchInput = document.getElementById('domains__search-field');
|
const domainsSearchInput = document.getElementById('domains__search-field');
|
||||||
const domainsSearchSubmit = document.getElementById('domains__search-field-submit');
|
const domainsSearchSubmit = document.getElementById('domains__search-field-submit');
|
||||||
const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]');
|
const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]');
|
||||||
const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region');
|
const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region');
|
||||||
const searchTermHolder = document.querySelector('.domains__search-term');
|
const resetSearchButton = document.querySelector('.domains__reset-search');
|
||||||
const resetButton = document.querySelector('.domains__reset-button');
|
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');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||||
|
@ -1105,21 +1147,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
* @param {*} page - the page number of the results (starts with 1)
|
* @param {*} page - the page number of the results (starts with 1)
|
||||||
* @param {*} sortBy - the sort column option
|
* @param {*} sortBy - the sort column option
|
||||||
* @param {*} order - the sort order {asc, desc}
|
* @param {*} order - the sort order {asc, desc}
|
||||||
* @param {*} loaded - control for the scrollToElement functionality
|
* @param {*} scroll - control for the scrollToElement functionality
|
||||||
* @param {*} searchTerm - the search term
|
* @param {*} searchTerm - the search term
|
||||||
*/
|
*/
|
||||||
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) {
|
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm) {
|
||||||
//fetch json of page of domains, given page # and sort
|
// fetch json of page of domains, given params
|
||||||
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.log('Error in AJAX call: ' + data.error);
|
console.error('Error in AJAX call: ' + data.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results
|
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results
|
||||||
updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm);
|
updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, currentSearchTerm);
|
||||||
|
|
||||||
// identify the DOM element where the domain list will be inserted into the DOM
|
// identify the DOM element where the domain list will be inserted into the DOM
|
||||||
const domainList = document.querySelector('.domains__table tbody');
|
const domainList = document.querySelector('.domains__table tbody');
|
||||||
|
@ -1132,7 +1174,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
|
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
|
||||||
const actionUrl = domain.action_url;
|
const actionUrl = domain.action_url;
|
||||||
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<th scope="row" role="rowheader" data-label="Domain name">
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
|
@ -1148,7 +1189,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
data-position="top"
|
data-position="top"
|
||||||
title="${domain.get_state_help_text}"
|
title="${domain.get_state_help_text}"
|
||||||
focusable="true"
|
focusable="true"
|
||||||
aria-label="Status Information"
|
aria-label="${domain.get_state_help_text}"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
|
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
|
||||||
|
@ -1169,16 +1210,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
initializeTooltips();
|
initializeTooltips();
|
||||||
|
|
||||||
// Do not scroll on first page load
|
// Do not scroll on first page load
|
||||||
if (loaded)
|
if (scroll)
|
||||||
ScrollToElement('id', 'domains-header');
|
ScrollToElement('class', 'domains');
|
||||||
hasLoaded = true;
|
scrollToTable = true;
|
||||||
|
|
||||||
// update pagination
|
// update pagination
|
||||||
updatePagination(
|
updatePagination(
|
||||||
'domain',
|
'domain',
|
||||||
'#domains-pagination',
|
'#domains-pagination',
|
||||||
'#domains-pagination .usa-pagination__counter',
|
'#domains-pagination .usa-pagination__counter',
|
||||||
'#domains-header',
|
'#domains',
|
||||||
loadDomains,
|
loadDomains,
|
||||||
data.page,
|
data.page,
|
||||||
data.num_pages,
|
data.num_pages,
|
||||||
|
@ -1214,13 +1255,51 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
currentSearchTerm = domainsSearchInput.value;
|
currentSearchTerm = domainsSearchInput.value;
|
||||||
// If the search is blank, we match the resetSearch functionality
|
// If the search is blank, we match the resetSearch functionality
|
||||||
if (currentSearchTerm) {
|
if (currentSearchTerm) {
|
||||||
showElement(resetButton);
|
showElement(resetSearchButton);
|
||||||
} else {
|
} else {
|
||||||
hideElement(resetButton);
|
hideElement(resetSearchButton);
|
||||||
}
|
}
|
||||||
loadDomains(1, 'id', 'asc');
|
loadDomains(1, 'id', 'asc');
|
||||||
resetHeaders();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Reset UI and accessibility
|
||||||
function resetHeaders() {
|
function resetHeaders() {
|
||||||
|
@ -1235,18 +1314,78 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
function resetSearch() {
|
function resetSearch() {
|
||||||
domainsSearchInput.value = '';
|
domainsSearchInput.value = '';
|
||||||
currentSearchTerm = '';
|
currentSearchTerm = '';
|
||||||
hideElement(resetButton);
|
hideElement(resetSearchButton);
|
||||||
loadDomains(1, 'id', 'asc', hasLoaded, '');
|
loadDomains(1, 'id', 'asc');
|
||||||
resetHeaders();
|
resetHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resetButton) {
|
if (resetSearchButton) {
|
||||||
resetButton.addEventListener('click', function() {
|
resetSearchButton.addEventListener('click', function() {
|
||||||
resetSearch();
|
resetSearch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the first page initially
|
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 accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||||
|
|
||||||
|
if (accordionIsOpen && !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 accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||||
|
|
||||||
|
if (accordionIsOpen && !accordion.contains(event.target)) {
|
||||||
|
closeFilters();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load
|
||||||
loadDomains(1);
|
loadDomains(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1279,14 +1418,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
let currentOrder = 'asc';
|
let currentOrder = 'asc';
|
||||||
const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data');
|
const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data');
|
||||||
const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results');
|
const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results');
|
||||||
let hasLoaded = false;
|
let scrollToTable = false;
|
||||||
let currentSearchTerm = ''
|
let currentSearchTerm = '';
|
||||||
const domainRequestsSearchInput = document.getElementById('domain-requests__search-field');
|
const domainRequestsSearchInput = document.getElementById('domain-requests__search-field');
|
||||||
const domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit');
|
const domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit');
|
||||||
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
|
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
|
||||||
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
|
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
|
||||||
const searchTermHolder = document.querySelector('.domain-requests__search-term');
|
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
|
||||||
const resetButton = document.querySelector('.domain-requests__reset-button');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
|
* 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.
|
||||||
|
@ -1316,7 +1454,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
// Update data and UI
|
// Update data and UI
|
||||||
loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, hasLoaded, currentSearchTerm);
|
loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, scrollToTable, currentSearchTerm);
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching domain requests:', error));
|
.catch(error => console.error('Error fetching domain requests:', error));
|
||||||
}
|
}
|
||||||
|
@ -1332,21 +1470,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
* @param {*} page - the page number of the results (starts with 1)
|
* @param {*} page - the page number of the results (starts with 1)
|
||||||
* @param {*} sortBy - the sort column option
|
* @param {*} sortBy - the sort column option
|
||||||
* @param {*} order - the sort order {asc, desc}
|
* @param {*} order - the sort order {asc, desc}
|
||||||
* @param {*} loaded - control for the scrollToElement functionality
|
* @param {*} scroll - control for the scrollToElement functionality
|
||||||
* @param {*} searchTerm - the search term
|
* @param {*} searchTerm - the search term
|
||||||
*/
|
*/
|
||||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) {
|
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm) {
|
||||||
//fetch json of page of domain requests, given page # and sort
|
// fetch json of page of domain requests, given params
|
||||||
fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.log('Error in AJAX call: ' + data.error);
|
console.error('Error in AJAX call: ' + data.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
||||||
updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm);
|
updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, currentSearchTerm);
|
||||||
|
|
||||||
// identify the DOM element where the domain request list will be inserted into the DOM
|
// identify the DOM element where the domain request list will be inserted into the DOM
|
||||||
const tbody = document.querySelector('.domain-requests__table tbody');
|
const tbody = document.querySelector('.domain-requests__table tbody');
|
||||||
|
@ -1533,16 +1671,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Do not scroll on first page load
|
// Do not scroll on first page load
|
||||||
if (loaded)
|
if (scroll)
|
||||||
ScrollToElement('id', 'domain-requests-header');
|
ScrollToElement('class', 'domain-requests');
|
||||||
hasLoaded = true;
|
scrollToTable = true;
|
||||||
|
|
||||||
// update the pagination after the domain requests list is updated
|
// update the pagination after the domain requests list is updated
|
||||||
updatePagination(
|
updatePagination(
|
||||||
'domain request',
|
'domain request',
|
||||||
'#domain-requests-pagination',
|
'#domain-requests-pagination',
|
||||||
'#domain-requests-pagination .usa-pagination__counter',
|
'#domain-requests-pagination .usa-pagination__counter',
|
||||||
'#domain-requests-header',
|
'#domain-requests',
|
||||||
loadDomainRequests,
|
loadDomainRequests,
|
||||||
data.page,
|
data.page,
|
||||||
data.num_pages,
|
data.num_pages,
|
||||||
|
@ -1577,13 +1715,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
currentSearchTerm = domainRequestsSearchInput.value;
|
currentSearchTerm = domainRequestsSearchInput.value;
|
||||||
// If the search is blank, we match the resetSearch functionality
|
// If the search is blank, we match the resetSearch functionality
|
||||||
if (currentSearchTerm) {
|
if (currentSearchTerm) {
|
||||||
showElement(resetButton);
|
showElement(resetSearchButton);
|
||||||
} else {
|
} else {
|
||||||
hideElement(resetButton);
|
hideElement(resetSearchButton);
|
||||||
}
|
}
|
||||||
loadDomainRequests(1, 'id', 'asc');
|
loadDomainRequests(1, 'id', 'asc');
|
||||||
resetHeaders();
|
resetHeaders();
|
||||||
})
|
});
|
||||||
|
|
||||||
// Reset UI and accessibility
|
// Reset UI and accessibility
|
||||||
function resetHeaders() {
|
function resetHeaders() {
|
||||||
|
@ -1598,24 +1736,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
function resetSearch() {
|
function resetSearch() {
|
||||||
domainRequestsSearchInput.value = '';
|
domainRequestsSearchInput.value = '';
|
||||||
currentSearchTerm = '';
|
currentSearchTerm = '';
|
||||||
hideElement(resetButton);
|
hideElement(resetSearchButton);
|
||||||
loadDomainRequests(1, 'id', 'asc', hasLoaded, '');
|
loadDomainRequests(1, 'id', 'asc');
|
||||||
resetHeaders();
|
resetHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resetButton) {
|
if (resetSearchButton) {
|
||||||
resetButton.addEventListener('click', function() {
|
resetSearchButton.addEventListener('click', function() {
|
||||||
resetSearch();
|
resetSearch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the first page initially
|
// Initial load
|
||||||
loadDomainRequests(1);
|
loadDomainRequests(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IIFE that displays confirmation modal on the user profile page
|
* An IIFE that displays confirmation modal on the user profile page
|
||||||
*/
|
*/
|
||||||
|
|
33
src/registrar/assets/sass/_theme/_accordions.scss
Normal file
33
src/registrar/assets/sass/_theme/_accordions.scss
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
|
.usa-accordion--select {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
position: relative;
|
||||||
|
.usa-accordion__button[aria-expanded=false],
|
||||||
|
.usa-accordion__button[aria-expanded=false]:hover,
|
||||||
|
.usa-accordion__button[aria-expanded=true],
|
||||||
|
.usa-accordion__button[aria-expanded=true]:hover {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
.usa-accordion__content {
|
||||||
|
// Note, width is determined by a custom width class on one of the children
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 33.88px;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px color('base-lighter');
|
||||||
|
padding: units(2) units(2) units(3) units(2);
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: size('body', 'sm');
|
||||||
|
}
|
||||||
|
.usa-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.margin-top-0 {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -83,6 +83,10 @@ body {
|
||||||
padding: 0 units(2) units(3);
|
padding: 0 units(2) units(3);
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
|
|
||||||
|
&.margin-top-0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: color('primary-dark');
|
color: color('primary-dark');
|
||||||
margin-top: units(2);
|
margin-top: units(2);
|
||||||
|
@ -96,6 +100,10 @@ body {
|
||||||
@include at-media(mobile-lg) {
|
@include at-media(mobile-lg) {
|
||||||
margin-top: units(5);
|
margin-top: units(5);
|
||||||
|
|
||||||
|
&.margin-top-0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
@ -211,3 +219,7 @@ abbr[title] {
|
||||||
.usa-logo button.usa-button--unstyled.disabled-button:hover{
|
.usa-logo button.usa-button--unstyled.disabled-button:hover{
|
||||||
color: #{$dhs-dark-gray-85};
|
color: #{$dhs-dark-gray-85};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.padding--8-8-9 {
|
||||||
|
padding: 8px 8px 9px !important;
|
||||||
|
}
|
||||||
|
|
|
@ -161,3 +161,19 @@ a.usa-button--unstyled:visited {
|
||||||
margin-left: units(2);
|
margin-left: units(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-button--filter {
|
||||||
|
width: auto;
|
||||||
|
// For mobile stacking
|
||||||
|
margin-bottom: units(1);
|
||||||
|
border: solid 1px color('base-light') !important;
|
||||||
|
padding: units(1);
|
||||||
|
color: color('primary-darker') !important;
|
||||||
|
font-weight: font-weight('normal');
|
||||||
|
font-size: size('ui', 'xs');
|
||||||
|
box-shadow: none;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
td .no-click-outline-and-cursor-help {
|
td .no-click-outline-and-cursor-help {
|
||||||
outline: none;
|
|
||||||
cursor: help;
|
cursor: help;
|
||||||
use {
|
use {
|
||||||
// USWDS has weird interactions with SVGs regarding tooltips,
|
// USWDS has weird interactions with SVGs regarding tooltips,
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
@forward "typography";
|
@forward "typography";
|
||||||
@forward "links";
|
@forward "links";
|
||||||
@forward "lists";
|
@forward "lists";
|
||||||
|
@forward "accordions";
|
||||||
@forward "buttons";
|
@forward "buttons";
|
||||||
@forward "pagination";
|
@forward "pagination";
|
||||||
@forward "forms";
|
@forward "forms";
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-07-02 16:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0108_domaininformation_authorizing_official_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="sub_organization",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="The suborganization that this domain is included under",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="information_sub_organization",
|
||||||
|
to="registrar.suborganization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="sub_organization",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="The suborganization that this domain request is included under",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="request_sub_organization",
|
||||||
|
to="registrar.suborganization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="portfolio",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Portfolio associated with this domain",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="information_portfolio",
|
||||||
|
to="registrar.portfolio",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="approved_domain",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Domain associated with this request; will be blank until request is approved",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="domain_request_approved_domain",
|
||||||
|
to="registrar.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="portfolio",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Portfolio associated with this domain request",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="DomainRequest_portfolio",
|
||||||
|
to="registrar.portfolio",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="requested_domain",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="domain_request_requested_domain",
|
||||||
|
to="registrar.draftdomain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -63,10 +63,19 @@ class DomainInformation(TimeStampedModel):
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="DomainRequest_portfolio",
|
related_name="information_portfolio",
|
||||||
help_text="Portfolio associated with this domain",
|
help_text="Portfolio associated with this domain",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sub_organization = models.ForeignKey(
|
||||||
|
"registrar.Suborganization",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="information_sub_organization",
|
||||||
|
help_text="The suborganization that this domain is included under",
|
||||||
|
)
|
||||||
|
|
||||||
domain_request = models.OneToOneField(
|
domain_request = models.OneToOneField(
|
||||||
"registrar.DomainRequest",
|
"registrar.DomainRequest",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
@ -361,6 +370,10 @@ class DomainInformation(TimeStampedModel):
|
||||||
# domain_request, if so short circuit the create
|
# domain_request, if so short circuit the create
|
||||||
existing_domain_info = cls.objects.filter(domain_request__id=domain_request.id).first()
|
existing_domain_info = cls.objects.filter(domain_request__id=domain_request.id).first()
|
||||||
if existing_domain_info:
|
if existing_domain_info:
|
||||||
|
logger.info(
|
||||||
|
f"create_from_da() -> Shortcircuting create on {existing_domain_info}. "
|
||||||
|
"This record already exists. No values updated!"
|
||||||
|
)
|
||||||
return existing_domain_info
|
return existing_domain_info
|
||||||
|
|
||||||
# Get the fields that exist on both DomainRequest and DomainInformation
|
# Get the fields that exist on both DomainRequest and DomainInformation
|
||||||
|
|
|
@ -315,10 +315,19 @@ class DomainRequest(TimeStampedModel):
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="DomainInformation_portfolio",
|
related_name="DomainRequest_portfolio",
|
||||||
help_text="Portfolio associated with this domain request",
|
help_text="Portfolio associated with this domain request",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sub_organization = models.ForeignKey(
|
||||||
|
"registrar.Suborganization",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="request_sub_organization",
|
||||||
|
help_text="The suborganization that this domain request is included under",
|
||||||
|
)
|
||||||
|
|
||||||
# This is the domain request user who created this domain request. The contact
|
# This is the domain request user who created this domain request. The contact
|
||||||
# information that they gave is in the `submitter` field
|
# information that they gave is in the `submitter` field
|
||||||
creator = models.ForeignKey(
|
creator = models.ForeignKey(
|
||||||
|
@ -444,7 +453,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Domain associated with this request; will be blank until request is approved",
|
help_text="Domain associated with this request; will be blank until request is approved",
|
||||||
related_name="domain_request",
|
related_name="domain_request_approved_domain",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -452,7 +461,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
"DraftDomain",
|
"DraftDomain",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="domain_request",
|
related_name="domain_request_requested_domain",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<section class="section--outlined domain-requests">
|
<section class="section--outlined domain-requests" id="domain-requests">
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
{% if portfolio is None %}
|
{% if portfolio is None %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
|
@ -11,10 +11,10 @@
|
||||||
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
||||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-button display-none" type="button">
|
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-search display-none" type="button">
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
<label class="usa-sr-only" for="domain-requests__search-field">Search</label>
|
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
||||||
<input
|
<input
|
||||||
class="usa-input"
|
class="usa-input"
|
||||||
id="domain-requests__search-field"
|
id="domain-requests__search-field"
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="domain-requests__table-wrapper display-none">
|
<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">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
<p>You haven't requested any domains.</p>
|
<p>You haven't requested any domains.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="domain-requests__no-search-results display-none">
|
<div class="domain-requests__no-search-results display-none">
|
||||||
<p>No results found for "<span class="domain-requests__search-term"></span>"</p>
|
<p>No results found</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<section class="section--outlined domains">
|
<section class="section--outlined domains{% if portfolio is not None %} margin-top-0{% endif %}" id="domains">
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
{% if portfolio is None %}
|
{% if portfolio is None %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
|
@ -11,10 +11,10 @@
|
||||||
<section aria-label="Domains search component" class="flex-6 margin-y-2">
|
<section aria-label="Domains search component" class="flex-6 margin-y-2">
|
||||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-button display-none" type="button">
|
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-search display-none" type="button">
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
<label class="usa-sr-only" for="domains__search-field">Search</label>
|
<label class="usa-sr-only" for="domains__search-field">Search by domain name</label>
|
||||||
<input
|
<input
|
||||||
class="usa-input"
|
class="usa-input"
|
||||||
id="domains__search-field"
|
id="domains__search-field"
|
||||||
|
@ -33,7 +33,102 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="domains__table-wrapper display-none">
|
{% if portfolio %}
|
||||||
|
<div class="display-flex flex-align-center margin-top-1">
|
||||||
|
<span class="margin-right-2 margin-top-neg-1 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="domain__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-dns-needed"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="unknown"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-dns-needed"
|
||||||
|
>DNS Needed</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-ready"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="ready"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-ready"
|
||||||
|
>Ready</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-on-hold"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="on hold"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-on-hold"
|
||||||
|
>On hold</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-expired"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="expired"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-expired"
|
||||||
|
>Expired</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-deleted"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="deleted"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-deleted"
|
||||||
|
>Deleted</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 domains__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="domains__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 domains__table">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domains__table">
|
||||||
<caption class="sr-only">Your registered domains</caption>
|
<caption class="sr-only">Your registered domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -70,7 +165,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="domains__no-search-results display-none">
|
<div class="domains__no-search-results display-none">
|
||||||
<p>No results found for "<span class="domains__search-term"></span>"</p>
|
<p>No results found</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
<h1>Domains</h1>
|
<h1 id="domains-header">Domains</h1>
|
||||||
{% include "includes/domains_table.html" with portfolio=portfolio %}
|
{% include "includes/domains_table.html" with portfolio=portfolio %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
<h1>Domain requests</h1>
|
<h1 id="domain-requests-header">Domain requests</h1>
|
||||||
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="margin-bottom-4 tablet:margin-bottom-0">
|
<div class="margin-bottom-4 tablet:margin-bottom-0">
|
||||||
<nav aria-label="">
|
<nav aria-label="">
|
||||||
<h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2>
|
<h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2>
|
||||||
<ul class="usa-sidenav">
|
<ul class="usa-sidenav usa-sidenav--portfolio">
|
||||||
<li class="usa-sidenav__item">
|
<li class="usa-sidenav__item">
|
||||||
{% url 'portfolio-domains' portfolio.id as url %}
|
{% url 'portfolio-domains' portfolio.id as url %}
|
||||||
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
||||||
|
|
|
@ -2249,6 +2249,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"action_needed_reason_email",
|
"action_needed_reason_email",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
"portfolio",
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
"creator",
|
"creator",
|
||||||
"investigator",
|
"investigator",
|
||||||
"generic_org_type",
|
"generic_org_type",
|
||||||
|
@ -3557,7 +3558,7 @@ class TestMyUserAdmin(MockDb):
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||||
("Permissions", {"fields": ("is_active", "groups")}),
|
("Permissions", {"fields": ("is_active", "groups")}),
|
||||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||||
)
|
)
|
||||||
|
@ -4048,9 +4049,7 @@ class TestContactAdmin(TestCase):
|
||||||
|
|
||||||
readonly_fields = self.admin.get_readonly_fields(request)
|
readonly_fields = self.admin.get_readonly_fields(request)
|
||||||
|
|
||||||
expected_fields = [
|
expected_fields = ["user", "email"]
|
||||||
"user",
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
|
|
@ -735,7 +735,6 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
csv_file.seek(0)
|
csv_file.seek(0)
|
||||||
# Read the content into a variable
|
# Read the content into a variable
|
||||||
csv_content = csv_file.read()
|
csv_content = csv_file.read()
|
||||||
self.maxDiff = None
|
|
||||||
expected_content = (
|
expected_content = (
|
||||||
# Header
|
# Header
|
||||||
"Domain request,Submitted at,Status,Domain type,Federal type,"
|
"Domain request,Submitted at,Status,Domain type,Federal type,"
|
||||||
|
|
|
@ -965,7 +965,7 @@ class PortfoliosTests(TestWithUser, WebTest):
|
||||||
# Assert that we're on the right page
|
# Assert that we're on the right page
|
||||||
self.assertContains(portfolio_page, self.portfolio.organization_name)
|
self.assertContains(portfolio_page, self.portfolio.organization_name)
|
||||||
|
|
||||||
self.assertContains(portfolio_page, "<h1>Domains</h1>")
|
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_no_redirect_when_org_flag_false(self):
|
def test_no_redirect_when_org_flag_false(self):
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.urls import reverse
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
from django.utils.dateparse import parse_date
|
from django.utils.dateparse import parse_date
|
||||||
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
|
||||||
|
|
||||||
class GetDomainsJsonTest(TestWithUser, WebTest):
|
class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
|
@ -11,9 +12,9 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
# Create test domains
|
# Create test domains
|
||||||
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="active")
|
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="unknown")
|
||||||
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="inactive")
|
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="dns needed")
|
||||||
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="active")
|
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready")
|
||||||
|
|
||||||
# Create UserDomainRoles
|
# Create UserDomainRoles
|
||||||
UserDomainRole.objects.create(user=self.user, domain=self.domain1)
|
UserDomainRole.objects.create(user=self.user, domain=self.domain1)
|
||||||
|
@ -25,6 +26,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_get_domains_json_unauthenticated(self):
|
def test_get_domains_json_unauthenticated(self):
|
||||||
"""for an unauthenticated user, test that the user is redirected for auth"""
|
"""for an unauthenticated user, test that the user is redirected for auth"""
|
||||||
self.app.reset()
|
self.app.reset()
|
||||||
|
@ -32,6 +34,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
response = self.client.get(reverse("get_domains_json"))
|
response = self.client.get(reverse("get_domains_json"))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_get_domains_json_authenticated(self):
|
def test_get_domains_json_authenticated(self):
|
||||||
"""Test that an authenticated user gets the list of 3 domains."""
|
"""Test that an authenticated user gets the list of 3 domains."""
|
||||||
response = self.app.get(reverse("get_domains_json"))
|
response = self.app.get(reverse("get_domains_json"))
|
||||||
|
@ -102,6 +105,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
)
|
)
|
||||||
self.assertEqual(svg_icon_expected, svg_icons[i])
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_get_domains_json_search(self):
|
def test_get_domains_json_search(self):
|
||||||
"""Test search."""
|
"""Test search."""
|
||||||
# Define your URL variables as a dictionary
|
# Define your URL variables as a dictionary
|
||||||
|
@ -131,6 +135,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
domains[0],
|
domains[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_pagination(self):
|
def test_pagination(self):
|
||||||
"""Test that pagination is correct in the response"""
|
"""Test that pagination is correct in the response"""
|
||||||
response = self.app.get(reverse("get_domains_json"), {"page": 1})
|
response = self.app.get(reverse("get_domains_json"), {"page": 1})
|
||||||
|
@ -143,6 +148,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_sorting(self):
|
def test_sorting(self):
|
||||||
"""test that sorting works properly in the response"""
|
"""test that sorting works properly in the response"""
|
||||||
response = self.app.get(reverse("get_domains_json"), {"sort_by": "expiration_date", "order": "desc"})
|
response = self.app.get(reverse("get_domains_json"), {"sort_by": "expiration_date", "order": "desc"})
|
||||||
|
@ -161,6 +167,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
|
expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
|
||||||
self.assertEqual(expiration_dates, sorted(expiration_dates))
|
self.assertEqual(expiration_dates, sorted(expiration_dates))
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_sorting_by_state_display(self):
|
def test_sorting_by_state_display(self):
|
||||||
"""test that the state_display sorting works properly"""
|
"""test that the state_display sorting works properly"""
|
||||||
response = self.app.get(reverse("get_domains_json"), {"sort_by": "state_display", "order": "asc"})
|
response = self.app.get(reverse("get_domains_json"), {"sort_by": "state_display", "order": "asc"})
|
||||||
|
@ -178,3 +185,21 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
# Check if sorted by state_display in descending order
|
# Check if sorted by state_display in descending order
|
||||||
states = [domain["state_display"] for domain in data["domains"]]
|
states = [domain["state_display"] for domain in data["domains"]]
|
||||||
self.assertEqual(states, sorted(states, reverse=True))
|
self.assertEqual(states, sorted(states, reverse=True))
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_state_filtering(self):
|
||||||
|
"""Test that different states in request get expected responses."""
|
||||||
|
|
||||||
|
expected_values = [
|
||||||
|
("unknown", 1),
|
||||||
|
("ready", 0),
|
||||||
|
("expired", 2),
|
||||||
|
("ready,expired", 2),
|
||||||
|
("unknown,expired", 3),
|
||||||
|
]
|
||||||
|
for state, num_domains in expected_values:
|
||||||
|
with self.subTest(state=state, num_domains=num_domains):
|
||||||
|
response = self.app.get(reverse("get_domains_json"), {"status": state})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domains"]), num_domains)
|
||||||
|
|
|
@ -20,11 +20,46 @@ def get_domains_json(request):
|
||||||
# Handle sorting
|
# Handle sorting
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
search_term = request.GET.get("search_term")
|
|
||||||
|
|
||||||
|
# Handle search term
|
||||||
|
search_term = request.GET.get("search_term")
|
||||||
if search_term:
|
if search_term:
|
||||||
objects = objects.filter(Q(name__icontains=search_term))
|
objects = objects.filter(Q(name__icontains=search_term))
|
||||||
|
|
||||||
|
# Handle state
|
||||||
|
status_param = request.GET.get("status")
|
||||||
|
if status_param:
|
||||||
|
status_list = status_param.split(",")
|
||||||
|
|
||||||
|
# if unknown is in status_list, append 'dns needed' since both
|
||||||
|
# unknown and dns needed display as DNS Needed, and both are
|
||||||
|
# searchable via state parameter of 'unknown'
|
||||||
|
if "unknown" in status_list:
|
||||||
|
status_list.append("dns needed")
|
||||||
|
|
||||||
|
# Split the status list into normal states and custom states
|
||||||
|
normal_states = [state for state in status_list if state in Domain.State.values]
|
||||||
|
custom_states = [state for state in status_list if state == "expired"]
|
||||||
|
|
||||||
|
# Construct Q objects for normal states that can be queried through ORM
|
||||||
|
state_query = Q()
|
||||||
|
if normal_states:
|
||||||
|
state_query |= Q(state__in=normal_states)
|
||||||
|
|
||||||
|
# Handle custom states in Python, as expired can not be queried through ORM
|
||||||
|
if "expired" in custom_states:
|
||||||
|
expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"]
|
||||||
|
state_query |= Q(id__in=expired_domain_ids)
|
||||||
|
|
||||||
|
# Apply the combined query
|
||||||
|
objects = objects.filter(state_query)
|
||||||
|
|
||||||
|
# If there are filtered states, and expired is not one of them, domains with
|
||||||
|
# state_display of 'Expired' must be removed
|
||||||
|
if "expired" not in custom_states:
|
||||||
|
expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"]
|
||||||
|
objects = objects.exclude(id__in=expired_domain_ids)
|
||||||
|
|
||||||
if sort_by == "state_display":
|
if sort_by == "state_display":
|
||||||
# Fetch the objects and sort them in Python
|
# Fetch the objects and sort them in Python
|
||||||
objects = list(objects) # Evaluate queryset to a list
|
objects = list(objects) # Evaluate queryset to a list
|
||||||
|
|
|
@ -68,6 +68,8 @@
|
||||||
10038 OUTOFSCOPE http://app:8080/dns/dnssec
|
10038 OUTOFSCOPE http://app:8080/dns/dnssec
|
||||||
10038 OUTOFSCOPE http://app:8080/dns/dnssec/dsdata
|
10038 OUTOFSCOPE http://app:8080/dns/dnssec/dsdata
|
||||||
10038 OUTOFSCOPE http://app:8080/org-name-address
|
10038 OUTOFSCOPE http://app:8080/org-name-address
|
||||||
|
10038 OUTOFSCOPE http://app:8080/domain_requests/
|
||||||
|
10038 OUTOFSCOPE http://app:8080/domains/
|
||||||
# This URL always returns 404, so include it as well.
|
# This URL always returns 404, so include it as well.
|
||||||
10038 OUTOFSCOPE http://app:8080/todo
|
10038 OUTOFSCOPE http://app:8080/todo
|
||||||
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue