mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-21 18:25:58 +02:00
Merge branch 'main' of https://github.com/cisagov/manage.get.gov into es/3160-confirmation-page-content
This commit is contained in:
commit
a7cab25390
28 changed files with 973 additions and 67 deletions
|
@ -16,7 +16,7 @@ function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, a
|
||||||
statusDropdown.value = valueToCheck;
|
statusDropdown.value = valueToCheck;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("displayModalOnDropdownClick() -> Cancel button was null");
|
console.warn("displayModalOnDropdownClick() -> Cancel button was null");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a change event listener to the dropdown.
|
// Add a change event listener to the dropdown.
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { initDomainsTable } from './table-domains.js';
|
||||||
import { initDomainRequestsTable } from './table-domain-requests.js';
|
import { initDomainRequestsTable } from './table-domain-requests.js';
|
||||||
import { initMembersTable } from './table-members.js';
|
import { initMembersTable } from './table-members.js';
|
||||||
import { initMemberDomainsTable } from './table-member-domains.js';
|
import { initMemberDomainsTable } from './table-member-domains.js';
|
||||||
|
import { initEditMemberDomainsTable } from './table-edit-member-domains.js';
|
||||||
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
|
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
|
||||||
import { initAddNewMemberPageListeners } from './portfolio-member-page.js';
|
import { initAddNewMemberPageListeners } from './portfolio-member-page.js';
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ initDomainsTable();
|
||||||
initDomainRequestsTable();
|
initDomainRequestsTable();
|
||||||
initMembersTable();
|
initMembersTable();
|
||||||
initMemberDomainsTable();
|
initMemberDomainsTable();
|
||||||
|
initEditMemberDomainsTable();
|
||||||
|
|
||||||
initPortfolioMemberPageToggle();
|
initPortfolioMemberPageToggle();
|
||||||
initAddNewMemberPageListeners();
|
initAddNewMemberPageListeners();
|
||||||
|
|
|
@ -49,7 +49,7 @@ export function initPortfolioMemberPageToggle() {
|
||||||
* on the Add New Member page.
|
* on the Add New Member page.
|
||||||
*/
|
*/
|
||||||
export function initAddNewMemberPageListeners() {
|
export function initAddNewMemberPageListeners() {
|
||||||
add_member_form = document.getElementById("add_member_form")
|
let add_member_form = document.getElementById("add_member_form");
|
||||||
if (!add_member_form){
|
if (!add_member_form){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,6 +126,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
||||||
export class BaseTable {
|
export class BaseTable {
|
||||||
constructor(itemName) {
|
constructor(itemName) {
|
||||||
this.itemName = itemName;
|
this.itemName = itemName;
|
||||||
|
this.displayName = itemName;
|
||||||
this.sectionSelector = itemName + 's';
|
this.sectionSelector = itemName + 's';
|
||||||
this.tableWrapper = document.getElementById(`${this.sectionSelector}__table-wrapper`);
|
this.tableWrapper = document.getElementById(`${this.sectionSelector}__table-wrapper`);
|
||||||
this.tableHeaders = document.querySelectorAll(`#${this.sectionSelector} th[data-sortable]`);
|
this.tableHeaders = document.querySelectorAll(`#${this.sectionSelector} th[data-sortable]`);
|
||||||
|
@ -183,7 +184,7 @@ export class BaseTable {
|
||||||
// Counter should only be displayed if there is more than 1 item
|
// Counter should only be displayed if there is more than 1 item
|
||||||
paginationSelectorEl.classList.toggle('display-none', totalItems < 1);
|
paginationSelectorEl.classList.toggle('display-none', totalItems < 1);
|
||||||
|
|
||||||
counterSelectorEl.innerHTML = `${totalItems} ${this.itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`;
|
counterSelectorEl.innerHTML = `${totalItems} ${this.displayName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`;
|
||||||
|
|
||||||
// Helper function to create a pagination item
|
// Helper function to create a pagination item
|
||||||
const createPaginationItem = (page) => {
|
const createPaginationItem = (page) => {
|
||||||
|
@ -416,6 +417,11 @@ export class BaseTable {
|
||||||
*/
|
*/
|
||||||
initShowMoreButtons(){}
|
initShowMoreButtons(){}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See function for more details
|
||||||
|
*/
|
||||||
|
initCheckboxListeners(){}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads rows in the members list, as well as updates pagination around the members list
|
* Loads rows in the members list, as well as updates pagination around the members list
|
||||||
* based on the supplied attributes.
|
* based on the supplied attributes.
|
||||||
|
@ -431,7 +437,7 @@ export class BaseTable {
|
||||||
let searchParams = this.getSearchParams(page, sortBy, order, searchTerm, status, portfolio);
|
let searchParams = this.getSearchParams(page, sortBy, order, searchTerm, status, portfolio);
|
||||||
|
|
||||||
// --------- FETCH DATA
|
// --------- FETCH DATA
|
||||||
// fetch json of page of domains, given params
|
// fetch json of page of objects, given params
|
||||||
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
|
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
|
||||||
if (!baseUrlValue) return;
|
if (!baseUrlValue) return;
|
||||||
|
|
||||||
|
@ -462,6 +468,7 @@ export class BaseTable {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initShowMoreButtons();
|
this.initShowMoreButtons();
|
||||||
|
this.initCheckboxListeners();
|
||||||
|
|
||||||
this.loadModals(data.page, data.total, data.unfiltered_total);
|
this.loadModals(data.page, data.total, data.unfiltered_total);
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ export class DomainRequestsTable extends BaseTable {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('domain-request');
|
super('domain-request');
|
||||||
|
this.displayName = "domain request";
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseUrl() {
|
getBaseUrl() {
|
||||||
|
|
234
src/registrar/assets/src/js/getgov/table-edit-member-domains.js
Normal file
234
src/registrar/assets/src/js/getgov/table-edit-member-domains.js
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
|
||||||
|
import { BaseTable } from './table-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EditMemberDomainsTable is used for PortfolioMember and PortfolioInvitedMember
|
||||||
|
* Domain Editing.
|
||||||
|
*
|
||||||
|
* This table has additional functionality for tracking and making changes
|
||||||
|
* to domains assigned to the member/invited member.
|
||||||
|
*/
|
||||||
|
export class EditMemberDomainsTable extends BaseTable {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('edit-member-domain');
|
||||||
|
this.displayName = "domain";
|
||||||
|
this.currentSortBy = 'name';
|
||||||
|
this.initialDomainAssignments = []; // list of initially assigned domains
|
||||||
|
this.initialDomainAssignmentsOnlyMember = []; // list of initially assigned domains which are readonly
|
||||||
|
this.addedDomains = []; // list of domains added to member
|
||||||
|
this.removedDomains = []; // list of domains removed from member
|
||||||
|
this.initializeDomainAssignments();
|
||||||
|
this.initCancelEditDomainAssignmentButton();
|
||||||
|
}
|
||||||
|
getBaseUrl() {
|
||||||
|
return document.getElementById("get_member_domains_json_url");
|
||||||
|
}
|
||||||
|
getDataObjects(data) {
|
||||||
|
return data.domains;
|
||||||
|
}
|
||||||
|
/** getDomainAssignmentSearchParams is used to prepare search to populate
|
||||||
|
* initialDomainAssignments and initialDomainAssignmentsOnlyMember
|
||||||
|
*
|
||||||
|
* searches with memberOnly True so that only domains assigned to the member are returned
|
||||||
|
*/
|
||||||
|
getDomainAssignmentSearchParams(portfolio) {
|
||||||
|
let searchParams = new URLSearchParams();
|
||||||
|
let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null;
|
||||||
|
let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null;
|
||||||
|
let memberOnly = true;
|
||||||
|
if (portfolio)
|
||||||
|
searchParams.append("portfolio", portfolio);
|
||||||
|
if (emailValue)
|
||||||
|
searchParams.append("email", emailValue);
|
||||||
|
if (memberIdValue)
|
||||||
|
searchParams.append("member_id", memberIdValue);
|
||||||
|
if (memberOnly)
|
||||||
|
searchParams.append("member_only", memberOnly);
|
||||||
|
return searchParams;
|
||||||
|
}
|
||||||
|
/** getSearchParams extends base class getSearchParams.
|
||||||
|
*
|
||||||
|
* additional searchParam for this table is checkedDomains. This is used to allow
|
||||||
|
* for backend sorting by domains which are 'checked' in the form.
|
||||||
|
*/
|
||||||
|
getSearchParams(page, sortBy, order, searchTerm, status, portfolio) {
|
||||||
|
let searchParams = super.getSearchParams(page, sortBy, order, searchTerm, status, portfolio);
|
||||||
|
// Add checkedDomains to searchParams
|
||||||
|
// Clone the initial domains to avoid mutating them
|
||||||
|
let checkedDomains = [...this.initialDomainAssignments];
|
||||||
|
// Add IDs from addedDomains that are not already in checkedDomains
|
||||||
|
this.addedDomains.forEach(domain => {
|
||||||
|
if (!checkedDomains.includes(domain.id)) {
|
||||||
|
checkedDomains.push(domain.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Remove IDs from removedDomains
|
||||||
|
this.removedDomains.forEach(domain => {
|
||||||
|
const index = checkedDomains.indexOf(domain.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
checkedDomains.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Append updated checkedDomain IDs to searchParams
|
||||||
|
if (checkedDomains.length > 0) {
|
||||||
|
searchParams.append("checkedDomainIds", checkedDomains.join(","));
|
||||||
|
}
|
||||||
|
return searchParams;
|
||||||
|
}
|
||||||
|
addRow(dataObject, tbody, customTableOptions) {
|
||||||
|
const domain = dataObject;
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
let checked = false;
|
||||||
|
let disabled = false;
|
||||||
|
if (
|
||||||
|
(this.initialDomainAssignments.includes(domain.id) ||
|
||||||
|
this.addedDomains.map(obj => obj.id).includes(domain.id)) &&
|
||||||
|
!this.removedDomains.map(obj => obj.id).includes(domain.id)
|
||||||
|
) {
|
||||||
|
checked = true;
|
||||||
|
}
|
||||||
|
if (this.initialDomainAssignmentsOnlyMember.includes(domain.id)) {
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td data-label="Selection" data-sort-value="0" class="padding-right-105">
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="${domain.id}"
|
||||||
|
type="checkbox"
|
||||||
|
name="${domain.name}"
|
||||||
|
value="${domain.id}"
|
||||||
|
${checked ? 'checked' : ''}
|
||||||
|
${disabled ? 'disabled' : ''}
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label margin-top-0" for="${domain.id}">
|
||||||
|
<span class="sr-only">${domain.id}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td data-label="Domain name">
|
||||||
|
${domain.name}
|
||||||
|
${disabled ? '<span class="display-block margin-top-05 text-gray-50">Domains must have one domain manager. To unassign this member, the domain needs another domain manager.</span>' : ''}
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* initializeDomainAssignments searches via ajax on page load for domains assigned to
|
||||||
|
* member. It populates both initialDomainAssignments and initialDomainAssignmentsOnlyMember.
|
||||||
|
* It is called once per page load, but not called with subsequent table changes.
|
||||||
|
*/
|
||||||
|
initializeDomainAssignments() {
|
||||||
|
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
|
||||||
|
if (!baseUrlValue) return;
|
||||||
|
let searchParams = this.getDomainAssignmentSearchParams(this.portfolioValue);
|
||||||
|
let url = baseUrlValue + "?" + searchParams.toString();
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error in AJAX call: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataObjects = this.getDataObjects(data);
|
||||||
|
// Map the id attributes of dataObjects to this.initialDomainAssignments
|
||||||
|
this.initialDomainAssignments = dataObjects.map(obj => obj.id);
|
||||||
|
this.initialDomainAssignmentsOnlyMember = dataObjects
|
||||||
|
.filter(obj => obj.member_is_only_manager)
|
||||||
|
.map(obj => obj.id);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching domain assignments:', error));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initializes listeners on checkboxes in the table. Checkbox listeners are used
|
||||||
|
* in this case to track changes to domain assignments in js (addedDomains and removedDomains)
|
||||||
|
* before changes are saved.
|
||||||
|
* initCheckboxListeners is called each time table is loaded.
|
||||||
|
*/
|
||||||
|
initCheckboxListeners() {
|
||||||
|
const checkboxes = this.tableWrapper.querySelectorAll('input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
const domain = { id: +checkbox.value, name: checkbox.name };
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
this.updateDomainLists(domain, this.removedDomains, this.addedDomains);
|
||||||
|
} else {
|
||||||
|
this.updateDomainLists(domain, this.addedDomains, this.removedDomains);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Helper function which updates domain lists. When called, if domain is in the fromList,
|
||||||
|
* it removes it; if domain is not in the toList, it is added to the toList.
|
||||||
|
* @param {*} domain - object containing the domain id and name
|
||||||
|
* @param {*} fromList - list of domains
|
||||||
|
* @param {*} toList - list of domains
|
||||||
|
*/
|
||||||
|
updateDomainLists(domain, fromList, toList) {
|
||||||
|
const index = fromList.findIndex(item => item.id === domain.id && item.name === domain.name);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
fromList.splice(index, 1); // Remove from the `fromList` if it exists
|
||||||
|
} else {
|
||||||
|
toList.push(domain); // Add to the `toList` if not already there
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* initializes the Cancel button on the Edit domains page.
|
||||||
|
* Cancel triggers modal in certain conditions and the initialization for the modal is done
|
||||||
|
* in this function.
|
||||||
|
*/
|
||||||
|
initCancelEditDomainAssignmentButton() {
|
||||||
|
const cancelEditDomainAssignmentButton = document.getElementById('cancel-edit-domain-assignments');
|
||||||
|
if (!cancelEditDomainAssignmentButton) {
|
||||||
|
console.error("Expected element #cancel-edit-domain-assignments, but it does not exist.");
|
||||||
|
return; // Exit early if the button doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the last breadcrumb link
|
||||||
|
const lastPageLinkElement = document.querySelector('.usa-breadcrumb__list-item:nth-last-child(2) a');
|
||||||
|
const lastPageLink = lastPageLinkElement ? lastPageLinkElement.getAttribute('href') : null;
|
||||||
|
|
||||||
|
const hiddenModalTrigger = document.getElementById("hidden-cancel-edit-domain-assignments-modal-trigger");
|
||||||
|
|
||||||
|
if (!lastPageLink) {
|
||||||
|
console.warn("Last breadcrumb link not found or missing href.");
|
||||||
|
}
|
||||||
|
if (!hiddenModalTrigger) {
|
||||||
|
console.warn("Hidden modal trigger not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click event listener
|
||||||
|
cancelEditDomainAssignmentButton.addEventListener('click', () => {
|
||||||
|
if (this.addedDomains.length || this.removedDomains.length) {
|
||||||
|
console.log('Changes detected. Triggering modal...');
|
||||||
|
hiddenModalTrigger.click();
|
||||||
|
} else if (lastPageLink) {
|
||||||
|
window.location.href = lastPageLink; // Redirect to the last breadcrumb link
|
||||||
|
} else {
|
||||||
|
console.warn("No changes detected, but no valid lastPageLink to navigate to.");
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initEditMemberDomainsTable() {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const isEditMemberDomainsPage = document.getElementById("edit-member-domains");
|
||||||
|
if (isEditMemberDomainsPage) {
|
||||||
|
const editMemberDomainsTable = new EditMemberDomainsTable();
|
||||||
|
if (editMemberDomainsTable.tableWrapper) {
|
||||||
|
// Initial load
|
||||||
|
editMemberDomainsTable.loadTable(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ export class MemberDomainsTable extends BaseTable {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('member-domain');
|
super('member-domain');
|
||||||
|
this.displayName = "domain";
|
||||||
this.currentSortBy = 'name';
|
this.currentSortBy = 'name';
|
||||||
}
|
}
|
||||||
getBaseUrl() {
|
getBaseUrl() {
|
||||||
|
|
|
@ -73,11 +73,15 @@ th {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td, th,
|
td, th {
|
||||||
.usa-tabel th{
|
|
||||||
padding: units(2) units(4) units(2) 0;
|
padding: units(2) units(4) units(2) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hack fix to the overly specific selector above that broke utility class usefulness
|
||||||
|
.padding-right-105 {
|
||||||
|
padding-right: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
thead tr:first-child th:first-child {
|
thead tr:first-child th:first-child {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,11 @@ urlpatterns = [
|
||||||
views.PortfolioMemberDomainsView.as_view(),
|
views.PortfolioMemberDomainsView.as_view(),
|
||||||
name="member-domains",
|
name="member-domains",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"member/<int:pk>/domains/edit",
|
||||||
|
views.PortfolioMemberDomainsEditView.as_view(),
|
||||||
|
name="member-domains-edit",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"invitedmember/<int:pk>",
|
"invitedmember/<int:pk>",
|
||||||
views.PortfolioInvitedMemberView.as_view(),
|
views.PortfolioInvitedMemberView.as_view(),
|
||||||
|
@ -129,6 +134,11 @@ urlpatterns = [
|
||||||
views.PortfolioInvitedMemberDomainsView.as_view(),
|
views.PortfolioInvitedMemberDomainsView.as_view(),
|
||||||
name="invitedmember-domains",
|
name="invitedmember-domains",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"invitedmember/<int:pk>/domains/edit",
|
||||||
|
views.PortfolioInvitedMemberDomainsEditView.as_view(),
|
||||||
|
name="invitedmember-domains-edit",
|
||||||
|
),
|
||||||
# path(
|
# path(
|
||||||
# "no-organization-members/",
|
# "no-organization-members/",
|
||||||
# views.PortfolioNoMembersView.as_view(),
|
# views.PortfolioNoMembersView.as_view(),
|
||||||
|
|
|
@ -99,7 +99,7 @@ def portfolio_permissions(request):
|
||||||
|
|
||||||
|
|
||||||
def is_widescreen_mode(request):
|
def is_widescreen_mode(request):
|
||||||
widescreen_paths = []
|
widescreen_paths = [] # If this list is meant to include specific paths, populate it.
|
||||||
portfolio_widescreen_paths = [
|
portfolio_widescreen_paths = [
|
||||||
"/domains/",
|
"/domains/",
|
||||||
"/requests/",
|
"/requests/",
|
||||||
|
@ -108,10 +108,21 @@ def is_widescreen_mode(request):
|
||||||
"/no-organization-domains/",
|
"/no-organization-domains/",
|
||||||
"/domain-request/",
|
"/domain-request/",
|
||||||
]
|
]
|
||||||
|
# widescreen_paths can be a bear as it trickles down sub-urls. exclude_paths gives us a way out.
|
||||||
|
exclude_paths = [
|
||||||
|
"/domains/edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check if the current path matches a widescreen path or the root path.
|
||||||
is_widescreen = any(path in request.path for path in widescreen_paths) or request.path == "/"
|
is_widescreen = any(path in request.path for path in widescreen_paths) or request.path == "/"
|
||||||
is_portfolio_widescreen = bool(
|
|
||||||
|
# Check if the user is an organization user and the path matches portfolio paths.
|
||||||
|
is_portfolio_widescreen = (
|
||||||
hasattr(request.user, "is_org_user")
|
hasattr(request.user, "is_org_user")
|
||||||
and request.user.is_org_user(request)
|
and request.user.is_org_user(request)
|
||||||
and any(path in request.path for path in portfolio_widescreen_paths)
|
and any(path in request.path for path in portfolio_widescreen_paths)
|
||||||
|
and not any(exclude_path in request.path for exclude_path in exclude_paths)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return a dictionary with the widescreen mode status.
|
||||||
return {"is_widescreen_mode": is_widescreen or is_portfolio_widescreen}
|
return {"is_widescreen_mode": is_widescreen or is_portfolio_widescreen}
|
||||||
|
|
142
src/registrar/templates/includes/member_domains_edit_table.html
Normal file
142
src/registrar/templates/includes/member_domains_edit_table.html
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% if member %}
|
||||||
|
<span
|
||||||
|
id="portfolio-js-value"
|
||||||
|
class="display-none"
|
||||||
|
data-portfolio="{{ portfolio.id }}"
|
||||||
|
data-email=""
|
||||||
|
data-member-id="{{ member.id }}"
|
||||||
|
data-member-only="false"
|
||||||
|
></span>
|
||||||
|
{% else %}
|
||||||
|
<span
|
||||||
|
id="portfolio-js-value"
|
||||||
|
class="display-none"
|
||||||
|
data-portfolio="{{ portfolio.id }}"
|
||||||
|
data-email="{{ portfolio_invitation.email }}"
|
||||||
|
data-member-id=""
|
||||||
|
data-member-only="false"
|
||||||
|
></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||||
|
{% url 'get_member_domains_json' as url %}
|
||||||
|
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
||||||
|
<section class="section-outlined member-domains margin-top-0 section-outlined--border-base-light" id="edit-member-domains">
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Edit domains assigned to
|
||||||
|
{% if member %}
|
||||||
|
{{ member.email }}
|
||||||
|
{% else %}
|
||||||
|
{{ portfolio_invitation.email }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||||
|
<!-- ---------- SEARCH ---------- -->
|
||||||
|
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||||
|
<section aria-label="Member domains search component" class="margin-top-2">
|
||||||
|
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label class="usa-label display-block margin-bottom-05" for="edit-member-domains__search-field">
|
||||||
|
{% if has_edit_members_portfolio_permission %}
|
||||||
|
Search all domains
|
||||||
|
{% else %}
|
||||||
|
Search domains assigned to
|
||||||
|
{% if member %}
|
||||||
|
{{ member.email }}
|
||||||
|
{% else %}
|
||||||
|
{{ portfolio_invitation.email }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
<div class="usa-search--show-label__input-wrapper">
|
||||||
|
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="edit-member-domains__reset-search" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
class="usa-input"
|
||||||
|
id="edit-member-domains__search-field"
|
||||||
|
type="search"
|
||||||
|
name="member-domains-search"
|
||||||
|
/>
|
||||||
|
<button class="usa-button" type="submit" id="edit-member-domains__search-field-submit">
|
||||||
|
<span class="usa-search__submit-text">Search </span>
|
||||||
|
<img
|
||||||
|
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||||
|
class="usa-search__submit-icon"
|
||||||
|
alt="Search"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ---------- MAIN TABLE ---------- -->
|
||||||
|
<div class="display-none margin-top-0" id="edit-member-domains__table-wrapper">
|
||||||
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||||
|
<caption class="sr-only">member domains</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sortable="checked" scope="col" role="columnheader" class="padding-right-105"><span class="sr-only">Assigned domains</span></th>
|
||||||
|
<!-- We override default sort to be name/ascending in the JSON endpoint. We add the correct aria-sort attribute here to reflect that in the UI -->
|
||||||
|
<th data-sortable="name" scope="col" role="columnheader" aria-sort="descending">Domains</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- AJAX will populate this tbody -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div
|
||||||
|
class="usa-sr-only usa-table__announcement-region" id="edit-member-domains__usa-table__announcement-region"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="display-none" id="edit-member-domains__no-data">
|
||||||
|
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
||||||
|
</div>
|
||||||
|
<div class="display-none" id="edit-member-domains__no-search-results">
|
||||||
|
<p>No results found</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="edit-member-domains-pagination">
|
||||||
|
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||||
|
<!-- Count will be dynamically populated by JS -->
|
||||||
|
</span>
|
||||||
|
<ul class="usa-pagination__list">
|
||||||
|
<!-- Pagination links will be dynamically populated by JS -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<a
|
||||||
|
id="hidden-cancel-edit-domain-assignments-modal-trigger"
|
||||||
|
href="#cancel-edit-domain-assignments-modal"
|
||||||
|
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||||
|
aria-controls="cancel-edit-domain-assignments-modal"
|
||||||
|
data-open-modal
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="cancel-edit-domain-assignments-modal"
|
||||||
|
aria-labelledby="Are you sure you want to continue?"
|
||||||
|
aria-describedby="You have unsaved changes that will be lost."
|
||||||
|
>
|
||||||
|
{% if portfolio_permission %}
|
||||||
|
{% url 'member-domains' pk=portfolio_permission.id as url %}
|
||||||
|
{% else %}
|
||||||
|
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="You have unsaved changes that will be lost." modal_button_url=url modal_button_text="Continue without saving" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -36,21 +36,17 @@
|
||||||
|
|
||||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||||
<!-- ---------- SEARCH ---------- -->
|
<!-- ---------- SEARCH ---------- -->
|
||||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6">
|
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||||
<section aria-label="Members search component" class="margin-top-2">
|
<section aria-label="Member domains search component" class="margin-top-2">
|
||||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label class="usa-label display-block margin-bottom-05" for="member-domains__search-field">
|
<label class="usa-label display-block margin-bottom-05" for="member-domains__search-field">
|
||||||
{% if has_edit_members_portfolio_permission %}
|
|
||||||
Search all domains
|
|
||||||
{% else %}
|
|
||||||
Search domains assigned to
|
Search domains assigned to
|
||||||
{% if member %}
|
{% if member %}
|
||||||
{{ member.email }}
|
{{ member.email }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ portfolio_invitation.email }}
|
{{ portfolio_invitation.email }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</label>
|
</label>
|
||||||
<div class="usa-search--show-label__input-wrapper">
|
<div class="usa-search--show-label__input-wrapper">
|
||||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="member-domains__reset-search" type="button">
|
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="member-domains__reset-search" type="button">
|
||||||
|
|
|
@ -23,18 +23,24 @@
|
||||||
|
|
||||||
<div class="usa-modal__footer">
|
<div class="usa-modal__footer">
|
||||||
<ul class="usa-button-group">
|
<ul class="usa-button-group">
|
||||||
{% if not_form %}
|
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
|
{% if not_form and modal_button %}
|
||||||
{{ modal_button }}
|
{{ modal_button }}
|
||||||
</li>
|
{% elif modal_button_url and modal_button_text %}
|
||||||
|
<a
|
||||||
|
href="{{ modal_button_url }}"
|
||||||
|
type="button"
|
||||||
|
class="usa-button"
|
||||||
|
>
|
||||||
|
{{ modal_button_text }}
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="usa-button-group__item">
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ modal_button }}
|
{{ modal_button }}
|
||||||
</form>
|
</form>
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||||
in addition to being a close modal hook {% endcomment %}
|
in addition to being a close modal hook {% endcomment %}
|
||||||
|
|
|
@ -11,8 +11,10 @@
|
||||||
{% url 'members' as url %}
|
{% url 'members' as url %}
|
||||||
{% if portfolio_permission %}
|
{% if portfolio_permission %}
|
||||||
{% url 'member' pk=portfolio_permission.id as url2 %}
|
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||||
|
{% url 'member-domains-edit' pk=portfolio_permission.id as url3 %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||||
|
{% url 'invitedmember-domains-edit' pk=portfolio_invitation.id as url3 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
||||||
<ol class="usa-breadcrumb__list">
|
<ol class="usa-breadcrumb__list">
|
||||||
|
@ -23,7 +25,7 @@
|
||||||
<a href="{{ url2 }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
<a href="{{ url2 }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||||
<span>Manage member</span>
|
<span>Domain assignments</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -35,7 +37,7 @@
|
||||||
{% if has_edit_members_portfolio_permission %}
|
{% if has_edit_members_portfolio_permission %}
|
||||||
<div class="mobile:grid-col-12 tablet:grid-col-5">
|
<div class="mobile:grid-col-12 tablet:grid-col-5">
|
||||||
<p class="float-right-tablet tablet:margin-y-0">
|
<p class="float-right-tablet tablet:margin-y-0">
|
||||||
<a href="#" class="usa-button"
|
<a href="{{ url3 }}" class="usa-button"
|
||||||
>
|
>
|
||||||
Edit domain assignments
|
Edit domain assignments
|
||||||
</a>
|
</a>
|
||||||
|
|
69
src/registrar/templates/portfolio_member_domains_edit.html
Normal file
69
src/registrar/templates/portfolio_member_domains_edit.html
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
{% extends 'portfolio_base.html' %}
|
||||||
|
{% load static field_helpers%}
|
||||||
|
|
||||||
|
{% block title %}Edit organization member domains {% endblock %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block portfolio_content %}
|
||||||
|
<div id="main-content">
|
||||||
|
|
||||||
|
{% url 'members' as url %}
|
||||||
|
{% if portfolio_permission %}
|
||||||
|
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||||
|
{% url 'member-domains' pk=portfolio_permission.id as url3 %}
|
||||||
|
{% else %}
|
||||||
|
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||||
|
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
||||||
|
{% endif %}
|
||||||
|
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
||||||
|
<ol class="usa-breadcrumb__list">
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
<a href="{{ url2 }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
<a href="{{ url3 }}" class="usa-breadcrumb__link"><span>Domain assignments</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-breadcrumb__list-item usa-current edit-domain-assignments-breadcrumb" aria-current="page">
|
||||||
|
<span>Edit domain assignments</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<h1 class="margin-bottom-3">Edit domain assignments</h1>
|
||||||
|
|
||||||
|
<p class="margin-bottom-0">
|
||||||
|
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When you save this form the member will get an email to notify them of any changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% include "includes/member_domains_edit_table.html" %}
|
||||||
|
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
id="cancel-edit-domain-assignments"
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--outline"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button"
|
||||||
|
>
|
||||||
|
Review
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -58,10 +58,13 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
cls.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-03-01", state="ready")
|
cls.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-03-01", state="ready")
|
||||||
cls.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-03-01", state="ready")
|
cls.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-03-01", state="ready")
|
||||||
cls.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready")
|
cls.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready")
|
||||||
|
cls.domain4 = Domain.objects.create(name="example4.com", expiration_date="2024-03-01", state="ready")
|
||||||
|
|
||||||
# Add domain1 and domain2 to portfolio
|
# Add domain1 and domain2 to portfolio
|
||||||
DomainInformation.objects.create(creator=cls.user, domain=cls.domain1, portfolio=cls.portfolio)
|
DomainInformation.objects.create(creator=cls.user, domain=cls.domain1, portfolio=cls.portfolio)
|
||||||
DomainInformation.objects.create(creator=cls.user, domain=cls.domain2, portfolio=cls.portfolio)
|
DomainInformation.objects.create(creator=cls.user, domain=cls.domain2, portfolio=cls.portfolio)
|
||||||
DomainInformation.objects.create(creator=cls.user, domain=cls.domain3, portfolio=cls.portfolio)
|
DomainInformation.objects.create(creator=cls.user, domain=cls.domain3, portfolio=cls.portfolio)
|
||||||
|
DomainInformation.objects.create(creator=cls.user, domain=cls.domain4, portfolio=cls.portfolio)
|
||||||
|
|
||||||
# Assign user_member to view all domains
|
# Assign user_member to view all domains
|
||||||
UserPortfolioPermission.objects.create(
|
UserPortfolioPermission.objects.create(
|
||||||
|
@ -70,8 +73,10 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
)
|
)
|
||||||
# Add user_member as manager of domains
|
# Add user_member as manager of domains
|
||||||
UserDomainRole.objects.create(user=cls.user_member, domain=cls.domain1)
|
UserDomainRole.objects.create(user=cls.user_member, domain=cls.domain1, role=UserDomainRole.Roles.MANAGER)
|
||||||
UserDomainRole.objects.create(user=cls.user_member, domain=cls.domain2)
|
UserDomainRole.objects.create(user=cls.user_member, domain=cls.domain2, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
UserDomainRole.objects.create(user=cls.user_member, domain=cls.domain3, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
UserDomainRole.objects.create(user=cls.user_no_perms, domain=cls.domain3, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
# Add an invited member who has been invited to manage domains
|
# Add an invited member who has been invited to manage domains
|
||||||
cls.invited_member_email = "invited@example.com"
|
cls.invited_member_email = "invited@example.com"
|
||||||
|
@ -123,11 +128,11 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 2)
|
self.assertEqual(data["total"], 3)
|
||||||
self.assertEqual(data["unfiltered_total"], 2)
|
self.assertEqual(data["unfiltered_total"], 3)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 2)
|
self.assertEqual(len(data["domains"]), 3)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
|
@ -169,11 +174,11 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 3)
|
self.assertEqual(data["total"], 4)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 3)
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
|
@ -192,11 +197,11 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 3)
|
self.assertEqual(data["total"], 4)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 3)
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
|
@ -221,7 +226,7 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 1)
|
self.assertEqual(data["total"], 1)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 1)
|
self.assertEqual(len(data["domains"]), 1)
|
||||||
|
@ -249,7 +254,7 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 1)
|
self.assertEqual(data["total"], 1)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 1)
|
self.assertEqual(len(data["domains"]), 1)
|
||||||
|
@ -278,11 +283,11 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 3)
|
self.assertEqual(data["total"], 4)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 3)
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
# Check the name of the first domain is example1.com
|
# Check the name of the first domain is example1.com
|
||||||
self.assertEqual(data["domains"][0]["name"], "example1.com")
|
self.assertEqual(data["domains"][0]["name"], "example1.com")
|
||||||
|
@ -306,14 +311,121 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 3)
|
self.assertEqual(data["total"], 4)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 3)
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
# Check the name of the first domain is example1.com
|
# Check the name of the first domain is example1.com
|
||||||
self.assertEqual(data["domains"][0]["name"], "example3.com")
|
self.assertEqual(data["domains"][0]["name"], "example4.com")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_get_portfolio_member_domains_json_authenticated_sort_by_checked(self):
|
||||||
|
"""Test that sort returns results in correct order."""
|
||||||
|
# Test by checked in ascending order
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_member_domains_json"),
|
||||||
|
params={
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"email": self.user_member.id,
|
||||||
|
"member_only": "false",
|
||||||
|
"checkedDomainIds": f"{self.domain2.id},{self.domain3.id}",
|
||||||
|
"sort_by": "checked",
|
||||||
|
"order": "asc",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
self.assertEqual(data["total"], 4)
|
||||||
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
|
# Check the number of domains
|
||||||
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
|
# Check the name of the first domain is the first unchecked domain sorted alphabetically
|
||||||
|
self.assertEqual(data["domains"][0]["name"], "example1.com")
|
||||||
|
self.assertEqual(data["domains"][1]["name"], "example4.com")
|
||||||
|
|
||||||
|
# Test by checked in descending order
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_member_domains_json"),
|
||||||
|
params={
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"email": self.user_member.id,
|
||||||
|
"member_only": "false",
|
||||||
|
"checkedDomainIds": f"{self.domain2.id},{self.domain3.id}",
|
||||||
|
"sort_by": "checked",
|
||||||
|
"order": "desc",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
self.assertEqual(data["total"], 4)
|
||||||
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
|
# Check the number of domains
|
||||||
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
|
# Check the name of the first domain is the first checked domain sorted alphabetically
|
||||||
|
self.assertEqual(data["domains"][0]["name"], "example2.com")
|
||||||
|
self.assertEqual(data["domains"][1]["name"], "example3.com")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_get_portfolio_member_domains_json_authenticated_member_is_only_manager(self):
|
||||||
|
"""Test that sort returns member_is_only_manager when member_domain_role_exists
|
||||||
|
and member_domain_role_count == 1"""
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_member_domains_json"),
|
||||||
|
params={
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"member_id": self.user_member.id,
|
||||||
|
"member_only": "false",
|
||||||
|
"sort_by": "name",
|
||||||
|
"order": "asc",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
self.assertEqual(data["total"], 4)
|
||||||
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
|
# Check the number of domains
|
||||||
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
|
self.assertEqual(data["domains"][0]["name"], "example1.com")
|
||||||
|
self.assertEqual(data["domains"][1]["name"], "example2.com")
|
||||||
|
self.assertEqual(data["domains"][2]["name"], "example3.com")
|
||||||
|
self.assertEqual(data["domains"][3]["name"], "example4.com")
|
||||||
|
|
||||||
|
self.assertEqual(data["domains"][0]["member_is_only_manager"], True)
|
||||||
|
self.assertEqual(data["domains"][1]["member_is_only_manager"], True)
|
||||||
|
# domain3 has 2 managers
|
||||||
|
self.assertEqual(data["domains"][2]["member_is_only_manager"], False)
|
||||||
|
# no managers on this one
|
||||||
|
self.assertEqual(data["domains"][3]["member_is_only_manager"], False)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
|
@ -339,11 +451,11 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 3)
|
self.assertEqual(data["total"], 4)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 3)
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
# Check the name of the first domain is example1.com
|
# Check the name of the first domain is example1.com
|
||||||
self.assertEqual(data["domains"][0]["name"], "example1.com")
|
self.assertEqual(data["domains"][0]["name"], "example1.com")
|
||||||
|
@ -367,14 +479,79 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertFalse(data["has_previous"])
|
self.assertFalse(data["has_previous"])
|
||||||
self.assertFalse(data["has_next"])
|
self.assertFalse(data["has_next"])
|
||||||
self.assertEqual(data["num_pages"], 1)
|
self.assertEqual(data["num_pages"], 1)
|
||||||
self.assertEqual(data["total"], 3)
|
self.assertEqual(data["total"], 4)
|
||||||
self.assertEqual(data["unfiltered_total"], 3)
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
# Check the number of domains
|
# Check the number of domains
|
||||||
self.assertEqual(len(data["domains"]), 3)
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
# Check the name of the first domain is example1.com
|
# Check the name of the first domain is example1.com
|
||||||
self.assertEqual(data["domains"][0]["name"], "example3.com")
|
self.assertEqual(data["domains"][0]["name"], "example4.com")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_get_portfolio_invitedmember_domains_json_authenticated_sort_by_checked(self):
|
||||||
|
"""Test that sort returns results in correct order."""
|
||||||
|
# Test by checked in ascending order
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_member_domains_json"),
|
||||||
|
params={
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"email": self.invited_member_email,
|
||||||
|
"member_only": "false",
|
||||||
|
"checkedDomainIds": f"{self.domain2.id},{self.domain3.id}",
|
||||||
|
"sort_by": "checked",
|
||||||
|
"order": "asc",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
self.assertEqual(data["total"], 4)
|
||||||
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
|
# Check the number of domains
|
||||||
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
|
# Check the name of the first domain is the first unchecked domain sorted alphabetically
|
||||||
|
self.assertEqual(data["domains"][0]["name"], "example1.com")
|
||||||
|
self.assertEqual(data["domains"][1]["name"], "example4.com")
|
||||||
|
|
||||||
|
# Test by checked in descending order
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_member_domains_json"),
|
||||||
|
params={
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"email": self.invited_member_email,
|
||||||
|
"member_only": "false",
|
||||||
|
"checkedDomainIds": f"{self.domain2.id},{self.domain3.id}",
|
||||||
|
"sort_by": "checked",
|
||||||
|
"order": "desc",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
self.assertEqual(data["total"], 4)
|
||||||
|
self.assertEqual(data["unfiltered_total"], 4)
|
||||||
|
|
||||||
|
# Check the number of domains
|
||||||
|
self.assertEqual(len(data["domains"]), 4)
|
||||||
|
|
||||||
|
# Check the name of the first domain is the first checked domain sorted alphabetically
|
||||||
|
self.assertEqual(data["domains"][0]["name"], "example2.com")
|
||||||
|
self.assertEqual(data["domains"][1]["name"], "example3.com")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
|
|
|
@ -2102,6 +2102,127 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest):
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPortfolioMemberDomainsEditView(TestPortfolioMemberDomainsView):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_domains_edit_authenticated(self):
|
||||||
|
"""Tests that the portfolio member domains edit view is accessible."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
|
||||||
|
|
||||||
|
# Make sure the page loaded, and that we're on the right page
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.user_member.email)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_domains_edit_no_perms(self):
|
||||||
|
"""Tests that the portfolio member domains edit view is not accessible to user with no perms."""
|
||||||
|
self.client.force_login(self.user_no_perms)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
|
||||||
|
|
||||||
|
# Make sure the request returns forbidden
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_domains_edit_unauthenticated(self):
|
||||||
|
"""Tests that the portfolio member domains edit view is not accessible when no authenticated user."""
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
|
||||||
|
|
||||||
|
# Make sure the request returns redirect to openid login
|
||||||
|
self.assertEqual(response.status_code, 302) # Redirect to openid login
|
||||||
|
self.assertIn("/openid/login", response.url)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_domains_edit_not_found(self):
|
||||||
|
"""Tests that the portfolio member domains edit view returns not found if user
|
||||||
|
portfolio permission not found."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": "0"}))
|
||||||
|
|
||||||
|
# Make sure the response is not found
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPortfolioInvitedMemberEditDomainsView(TestPortfolioInvitedMemberDomainsView):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_invitedmember_domains_edit_authenticated(self):
|
||||||
|
"""Tests that the portfolio invited member domains edit view is accessible."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
|
||||||
|
|
||||||
|
# Make sure the page loaded, and that we're on the right page
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.invited_member_email)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_invitedmember_domains_edit_no_perms(self):
|
||||||
|
"""Tests that the portfolio invited member domains edit view is not accessible to user with no perms."""
|
||||||
|
self.client.force_login(self.user_no_perms)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
|
||||||
|
|
||||||
|
# Make sure the request returns forbidden
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_invitedmember_domains_edit_unauthenticated(self):
|
||||||
|
"""Tests that the portfolio invited member domains edit view is not accessible when no authenticated user."""
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
|
||||||
|
|
||||||
|
# Make sure the request returns redirect to openid login
|
||||||
|
self.assertEqual(response.status_code, 302) # Redirect to openid login
|
||||||
|
self.assertIn("/openid/login", response.url)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_domains_edit_not_found(self):
|
||||||
|
"""Tests that the portfolio invited member domains edit view returns not found if user is not a member."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": "0"}))
|
||||||
|
|
||||||
|
# Make sure the response is not found
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
class TestRequestingEntity(WebTest):
|
class TestRequestingEntity(WebTest):
|
||||||
"""The requesting entity page is a domain request form that only exists
|
"""The requesting entity page is a domain request form that only exists
|
||||||
within the context of a portfolio."""
|
within the context of a portfolio."""
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from django.db import models
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
@ -28,11 +29,12 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
||||||
objects = self.apply_search(objects, request)
|
objects = self.apply_search(objects, request)
|
||||||
objects = self.apply_sorting(objects, request)
|
objects = self.apply_sorting(objects, request)
|
||||||
|
|
||||||
paginator = Paginator(objects, 10)
|
paginator = Paginator(objects, self.get_page_size(request))
|
||||||
page_number = request.GET.get("page")
|
page_number = request.GET.get("page")
|
||||||
page_obj = paginator.get_page(page_number)
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
domains = [self.serialize_domain(domain, request.user) for domain in page_obj.object_list]
|
member_id = request.GET.get("member_id")
|
||||||
|
domains = [self.serialize_domain(domain, member_id, request.user) for domain in page_obj.object_list]
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
|
@ -46,6 +48,23 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_page_size(self, request):
|
||||||
|
"""Gets the page size.
|
||||||
|
|
||||||
|
If member_only, need to return the entire result set every time, so need
|
||||||
|
to set to a very large page size. If not member_only, this can be adjusted
|
||||||
|
to provide a smaller page size"""
|
||||||
|
|
||||||
|
member_only = request.GET.get("member_only", "false").lower() in ["true", "1"]
|
||||||
|
if member_only:
|
||||||
|
# This number needs to remain very high as the entire result set
|
||||||
|
# must be returned when member_only
|
||||||
|
return 1000
|
||||||
|
else:
|
||||||
|
# This number can be adjusted if we want to add pagination to the result page
|
||||||
|
# later
|
||||||
|
return 1000
|
||||||
|
|
||||||
def get_domain_ids_from_request(self, request):
|
def get_domain_ids_from_request(self, request):
|
||||||
"""Get domain ids from request.
|
"""Get domain ids from request.
|
||||||
|
|
||||||
|
@ -86,13 +105,41 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def apply_sorting(self, queryset, request):
|
def apply_sorting(self, queryset, request):
|
||||||
|
# Get the sorting parameters from the request
|
||||||
sort_by = request.GET.get("sort_by", "name")
|
sort_by = request.GET.get("sort_by", "name")
|
||||||
order = request.GET.get("order", "asc")
|
order = request.GET.get("order", "asc")
|
||||||
|
# Sort by 'checked' if specified, otherwise by the given field
|
||||||
|
if sort_by == "checked":
|
||||||
|
# Get list of checked ids from the request
|
||||||
|
checked_ids = request.GET.get("checkedDomainIds")
|
||||||
|
if checked_ids:
|
||||||
|
# Split the comma-separated string into a list of integers
|
||||||
|
checked_ids = [int(id.strip()) for id in checked_ids.split(",") if id.strip().isdigit()]
|
||||||
|
else:
|
||||||
|
# If no value is passed, set checked_ids to an empty list
|
||||||
|
checked_ids = []
|
||||||
|
# Annotate each object with a 'checked' value based on whether its ID is in checkedIds
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
checked=models.Case(
|
||||||
|
models.When(id__in=checked_ids, then=models.Value(True)),
|
||||||
|
default=models.Value(False),
|
||||||
|
output_field=models.BooleanField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Add ordering logic for 'checked'
|
||||||
|
if order == "desc":
|
||||||
|
queryset = queryset.order_by("-checked", "name")
|
||||||
|
else:
|
||||||
|
queryset = queryset.order_by("checked", "name")
|
||||||
|
else:
|
||||||
|
# Handle other fields as normal
|
||||||
if order == "desc":
|
if order == "desc":
|
||||||
sort_by = f"-{sort_by}"
|
sort_by = f"-{sort_by}"
|
||||||
return queryset.order_by(sort_by)
|
queryset = queryset.order_by(sort_by)
|
||||||
|
|
||||||
def serialize_domain(self, domain, user):
|
return queryset
|
||||||
|
|
||||||
|
def serialize_domain(self, domain, member_id, user):
|
||||||
suborganization_name = None
|
suborganization_name = None
|
||||||
try:
|
try:
|
||||||
domain_info = domain.domain_info
|
domain_info = domain.domain_info
|
||||||
|
@ -107,9 +154,22 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
||||||
# Check if there is a UserDomainRole for this domain and user
|
# Check if there is a UserDomainRole for this domain and user
|
||||||
user_domain_role_exists = UserDomainRole.objects.filter(domain_id=domain.id, user=user).exists()
|
user_domain_role_exists = UserDomainRole.objects.filter(domain_id=domain.id, user=user).exists()
|
||||||
view_only = not user_domain_role_exists or domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD]
|
view_only = not user_domain_role_exists or domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD]
|
||||||
|
|
||||||
|
# Check if the specified member is the only member assigned as manager of domain
|
||||||
|
only_member_assigned_to_domain = False
|
||||||
|
if member_id:
|
||||||
|
member_domain_role_count = UserDomainRole.objects.filter(
|
||||||
|
domain_id=domain.id, role=UserDomainRole.Roles.MANAGER
|
||||||
|
).count()
|
||||||
|
member_domain_role_exists = UserDomainRole.objects.filter(
|
||||||
|
domain_id=domain.id, user_id=member_id, role=UserDomainRole.Roles.MANAGER
|
||||||
|
).exists()
|
||||||
|
only_member_assigned_to_domain = member_domain_role_exists and member_domain_role_count == 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": domain.id,
|
"id": domain.id,
|
||||||
"name": domain.name,
|
"name": domain.name,
|
||||||
|
"member_is_only_manager": only_member_assigned_to_domain,
|
||||||
"expiration_date": domain.expiration_date,
|
"expiration_date": domain.expiration_date,
|
||||||
"state": domain.state,
|
"state": domain.state,
|
||||||
"state_display": domain.state_display(),
|
"state_display": domain.state_display(),
|
||||||
|
|
|
@ -20,6 +20,7 @@ from registrar.views.utility.permission_views import (
|
||||||
PortfolioBasePermissionView,
|
PortfolioBasePermissionView,
|
||||||
NoPortfolioDomainsPermissionView,
|
NoPortfolioDomainsPermissionView,
|
||||||
PortfolioMemberDomainsPermissionView,
|
PortfolioMemberDomainsPermissionView,
|
||||||
|
PortfolioMemberDomainsEditPermissionView,
|
||||||
PortfolioMemberEditPermissionView,
|
PortfolioMemberEditPermissionView,
|
||||||
PortfolioMemberPermissionView,
|
PortfolioMemberPermissionView,
|
||||||
PortfolioMembersPermissionView,
|
PortfolioMembersPermissionView,
|
||||||
|
@ -198,6 +199,24 @@ class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, View):
|
||||||
|
|
||||||
|
template_name = "portfolio_member_domains_edit.html"
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||||
|
member = portfolio_permission.user
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
self.template_name,
|
||||||
|
{
|
||||||
|
"portfolio_permission": portfolio_permission,
|
||||||
|
"member": member,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
|
class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
|
||||||
|
|
||||||
template_name = "portfolio_member.html"
|
template_name = "portfolio_member.html"
|
||||||
|
@ -307,6 +326,22 @@ class PortfolioInvitedMemberDomainsView(PortfolioMemberDomainsPermissionView, Vi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, View):
|
||||||
|
|
||||||
|
template_name = "portfolio_member_domains_edit.html"
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
self.template_name,
|
||||||
|
{
|
||||||
|
"portfolio_invitation": portfolio_invitation,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
||||||
"""Some users have access to the underlying portfolio, but not any domains.
|
"""Some users have access to the underlying portfolio, but not any domains.
|
||||||
This is a custom view which explains that to the user - and denotes who to contact.
|
This is a custom view which explains that to the user - and denotes who to contact.
|
||||||
|
|
|
@ -572,3 +572,20 @@ class PortfolioMemberDomainsPermission(PortfolioBasePermission):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return super().has_permission()
|
return super().has_permission()
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioMemberDomainsEditPermission(PortfolioBasePermission):
|
||||||
|
"""Permission mixin that allows access to portfolio member or invited member domains edit pages if user
|
||||||
|
has access to edit, otherwise 403"""
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
"""Check if this user has access to member or invited member domains for this portfolio.
|
||||||
|
|
||||||
|
The user is in self.request.user and the portfolio can be looked
|
||||||
|
up from the portfolio's primary key in self.kwargs["pk"]"""
|
||||||
|
|
||||||
|
portfolio = self.request.session.get("portfolio")
|
||||||
|
if not self.request.user.has_edit_members_portfolio_permission(portfolio):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return super().has_permission()
|
||||||
|
|
|
@ -16,6 +16,7 @@ from .mixins import (
|
||||||
PortfolioDomainRequestsPermission,
|
PortfolioDomainRequestsPermission,
|
||||||
PortfolioDomainsPermission,
|
PortfolioDomainsPermission,
|
||||||
PortfolioMemberDomainsPermission,
|
PortfolioMemberDomainsPermission,
|
||||||
|
PortfolioMemberDomainsEditPermission,
|
||||||
PortfolioMemberEditPermission,
|
PortfolioMemberEditPermission,
|
||||||
UserDeleteDomainRolePermission,
|
UserDeleteDomainRolePermission,
|
||||||
UserProfilePermission,
|
UserProfilePermission,
|
||||||
|
@ -279,3 +280,13 @@ class PortfolioMemberDomainsPermissionView(PortfolioMemberDomainsPermission, Por
|
||||||
This abstract view cannot be instantiated. Actual views must specify
|
This abstract view cannot be instantiated. Actual views must specify
|
||||||
`template_name`.
|
`template_name`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioMemberDomainsEditPermissionView(
|
||||||
|
PortfolioMemberDomainsEditPermission, PortfolioBasePermissionView, abc.ABC
|
||||||
|
):
|
||||||
|
"""Abstract base view for portfolio member domains edit views that enforces permissions.
|
||||||
|
|
||||||
|
This abstract view cannot be instantiated. Actual views must specify
|
||||||
|
`template_name`.
|
||||||
|
"""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue