mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-23 19:20:47 +02:00
Merge branch 'main' into za/3365-change-tab-order-for-members-table
This commit is contained in:
commit
b80a94b3a6
34 changed files with 503 additions and 302 deletions
|
@ -96,3 +96,14 @@ export function submitForm(form_id) {
|
|||
console.error("Form '" + form_id + "' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to strip HTML tags
|
||||
* THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
|
||||
*/
|
||||
export function unsafeStripHtmlTags(input) {
|
||||
const tempDiv = document.createElement("div");
|
||||
// NOTE: THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
|
||||
tempDiv.innerHTML = input;
|
||||
return tempDiv.textContent || tempDiv.innerText || "";
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export function initPortfolioNewMemberPageToggle() {
|
|||
const unique_id = `${member_type}-${member_id}`;
|
||||
|
||||
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
||||
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`);
|
||||
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`, "usa-icon--large");
|
||||
|
||||
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
||||
// All prod users will have emails linked to their account
|
||||
|
@ -100,8 +100,8 @@ export function initAddNewMemberPageListeners() {
|
|||
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
|
||||
|
||||
permissionSections.forEach(section => {
|
||||
// Find the <h3> element text
|
||||
const sectionTitle = section.textContent;
|
||||
// Find the <h3> element text, strip out the '*'
|
||||
const sectionTitle = section.textContent.trim().replace(/\*$/, "") + ": ";
|
||||
|
||||
// Find the associated radio buttons container (next fieldset)
|
||||
const fieldset = section.nextElementSibling;
|
||||
|
@ -128,25 +128,29 @@ export function initAddNewMemberPageListeners() {
|
|||
});
|
||||
} else {
|
||||
// for admin users, the permissions are always the same
|
||||
appendPermissionInContainer('Domains', 'Viewer', permissionDetailsContainer);
|
||||
appendPermissionInContainer('Domain requests', 'Creator', permissionDetailsContainer);
|
||||
appendPermissionInContainer('Members', 'Manager', permissionDetailsContainer);
|
||||
appendPermissionInContainer('Domains: ', 'Viewer', permissionDetailsContainer);
|
||||
appendPermissionInContainer('Domain requests: ', 'Creator', permissionDetailsContainer);
|
||||
appendPermissionInContainer('Members: ', 'Manager', permissionDetailsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
function appendPermissionInContainer(sectionTitle, permissionDisplay, permissionContainer) {
|
||||
// Create new elements for the content
|
||||
const titleElement = document.createElement("h4");
|
||||
titleElement.textContent = sectionTitle;
|
||||
titleElement.classList.add("text-primary", "margin-bottom-0");
|
||||
const elementContainer = document.createElement("p");
|
||||
elementContainer.classList.add("margin-top-0", "margin-bottom-1");
|
||||
|
||||
const permissionElement = document.createElement("p");
|
||||
const titleElement = document.createElement("strong");
|
||||
titleElement.textContent = sectionTitle;
|
||||
titleElement.classList.add("text-primary-darker");
|
||||
|
||||
const permissionElement = document.createElement("span");
|
||||
permissionElement.textContent = permissionDisplay;
|
||||
permissionElement.classList.add("margin-top-0");
|
||||
|
||||
// Append to the content container
|
||||
permissionContainer.appendChild(titleElement);
|
||||
permissionContainer.appendChild(permissionElement);
|
||||
elementContainer.appendChild(titleElement);
|
||||
elementContainer.appendChild(permissionElement);
|
||||
|
||||
permissionContainer.appendChild(elementContainer);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -79,13 +79,13 @@ export function addModal(id, ariaLabelledby, ariaDescribedby, modalHeading, moda
|
|||
* @param {string} modal_button_text - The action button's text
|
||||
* @param {string} screen_reader_text - A screen reader helper
|
||||
*/
|
||||
export function generateKebabHTML(action, unique_id, modal_button_text, screen_reader_text) {
|
||||
export function generateKebabHTML(action, unique_id, modal_button_text, screen_reader_text, icon_class) {
|
||||
const generateModalButton = (mobileOnly = false) => `
|
||||
<a
|
||||
role="button"
|
||||
id="button-trigger-${action}-${unique_id}"
|
||||
href="#toggle-${action}-${unique_id}"
|
||||
class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary ${mobileOnly ? 'visible-mobile-flex' : ''}"
|
||||
class="usa-button usa-button--unstyled text-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary ${mobileOnly ? 'visible-mobile-flex' : ''}"
|
||||
aria-controls="toggle-${action}-${unique_id}"
|
||||
data-open-modal
|
||||
>
|
||||
|
@ -99,7 +99,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
|||
// Main kebab structure
|
||||
const kebab = `
|
||||
${generateModalButton(true)} <!-- Mobile button -->
|
||||
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
|
||||
<div class="usa-accordion usa-accordion--more-actions margin-right-2 margin-top-3px hidden-mobile-flex">
|
||||
<div class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -108,12 +108,12 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
|||
aria-controls="more-actions-${unique_id}"
|
||||
aria-label="${screen_reader_text}"
|
||||
>
|
||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<svg class="usa-icon${icon_class ? " " + icon_class : ""}" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="more-actions-${unique_id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-neg-1" hidden>
|
||||
<div id="more-actions-${unique_id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-0${icon_class === 'usa-icon--large' ? ' top-28px' : ''}" hidden>
|
||||
<h2>More options</h2>
|
||||
${generateModalButton()} <!-- Desktop button -->
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { hideElement, showElement, getCsrfToken } from './helpers.js';
|
||||
import { hideElement, showElement, getCsrfToken, unsafeStripHtmlTags } from './helpers.js';
|
||||
import { uswdsInitializeModals, uswdsUnloadModals } from './helpers-uswds.js';
|
||||
|
||||
import { BaseTable, addModal, generateKebabHTML } from './table-base.js';
|
||||
|
@ -98,9 +98,10 @@ export class DomainRequestsTable extends BaseTable {
|
|||
|
||||
// Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly
|
||||
if (this.portfolioValue) {
|
||||
|
||||
// NOTE: THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
|
||||
const sanitizedDomainName = unsafeStripHtmlTags(domainName);
|
||||
// 2nd path (org model): Just a modal trigger on mobile for org users or kebab + accordion with nested modal trigger on desktop for org users
|
||||
modalTrigger = generateKebabHTML('delete-domain', request.id, 'Delete', domainName);
|
||||
modalTrigger = generateKebabHTML('delete-domain', request.id, 'Delete', sanitizedDomainName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +118,7 @@ export class DomainRequestsTable extends BaseTable {
|
|||
${request.status}
|
||||
</td>
|
||||
<td class="width--action-column">
|
||||
<div class="tablet:display-flex tablet:flex-row flex-wrap">
|
||||
<div class="tablet:display-flex tablet:flex-row">
|
||||
<a href="${actionUrl}" ${customTableOptions.hasAdditionalActions ? "class='margin-right-2'" : ''}>
|
||||
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#${request.svg_icon}"></use>
|
||||
|
|
|
@ -24,9 +24,12 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
this.reviewButton = document.getElementById('review-domain-assignments');
|
||||
this.backButton = document.getElementById('back-to-edit-domain-assignments');
|
||||
this.saveButton = document.getElementById('save-domain-assignments');
|
||||
this.initializeDomainAssignments();
|
||||
}
|
||||
async init() {
|
||||
await this.initializeDomainAssignments();
|
||||
this.initCancelEditDomainAssignmentButton();
|
||||
this.initEventListeners();
|
||||
return this;
|
||||
}
|
||||
getBaseUrl() {
|
||||
return document.getElementById("get_member_domains_json_url");
|
||||
|
@ -134,27 +137,33 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
* member. It populates both initialDomainAssignments and initialDomainAssignmentsOnlyMember.
|
||||
* It is called once per page load, but not called with subsequent table changes.
|
||||
*/
|
||||
initializeDomainAssignments() {
|
||||
async 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);
|
||||
if (!baseUrlValue) {
|
||||
console.error("Base URL not found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let searchParams = this.getDomainAssignmentSearchParams(this.portfolioValue);
|
||||
let url = baseUrlValue + "?" + searchParams.toString();
|
||||
|
||||
let response = await fetch(url);
|
||||
let data = await response.json();
|
||||
|
||||
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));
|
||||
.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
|
||||
|
@ -232,8 +241,6 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
}
|
||||
|
||||
updateReadonlyDisplay() {
|
||||
let totalAssignedDomains = this.getCheckedDomains().length;
|
||||
|
||||
// Create unassigned domains list
|
||||
const unassignedDomainsList = document.createElement('ul');
|
||||
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
||||
|
@ -260,35 +267,30 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
// Clear existing content
|
||||
domainAssignmentSummary.innerHTML = '';
|
||||
|
||||
// Append unassigned domains section
|
||||
if (this.removedDomains.length) {
|
||||
const unassignedHeader = document.createElement('h3');
|
||||
unassignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||
unassignedHeader.textContent = 'Unassigned domains';
|
||||
domainAssignmentSummary.appendChild(unassignedHeader);
|
||||
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
||||
}
|
||||
|
||||
// Append assigned domains section
|
||||
if (this.addedDomains.length) {
|
||||
const assignedHeader = document.createElement('h3');
|
||||
// Make this h3 look like a h4
|
||||
assignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||
assignedHeader.textContent = 'Assigned domains';
|
||||
assignedHeader.textContent = `New assignments (${this.addedDomains.length})`;
|
||||
domainAssignmentSummary.appendChild(assignedHeader);
|
||||
domainAssignmentSummary.appendChild(assignedDomainsList);
|
||||
}
|
||||
|
||||
// Append total assigned domains section
|
||||
const totalHeader = document.createElement('h3');
|
||||
// Make this h3 look like a h4
|
||||
totalHeader.classList.add('margin-bottom-05', 'h4');
|
||||
totalHeader.textContent = 'Total assigned domains';
|
||||
domainAssignmentSummary.appendChild(totalHeader);
|
||||
const totalCount = document.createElement('p');
|
||||
totalCount.classList.add('margin-y-0');
|
||||
totalCount.textContent = totalAssignedDomains;
|
||||
domainAssignmentSummary.appendChild(totalCount);
|
||||
// Append unassigned domains section
|
||||
if (this.removedDomains.length) {
|
||||
const unassignedHeader = document.createElement('h3');
|
||||
unassignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||
unassignedHeader.textContent =`Removed assignments (${this.removedDomains.length})`;
|
||||
domainAssignmentSummary.appendChild(unassignedHeader);
|
||||
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
||||
}
|
||||
|
||||
if (!this.addedDomains.length && !this.removedDomains.length) {
|
||||
const noChangesParagraph = document.createElement('p');
|
||||
noChangesParagraph.textContent = "No changes were detected. Click the “Back” button to edit this member’s domain assignments.";
|
||||
domainAssignmentSummary.appendChild(noChangesParagraph);
|
||||
}
|
||||
}
|
||||
|
||||
showReadonlyMode() {
|
||||
|
@ -355,14 +357,14 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const isEditMemberDomainsPage = document.getElementById("edit-member-domains");
|
||||
if (!isEditMemberDomainsPage) return; // Exit if not on the right page
|
||||
|
||||
const editMemberDomainsTable = await new EditMemberDomainsTable().init();
|
||||
|
||||
if (editMemberDomainsTable.tableWrapper) {
|
||||
editMemberDomainsTable.loadTable(1); // Initial load
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ export class MembersTable extends BaseTable {
|
|||
|
||||
// generate html blocks for domains and permissions for the member
|
||||
let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.action_url);
|
||||
let permissionsHTML = this.generatePermissionsHTML(member.permissions, customTableOptions.UserPortfolioPermissionChoices);
|
||||
let permissionsHTML = this.generatePermissionsHTML(member.is_admin, member.permissions, customTableOptions.UserPortfolioPermissionChoices);
|
||||
|
||||
// domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand
|
||||
let showMoreButton = '';
|
||||
|
@ -98,9 +98,9 @@ export class MembersTable extends BaseTable {
|
|||
`;
|
||||
|
||||
showMoreRow.innerHTML = `
|
||||
<td colspan='3' headers="header-member row-header-${unique_id}" class="padding-top-0">
|
||||
<td colspan='4' headers="header-member row-header-${unique_id}" class="padding-top-0">
|
||||
${showMoreButton}
|
||||
<div class='grid-row show-more-content display-none'>
|
||||
<div class='grid-row grid-gap-2 show-more-content display-none'>
|
||||
${domainsHTML}
|
||||
${permissionsHTML}
|
||||
</div>
|
||||
|
@ -118,13 +118,13 @@ export class MembersTable extends BaseTable {
|
|||
</td>
|
||||
<td class="padding-bottom-0" headers="header-action row-header-${unique_id}" class="width--action-column">
|
||||
<div class="tablet:display-flex tablet:flex-row flex-align-center">
|
||||
<a href="${member.action_url}">
|
||||
<a href="${member.action_url}" ${customTableOptions.hasAdditionalActions ? "class='margin-right-2'" : ''}>
|
||||
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#${member.svg_icon}"></use>
|
||||
</svg>
|
||||
${member.action_label} <span class="usa-sr-only">${member.name}</span>
|
||||
</a>
|
||||
<span class="padding-left-1">${customTableOptions.hasAdditionalActions ? kebabHTML : ''}</span>
|
||||
${customTableOptions.hasAdditionalActions ? kebabHTML : ''}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
@ -250,10 +250,11 @@ export class MembersTable extends BaseTable {
|
|||
let domainsHTML = '';
|
||||
|
||||
// Only generate HTML if the member has one or more assigned domains
|
||||
|
||||
domainsHTML += "<div class='desktop:grid-col-4 margin-bottom-2 desktop:margin-bottom-0'>";
|
||||
domainsHTML += "<h4 class='font-body-xs margin-y-0'>Domains assigned</h4>";
|
||||
if (num_domains > 0) {
|
||||
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
||||
domainsHTML += "<h4 class='font-body-xs margin-y-0'>Domains assigned</h4>";
|
||||
domainsHTML += `<p class='font-body-xs text-base-dark margin-y-0'>This member is assigned to ${num_domains} domain${num_domains > 1 ? 's' : ''}:</p>`;
|
||||
domainsHTML += `<p class='font-body-xs text-base-darker margin-y-0'>This member is assigned to ${num_domains} domain${num_domains > 1 ? 's' : ''}:</p>`;
|
||||
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
||||
|
||||
// Display up to 6 domains with their URLs
|
||||
|
@ -262,13 +263,15 @@ export class MembersTable extends BaseTable {
|
|||
}
|
||||
|
||||
domainsHTML += "</ul>";
|
||||
|
||||
// If there are more than 6 domains, display a "View assigned domains" link
|
||||
domainsHTML += `<p class="font-body-xs"><a href="${action_url}/domains">View assigned domains</a></p>`;
|
||||
|
||||
domainsHTML += "</div>";
|
||||
} else {
|
||||
domainsHTML += `<p class='font-body-xs text-base-darker margin-y-0'>This member is assigned to 0 domains.</p>`;
|
||||
}
|
||||
|
||||
// If there are more than 6 domains, display a "View assigned domains" link
|
||||
domainsHTML += `<p class="font-body-xs"><a href="${action_url}/domains">View domain assignments</a></p>`;
|
||||
|
||||
domainsHTML += "</div>";
|
||||
|
||||
return domainsHTML;
|
||||
}
|
||||
|
||||
|
@ -380,40 +383,51 @@ export class MembersTable extends BaseTable {
|
|||
* - If no relevant permissions are found, the function returns a message stating that the user has no additional permissions.
|
||||
* - The resulting HTML always includes a header "Additional permissions for this member" and appends the relevant permission descriptions.
|
||||
*/
|
||||
generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) {
|
||||
generatePermissionsHTML(is_admin, member_permissions, UserPortfolioPermissionChoices) {
|
||||
let permissionsHTML = '';
|
||||
|
||||
// Define shared classes across elements for easier refactoring
|
||||
let sharedParagraphClasses = "font-body-xs text-base-dark margin-top-1 p--blockquote";
|
||||
let sharedParagraphClasses = "font-body-xs text-base-darker margin-top-1 p--blockquote";
|
||||
|
||||
// Member access
|
||||
if (is_admin) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Member access: <strong>Admin</strong></p>`;
|
||||
} else {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Member access: <strong>Basic</strong></p>`;
|
||||
}
|
||||
|
||||
// Check domain-related permissions
|
||||
if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domains:</strong> Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>`;
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Domains: <strong>Viewer</strong></p>`;
|
||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domains:</strong> Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>`;
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Domains: <strong>Viewer, limited</strong></p>`;
|
||||
}
|
||||
|
||||
// Check request-related permissions
|
||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domain requests:</strong> Can view all organization domain requests. Can create domain requests and modify their own requests.</p>`;
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Domain requests: <strong>Creator</strong></p>`;
|
||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domain requests (view-only):</strong> Can view all organization domain requests. Can't create or modify any domain requests.</p>`;
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Domain requests: <strong>Viewer</strong></p>`;
|
||||
} else {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Domain requests: <strong>No access</strong></p>`;
|
||||
}
|
||||
|
||||
// Check member-related permissions
|
||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members.</p>`;
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Members: <strong>Manager</strong></p>`;
|
||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.</p>`;
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Members: <strong>Viewer</strong></p>`;
|
||||
} else {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>Members: <strong>No access</strong></p>`;
|
||||
}
|
||||
|
||||
// If no specific permissions are assigned, display a message indicating no additional permissions
|
||||
if (!permissionsHTML) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'><b>No additional permissions:</b> There are no additional permissions for this member.</p>`;
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'>No additional permissions: <strong>There are no additional permissions for this member</strong>.</p>`;
|
||||
}
|
||||
|
||||
// Add a permissions header and wrap the entire output in a container
|
||||
permissionsHTML = `<div class='desktop:grid-col-7'><h4 class='font-body-xs margin-y-0'>Additional permissions for this member</h4>${permissionsHTML}</div>`;
|
||||
permissionsHTML = `<div class='desktop:grid-col-8'><h4 class='font-body-xs margin-y-0'>Member access and permissions</h4>${permissionsHTML}</div>`;
|
||||
|
||||
return permissionsHTML;
|
||||
}
|
||||
|
|
|
@ -5,12 +5,22 @@
|
|||
display: inline-block;
|
||||
width: auto;
|
||||
position: relative;
|
||||
.usa-accordion__button {
|
||||
border-radius: units(.5);
|
||||
}
|
||||
.usa-accordion__button:focus {
|
||||
outline-offset: 0;
|
||||
outline-width: 3px;
|
||||
}
|
||||
.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__button[aria-expanded=true] {
|
||||
background-color: color('primary-lighter');
|
||||
}
|
||||
.usa-accordion__content {
|
||||
// Note, width is determined by a custom width class on one of the children
|
||||
position: absolute;
|
||||
|
@ -37,7 +47,12 @@
|
|||
}
|
||||
|
||||
.usa-accordion--more-actions .usa-accordion__content {
|
||||
top: 30px;
|
||||
// We need to match the height of the trigger button
|
||||
// to align the 'popup' underneath
|
||||
top: 20px;
|
||||
&.top-28px {
|
||||
top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
// Special positioning for the kabob menu popup in the last row on a given page
|
||||
|
|
|
@ -275,6 +275,14 @@ abbr[title] {
|
|||
width: 25%;
|
||||
}
|
||||
|
||||
.margin-top-3px {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.top-28px {
|
||||
top: 28px;
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE: width: 3% basically forces a fit-content effect in the table.
|
||||
Fit-content itself does not work.
|
||||
|
|
|
@ -143,7 +143,7 @@ th {
|
|||
}
|
||||
|
||||
.usa-table--bg-transparent {
|
||||
td, thead th {
|
||||
td, th, thead th {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,16 @@ from registrar.models import (
|
|||
Portfolio,
|
||||
SeniorOfficial,
|
||||
)
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.models.utility.portfolio_helper import (
|
||||
UserPortfolioPermissionChoices,
|
||||
UserPortfolioRoleChoices,
|
||||
get_domain_requests_description_display,
|
||||
get_domain_requests_display,
|
||||
get_domains_description_display,
|
||||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -126,8 +135,16 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
|
||||
domain_permissions = forms.ChoiceField(
|
||||
choices=[
|
||||
(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"),
|
||||
(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer"),
|
||||
(
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
|
||||
get_domains_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None),
|
||||
),
|
||||
(
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value,
|
||||
get_domains_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS]
|
||||
),
|
||||
),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
|
@ -139,9 +156,19 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
|
||||
domain_request_permissions = forms.ChoiceField(
|
||||
choices=[
|
||||
("no_access", "No access"),
|
||||
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "Viewer"),
|
||||
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Creator"),
|
||||
("no_access", get_domain_requests_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None)),
|
||||
(
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
|
||||
get_domain_requests_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS]
|
||||
),
|
||||
),
|
||||
(
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS.value,
|
||||
get_domain_requests_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.EDIT_REQUESTS]
|
||||
),
|
||||
),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
|
@ -153,8 +180,13 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
|
||||
member_permissions = forms.ChoiceField(
|
||||
choices=[
|
||||
("no_access", "No access"),
|
||||
(UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "Viewer"),
|
||||
("no_access", get_members_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None)),
|
||||
(
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS.value,
|
||||
get_members_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_MEMBERS]
|
||||
),
|
||||
),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
|
@ -191,19 +223,31 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
|
||||
# Adds a <p> description beneath each option
|
||||
self.fields["domain_permissions"].descriptions = {
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "Can view only the domains they manage",
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "Can view all domains for the organization",
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: get_domains_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None
|
||||
),
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: get_domains_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS]
|
||||
),
|
||||
}
|
||||
self.fields["domain_request_permissions"].descriptions = {
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS.value: (
|
||||
"Can view all domain requests for the organization and create requests"
|
||||
get_domain_requests_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.EDIT_REQUESTS]
|
||||
)
|
||||
),
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: "Can view all domain requests for the organization",
|
||||
"no_access": "Cannot view or create domain requests",
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: (
|
||||
get_domain_requests_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS]
|
||||
)
|
||||
),
|
||||
"no_access": get_domain_requests_description_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None),
|
||||
}
|
||||
self.fields["member_permissions"].descriptions = {
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS.value: "Can view all members permissions",
|
||||
"no_access": "Cannot view member permissions",
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS.value: get_members_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_MEMBERS]
|
||||
),
|
||||
"no_access": get_members_description_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None),
|
||||
}
|
||||
|
||||
# Map model instance values to custom form fields
|
||||
|
@ -218,6 +262,9 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
cleaned_data = super().clean()
|
||||
role = cleaned_data.get("role")
|
||||
|
||||
# handle role
|
||||
cleaned_data["roles"] = [role] if role else []
|
||||
|
||||
# Get required fields for the selected role. Then validate all required fields for the role.
|
||||
required_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
|
||||
for field_name in required_fields:
|
||||
|
@ -236,9 +283,6 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
if cleaned_data.get("member_permissions") == "no_access":
|
||||
cleaned_data["member_permissions"] = None
|
||||
|
||||
# Handle roles
|
||||
cleaned_data["roles"] = [role]
|
||||
|
||||
# Handle additional_permissions
|
||||
valid_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
|
||||
additional_permissions = {cleaned_data.get(field) for field in valid_fields if cleaned_data.get(field)}
|
||||
|
|
|
@ -9,8 +9,11 @@ from .utility.portfolio_helper import (
|
|||
UserPortfolioPermissionChoices,
|
||||
UserPortfolioRoleChoices,
|
||||
cleanup_after_portfolio_member_deletion,
|
||||
get_domain_requests_description_display,
|
||||
get_domain_requests_display,
|
||||
get_domains_description_display,
|
||||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
get_role_display,
|
||||
validate_portfolio_invitation,
|
||||
|
@ -115,6 +118,16 @@ class PortfolioInvitation(TimeStampedModel):
|
|||
"""
|
||||
return get_domains_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domains_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's domain access level.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain permissions description.
|
||||
"""
|
||||
return get_domains_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domain_requests_display(self):
|
||||
"""
|
||||
|
@ -129,6 +142,16 @@ class PortfolioInvitation(TimeStampedModel):
|
|||
"""
|
||||
return get_domain_requests_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domain_requests_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's access to domain requests.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain request permissions description.
|
||||
"""
|
||||
return get_domain_requests_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def members_display(self):
|
||||
"""
|
||||
|
@ -143,6 +166,16 @@ class PortfolioInvitation(TimeStampedModel):
|
|||
"""
|
||||
return get_members_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def members_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's access to managing members.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's member management permissions description.
|
||||
"""
|
||||
return get_members_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@transition(field="status", source=PortfolioInvitationStatus.INVITED, target=PortfolioInvitationStatus.RETRIEVED)
|
||||
def retrieve(self):
|
||||
"""When an invitation is retrieved, create the corresponding permission.
|
||||
|
|
|
@ -7,8 +7,11 @@ from registrar.models.utility.portfolio_helper import (
|
|||
MemberPermissionDisplay,
|
||||
cleanup_after_portfolio_member_deletion,
|
||||
get_domain_requests_display,
|
||||
get_domain_requests_description_display,
|
||||
get_domains_display,
|
||||
get_domains_description_display,
|
||||
get_members_display,
|
||||
get_members_description_display,
|
||||
get_role_display,
|
||||
validate_user_portfolio_permission,
|
||||
)
|
||||
|
@ -211,6 +214,16 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
"""
|
||||
return get_domains_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domains_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's domain access level.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain permissions description.
|
||||
"""
|
||||
return get_domains_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domain_requests_display(self):
|
||||
"""
|
||||
|
@ -225,6 +238,16 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
"""
|
||||
return get_domain_requests_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domain_requests_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's access to domain requests.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain request permissions description.
|
||||
"""
|
||||
return get_domain_requests_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def members_display(self):
|
||||
"""
|
||||
|
@ -239,6 +262,16 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
"""
|
||||
return get_members_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def members_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's access to managing members.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's member management permissions description.
|
||||
"""
|
||||
return get_members_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
def clean(self):
|
||||
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
||||
super().clean()
|
||||
|
|
|
@ -123,6 +123,25 @@ def get_domains_display(roles, permissions):
|
|||
return "Viewer, limited"
|
||||
|
||||
|
||||
def get_domains_description_display(roles, permissions):
|
||||
"""
|
||||
Determines the display description for a user's domain viewing permissions.
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's domain viewing access description.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in all_permissions:
|
||||
return "Can view all domains for the organization"
|
||||
else:
|
||||
return "Can view only the domains they manage"
|
||||
|
||||
|
||||
def get_domain_requests_display(roles, permissions):
|
||||
"""
|
||||
Determines the display name for a user's domain request permissions.
|
||||
|
@ -148,6 +167,27 @@ def get_domain_requests_display(roles, permissions):
|
|||
return "No access"
|
||||
|
||||
|
||||
def get_domain_requests_description_display(roles, permissions):
|
||||
"""
|
||||
Determines the display description for a user's domain request permissions.
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's domain request access level description.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.EDIT_REQUESTS in all_permissions:
|
||||
return "Can view all domain requests for the organization and create requests"
|
||||
elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions:
|
||||
return "Can view all domain requests for the organization"
|
||||
else:
|
||||
return "Cannot view or create domain requests"
|
||||
|
||||
|
||||
def get_members_display(roles, permissions):
|
||||
"""
|
||||
Determines the display name for a user's member management permissions.
|
||||
|
@ -173,6 +213,27 @@ def get_members_display(roles, permissions):
|
|||
return "No access"
|
||||
|
||||
|
||||
def get_members_description_display(roles, permissions):
|
||||
"""
|
||||
Determines the display description for a user's member management permissions.
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's member management access level description.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions:
|
||||
return "Can view and manage all member permissions"
|
||||
elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions:
|
||||
return "Can view all member permissions"
|
||||
else:
|
||||
return "Cannot view member permissions"
|
||||
|
||||
|
||||
def validate_user_portfolio_permission(user_portfolio_permission):
|
||||
"""
|
||||
Validates a UserPortfolioPermission instance. Located in portfolio_helper to avoid circular imports
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
{% if field and field.field and field.field.descriptions %}
|
||||
{% with description=field.field.descriptions|get_dict_value:option.value %}
|
||||
{% if description %}
|
||||
<p class="margin-0 margin-top-1 font-body-2xs">{{ description }}</p>
|
||||
<p class="margin-0 font-body-2xs">{{ description }}</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
|
|
@ -42,17 +42,18 @@
|
|||
{% endblock breadcrumb %}
|
||||
|
||||
<h1>Add a domain manager</h1>
|
||||
{% if has_organization_feature_flag %}
|
||||
{% if portfolio %}
|
||||
<p>
|
||||
Provide an email address for the domain manager you’d like to add.
|
||||
They’ll need to access the registrar using a Login.gov account that’s associated with this email address.
|
||||
Domain managers can be a member of only one .gov organization.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
You can add another user to help manage your domain. Users can only be a member of one .gov organization,
|
||||
and they'll need to sign in with their Login.gov account.
|
||||
Provide an email address for the domain manager you’d like to add.
|
||||
They’ll need to access the registrar using a Login.gov account that’s associated with this email address.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
You can add another user to help manage your domain. They will need to sign in to the .gov registrar with
|
||||
their Login.gov account.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -25,29 +25,25 @@
|
|||
|
||||
<h1>Domain managers</h1>
|
||||
|
||||
{% comment %}Copy below differs depending on whether view is in portfolio mode.{% endcomment %}
|
||||
{% if not portfolio %}
|
||||
<p>
|
||||
Domain managers can update all information related to a domain within the
|
||||
.gov registrar, including security email and DNS name servers.
|
||||
Domain managers can update information related to this domain, including security email and DNS name servers.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
Domain managers can update all information related to a domain within the
|
||||
.gov registrar, including contact details, senior official, security email, and DNS name servers.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<ul class="usa-list">
|
||||
<li>There is no limit to the number of domain managers you can add.</li>
|
||||
<li>After adding a domain manager, an email invitation will be sent to that user with
|
||||
instructions on how to set up an account.</li>
|
||||
<li>There is no limit on the number of domain managers you can add.</li>
|
||||
<li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li>
|
||||
{% if not portfolio %}<li>All domain managers will be notified when updates are made to this domain.</li>{% endif %}
|
||||
<li>Domains must have at least one domain manager. You can’t remove yourself as a domain manager if you’re the only one assigned to this domain.
|
||||
{% if portfolio %} Add another domain manager before you remove yourself from this domain.{% endif %}</li>
|
||||
<li>All domain managers will be notified when updates are made to this domain and when managers are added or removed.</li>
|
||||
<li>Domains must have at least one manager. You can’t remove yourself if you’re the only one assigned to this domain.</li>
|
||||
</ul>
|
||||
|
||||
{% if domain_manager_roles and domain_manager_roles|length == 1 %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body">
|
||||
This domain has only one manager. Consider adding another manager to ensure the domain has continuous oversight and support.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if domain_manager_roles %}
|
||||
<section class="section-outlined" id="domain-managers">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||
|
|
|
@ -8,7 +8,6 @@ To manage domain information, visit the .gov registrar <{{ manage_url }}>.
|
|||
|
||||
----------------------------------------------------------------
|
||||
{% if not requested_user %}
|
||||
|
||||
YOU NEED A LOGIN.GOV ACCOUNT
|
||||
You’ll need a Login.gov account to access the .gov registrar. That account needs to be
|
||||
associated with the following email address: {{ invitee_email_address }}
|
||||
|
@ -17,8 +16,6 @@ Login.gov provides a simple and secure process for signing in to many government
|
|||
services with one account. If you don’t already have one, follow these steps to create
|
||||
your Login.gov account <https://login.gov/help/get-started/create-your-account/>.
|
||||
{% endif %}
|
||||
|
||||
|
||||
DOMAIN MANAGEMENT
|
||||
As a .gov domain manager, you can add or update information like name servers. You’ll
|
||||
also serve as a contact for the domains you manage. Please keep your contact
|
||||
|
|
|
@ -11,6 +11,7 @@ MANAGER REMOVED: {{ manager_removed.email }}
|
|||
|
||||
WHY DID YOU RECEIVE THIS EMAIL?
|
||||
You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever a domain manager is removed from that domain.
|
||||
|
||||
If you have questions or concerns, reach out to the person who removed the domain manager or reply to this email.
|
||||
|
||||
THANK YOU
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
{% load field_helpers %}
|
||||
<div id="member-basic-permissions" class="margin-top-2">
|
||||
<h2>What permissions do you want to add?</h2>
|
||||
<h2>Member permissions</h2>
|
||||
<p>Configure the permissions for this member. Basic members cannot manage member permissions or organization metadata.</p>
|
||||
|
||||
<h3 class="margin-bottom-0">Domains <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
|
||||
<h3 class="margin-bottom-neg-1 margin-top-4">Domains <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" label_classes="margin-top-1" %}
|
||||
{% input_with_errors form.domain_permissions %}
|
||||
{% endwith %}
|
||||
|
||||
<h3 class="margin-bottom-0">Domain requests <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
|
||||
<h3 class="margin-bottom-neg-1 margin-top-2">Domain requests <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" label_classes="margin-top-1" %}
|
||||
{% input_with_errors form.domain_request_permissions %}
|
||||
{% endwith %}
|
||||
|
||||
<h3 class="margin-bottom-0">Members <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
|
||||
<h3 class="margin-bottom-neg-1 margin-top-2">Members <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" label_classes="margin-top-1" %}
|
||||
{% input_with_errors form.member_permissions %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<h4 class="margin-bottom-0">Assigned domains</h4>
|
||||
{% if domain_count > 0 %}
|
||||
<h4 class="margin-bottom-0">Domains assigned</h4>
|
||||
<p class="margin-top-0">{{domain_count}}</p>
|
||||
{% else %}
|
||||
<p class="margin-top-0">This member does not manage any domains.{% if manage_button %} To assign this member a domain, click "Manage".{% endif %}</p>
|
||||
<p class="margin-top-0">This member does not manage any domains.{% if manage_button %} To assign this member a domain, click "Edit".{% endif %}</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
></div>
|
||||
</div>
|
||||
<div class="display-none margin-bottom-4" id="edit-member-domains__no-data">
|
||||
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
||||
<p>This member does not manage any domains.</p>
|
||||
</div>
|
||||
<div class="display-none margin-bottom-4" id="edit-member-domains__no-search-results">
|
||||
<p>No results found</p>
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
></div>
|
||||
</div>
|
||||
<div class="display-none margin-bottom-4" id="member-domains__no-data">
|
||||
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
||||
<p>This member does not manage any domains.</p>
|
||||
</div>
|
||||
<div class="display-none margin-bottom-4" id="member-domains__no-search-results">
|
||||
<p>No results found</p>
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
<p class="margin-top-0">{{ permissions.role_display }}</p>
|
||||
|
||||
<h4 class="margin-bottom-0 text-primary">Domains</h4>
|
||||
<p class="margin-top-0">{{ permissions.domains_display }}</p>
|
||||
<p class="margin-top-0">{{ permissions.domains_display }}: {{ permissions.domains_description_display }}</p>
|
||||
|
||||
<h4 class="margin-bottom-0 text-primary">Domain requests</h4>
|
||||
<p class="margin-top-0">{{ permissions.domain_requests_display }}</p>
|
||||
<p class="margin-top-0">{{ permissions.domain_requests_display }}: {{ permissions.domain_requests_description_display }}</p>
|
||||
|
||||
<h4 class="margin-bottom-0 text-primary">Members</h4>
|
||||
<p class="margin-top-0">{{ permissions.members_display }}</p>
|
||||
<p class="margin-top-0">{{ permissions.members_display }}: {{ permissions.members_description_display }}</p>
|
||||
|
|
|
@ -134,18 +134,29 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if editable and edit_link %}
|
||||
<div class="text-right">
|
||||
<a
|
||||
href="{{ edit_link }}"
|
||||
class="usa-link usa-link--icon font-sans-sm line-height-sans-4"
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#{% if manage_button %}settings{% elif view_button %}visibility{% else %}edit{% endif %}"></use>
|
||||
</svg>
|
||||
{% if manage_button %}Manage{% elif view_button %}View{% else %}Edit{% endif %}<span class="sr-only"> {{ title }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% comment %}We have conditions where an edit_link is set but editable can be true or false{% endcomment %}
|
||||
{% if edit_link %}
|
||||
{% if manage_button or editable or view_button %}
|
||||
<div class="text-right">
|
||||
<a
|
||||
href="{{ edit_link }}"
|
||||
class="usa-link usa-link--icon font-sans-sm line-height-sans-4"
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#{% if manage_button %}settings{% elif editable %}edit{% else %}visibility{% endif %}"></use>
|
||||
</svg>
|
||||
{% if manage_button %}
|
||||
Manage
|
||||
{% elif editable %}
|
||||
Edit
|
||||
{% else %}
|
||||
View
|
||||
{% endif %}
|
||||
<span class="sr-only"> {{ title|default:"Page" }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{# the entire logged in page goes here #}
|
||||
|
||||
<div class="grid-row {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
|
||||
<div class="desktop:grid-col-10 {% if not is_widescreen_centered %}tablet:grid-col-11 {% else %}tablet:padding-left-4 tablet:padding-right-4 tablet:grid-col-12 desktop:grid-offset-1{% endif %}">
|
||||
<div class="desktop:grid-col-10{% if not is_widescreen_centered %} tablet:grid-col-11{% else %} tablet:grid-col-12 desktop:grid-offset-1{% endif %}">
|
||||
|
||||
{% block portfolio_content %}{% endblock %}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ Organization member
|
|||
<h1 class="margin-bottom-3">Manage member</h1>
|
||||
|
||||
<div class="tablet:display-flex tablet:flex-justify">
|
||||
<h2 class="margin-top-0 margin-bottom-3 break-word">
|
||||
<h2 class="margin-top-0 margin-bottom-3 break-word flex-align-self-baseline">
|
||||
{% if member %}
|
||||
{{ member.email }}
|
||||
{% elif portfolio_invitation %}
|
||||
|
@ -46,6 +46,7 @@ Organization member
|
|||
data-member-id="{{ member.id }}"
|
||||
data-num-domains="{{ portfolio_permission.get_managed_domains_count }}"
|
||||
data-member-email="{{ member.email }}"
|
||||
class="flex-align-self-baseline"
|
||||
>
|
||||
<!-- JS should inject member kebob here -->
|
||||
</div>
|
||||
|
@ -56,6 +57,7 @@ Organization member
|
|||
data-member-id="{{ portfolio_invitation.id }}"
|
||||
data-num-domains="{{ portfolio_invitation.get_managed_domains_count }}"
|
||||
data-member-email="{{ portfolio_invitation.email }}"
|
||||
class="flex-align-self-baseline"
|
||||
>
|
||||
<!-- JS should inject invited kebob here -->
|
||||
</div>
|
||||
|
@ -65,9 +67,9 @@ Organization member
|
|||
|
||||
<form method="post" id="member-delete-form" action="{{ request.path }}/delete"> {% csrf_token %} </form>
|
||||
<address>
|
||||
<strong class="text-primary-dark">Last active:</strong>
|
||||
<strong class="text-primary-darker">Last active:</strong>
|
||||
{% if member and member.last_login %}
|
||||
{{ member.last_login }}
|
||||
{{ member.last_login|date:"F j, Y" }}
|
||||
{% elif portfolio_invitation %}
|
||||
Invited
|
||||
{% else %}
|
||||
|
@ -75,7 +77,7 @@ Organization member
|
|||
{% endif %}
|
||||
<br />
|
||||
|
||||
<strong class="text-primary-dark">Full name:</strong>
|
||||
<strong class="text-primary-darker">Full name:</strong>
|
||||
{% if member %}
|
||||
{% if member.first_name or member.last_name %}
|
||||
{{ member.get_formatted_name }}
|
||||
|
@ -87,7 +89,7 @@ Organization member
|
|||
{% endif %}
|
||||
<br />
|
||||
|
||||
<strong class="text-primary-dark">Title or organization role:</strong>
|
||||
<strong class="text-primary-darker">Title or organization role:</strong>
|
||||
{% if member and member.title %}
|
||||
{{ member.title }}
|
||||
{% else %}
|
||||
|
@ -101,11 +103,11 @@ Organization member
|
|||
{% include "includes/summary_item.html" with title='Member access and permissions' permissions=True value=portfolio_invitation edit_link=edit_url editable=has_edit_members_portfolio_permission %}
|
||||
{% endif %}
|
||||
|
||||
{% comment %}view_button is passed below as true in all cases. This is because manage_button logic will trump view_button logic; ie. if manage_button is true, view_button will never be looked at{% endcomment %}
|
||||
{% comment %}view_button is passed below as true in all cases. This is because editable logic will trump view_button logic; ie. if editable is true, view_button will never be looked at{% endcomment %}
|
||||
{% if portfolio_permission %}
|
||||
{% include "includes/summary_item.html" with title='Domain management' domain_mgmt=True value=portfolio_permission.get_managed_domains_count edit_link=domains_url editable=True manage_button=has_edit_members_portfolio_permission view_button=True %}
|
||||
{% include "includes/summary_item.html" with title='Domain assignments' domain_mgmt=True value=portfolio_permission.get_managed_domains_count edit_link=domains_url editable=has_edit_members_portfolio_permission view_button=True %}
|
||||
{% elif portfolio_invitation %}
|
||||
{% include "includes/summary_item.html" with title='Domain management' domain_mgmt=True value=portfolio_invitation.get_managed_domains_count edit_link=domains_url editable=True manage_button=has_edit_members_portfolio_permission view_button=True %}
|
||||
{% include "includes/summary_item.html" with title='Domain assignments' domain_mgmt=True value=portfolio_invitation.get_managed_domains_count edit_link=domains_url editable=has_edit_members_portfolio_permission view_button=True %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
</div>
|
||||
|
||||
<p class="margin-top-0 margin-bottom-4 maxw-none">
|
||||
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.
|
||||
Members can update information related to their assigned domains, including security email and DNS name servers. They can also invite other managers to those domains.
|
||||
</p>
|
||||
|
||||
{% include "includes/member_domains_table.html" %}
|
||||
|
|
|
@ -41,9 +41,11 @@
|
|||
<section id="domain-assignments-edit-view">
|
||||
<h1>Edit domain assignments</h1>
|
||||
|
||||
<p class="margin-top-0 margin-bottom-4 maxw-none">
|
||||
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.
|
||||
When you save this form the member will get an email to notify them of any changes.
|
||||
<p class="margin-top-0 maxw-none">
|
||||
Use the checkboxes to add or remove domain assignments for this member. Then proceed to the next step to confirm and save your changes.
|
||||
</p>
|
||||
<p class="margin-bottom-4 maxw-none">
|
||||
Domains must have at least one domain manager. You can't remove this member from a domain if they’re the only one assigned to it.
|
||||
</p>
|
||||
|
||||
{% include "includes/member_domains_edit_table.html" %}
|
||||
|
@ -72,33 +74,19 @@
|
|||
</section>
|
||||
|
||||
<section id="domain-assignments-readonly-view" class="display-none">
|
||||
<h1 class="margin-bottom-3">Review domain assignments</h1>
|
||||
<h1 class="margin-bottom-4">Review and apply domain assignment changes</h1>
|
||||
|
||||
<h2 class="margin-top-0">Would you like to continue with the following domain assignment changes for
|
||||
<h3 class="margin-bottom-05 h4">Member</h3>
|
||||
<p class="margin-top-0">
|
||||
{% if member %}
|
||||
{{ member.email }}
|
||||
{% else %}
|
||||
{{ portfolio_invitation.email }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<p class="margin-bottom-4">
|
||||
When you save this form the member will get an email to notify them of any changes.
|
||||
</p>
|
||||
|
||||
<div id="domain-assignments-summary" class="margin-bottom-5">
|
||||
<!-- AJAX will populate this summary -->
|
||||
<h3 class="margin-bottom-1 h4">Unassigned domains</h3>
|
||||
<ul class="usa-list usa-list--unstyled">
|
||||
<li>item1</li>
|
||||
<li>item2</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="margin-bottom-0 h4">Assigned domains</h3>
|
||||
<ul class="usa-list usa-list--unstyled">
|
||||
<li>item1</li>
|
||||
<li>item2</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="usa-button-group">
|
||||
|
@ -118,7 +106,7 @@
|
|||
type="button"
|
||||
class="usa-button"
|
||||
>
|
||||
Save
|
||||
Apply changes
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -18,24 +18,24 @@
|
|||
{% endblock messages%}
|
||||
|
||||
<!-- Navigation breadcrumbs -->
|
||||
<nav class="usa-breadcrumb padding-top-0 bg-gray-1" aria-label="Domain request breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
{% url 'members' as url %}
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||
{% else %}
|
||||
{% url 'invitedmember' pk=invitation.id as url2 %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0 bg-gray-1" aria-label="Portfolio member breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'members' %}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
{% if member %}
|
||||
{% url 'member' pk=member.pk as back_url %}
|
||||
{% elif invitation %}
|
||||
{% url 'invitedmember' pk=invitation.pk as back_url %}
|
||||
{% endif %}
|
||||
<a href="{{ back_url }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
||||
<a href="{{ url2 }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
||||
</li>
|
||||
{% comment %} Manage members {% endcomment %}
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Member access and permissions</span>
|
||||
</li>
|
||||
</ol>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Page header -->
|
||||
|
@ -78,12 +78,12 @@
|
|||
<!-- Member access radio buttons (Toggles other sections) -->
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend>
|
||||
<h2 class="margin-top-0">Member Access</h2>
|
||||
<h2 class="margin-top-0">Member access</h2>
|
||||
</legend>
|
||||
|
||||
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% with group_classes="margin-top-0" add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors form.role %}
|
||||
{% endwith %}
|
||||
|
||||
|
@ -98,14 +98,14 @@
|
|||
<div class="margin-top-3">
|
||||
<a
|
||||
type="button"
|
||||
href="{{ back_url }}"
|
||||
href="{{ url2 }}"
|
||||
class="usa-button usa-button--outline"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Cancel editing member"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="usa-button">Update Member</button>
|
||||
<button type="submit" class="usa-button">Update member</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -55,9 +55,9 @@
|
|||
<h2>What level of access would you like to grant this member?</h2>
|
||||
</legend>
|
||||
|
||||
<p class="margin-y-0">Select one <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
|
||||
<p class="margin-y-0">Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
|
||||
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% with group_classes="margin-top-0" add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors form.role %}
|
||||
{% endwith %}
|
||||
</fieldset>
|
||||
|
@ -67,7 +67,7 @@
|
|||
<!-- Basic access form -->
|
||||
{% include "includes/member_basic_permissions.html" %}
|
||||
|
||||
<h3 class="margin-bottom-1">Domain management</h3>
|
||||
<h3 class="margin-bottom-1">Domain assignments</h3>
|
||||
|
||||
<p class="margin-top-0">After you invite this person to your organization, you can assign domain management permissions on their member profile.</p>
|
||||
|
||||
|
@ -88,7 +88,7 @@
|
|||
aria-controls="invite-member-modal"
|
||||
data-open-modal
|
||||
>Trigger invite member modal</a>
|
||||
<button id="invite_new_member_submit" type="submit" class="usa-button">Invite Member</button>
|
||||
<button id="invite_new_member_submit" type="submit" class="usa-button">Invite member</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -104,13 +104,10 @@
|
|||
<h2 class="usa-modal__heading" id="invite-member-heading">
|
||||
Invite this member to the organization?
|
||||
</h2>
|
||||
<h3>Member information and permissions</h3>
|
||||
<!-- Display email as a header and access level -->
|
||||
<h4 class="margin-bottom-0">Email</h4>
|
||||
<p class="margin-top-0" id="modalEmail"></p>
|
||||
<h3>Member access and permissions</h3>
|
||||
|
||||
<h4 class="margin-bottom-0">Member Access</h4>
|
||||
<p class="margin-top-0" id="modalAccessLevel"></p>
|
||||
<p class="margin-bottom-1"><strong class="text-primary-darker">Email:</strong> <span id="modalEmail"></span></p>
|
||||
<p class="margin-top-0 margin-bottom-1"><strong class="text-primary-darker">Member access:</strong> <span id="modalAccessLevel"></span></p>
|
||||
|
||||
<!-- Dynamic Permissions Details -->
|
||||
<div id="permission_details"></div>
|
||||
|
@ -123,7 +120,7 @@
|
|||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
data-close-modal
|
||||
onclick="closeModal()"
|
||||
>
|
||||
|
|
|
@ -667,7 +667,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
# Simulate clicking on edit button
|
||||
edit_page = renewal_page.click(href=edit_button_url, index=1)
|
||||
self.assertEqual(edit_page.status_code, 200)
|
||||
self.assertContains(edit_page, "Domain managers can update all information related to a domain")
|
||||
self.assertContains(edit_page, "Domain managers can update information related to this domain")
|
||||
|
||||
def test_domain_renewal_form_not_expired_or_expiring(self):
|
||||
"""Checking that if the user's domain is not expired or expiring that user should not be able
|
||||
|
@ -764,7 +764,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
# assert that the non-portfolio view contains Role column and doesn't contain Admin
|
||||
self.assertContains(response, "Role</th>")
|
||||
self.assertNotContains(response, "Admin")
|
||||
self.assertContains(response, "This domain has one manager. Adding more can prevent issues.")
|
||||
self.assertContains(response, "This domain has only one manager. Consider adding another manager")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
@ -775,7 +775,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
# assert that the portfolio view doesn't contain Role column and does contain Admin
|
||||
self.assertNotContains(response, "Role</th>")
|
||||
self.assertContains(response, "Admin")
|
||||
self.assertContains(response, "This domain has one manager. Adding more can prevent issues.")
|
||||
self.assertContains(response, "This domain has only one manager. Consider adding another manager")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_user_add(self):
|
||||
|
|
|
@ -951,14 +951,12 @@ class TestPortfolio(WebTest):
|
|||
self.assertContains(response, "Admin")
|
||||
self.assertContains(response, "Creator")
|
||||
self.assertContains(response, "Manager")
|
||||
self.assertContains(
|
||||
response, 'This member does not manage any domains. To assign this member a domain, click "Manage"'
|
||||
)
|
||||
self.assertContains(response, "This member does not manage any domains.")
|
||||
|
||||
# Assert buttons and links within the page are correct
|
||||
self.assertContains(response, "wrapper-delete-action") # test that 3 dot is present
|
||||
self.assertContains(response, "sprite.svg#edit") # test that Edit link is present
|
||||
self.assertContains(response, "sprite.svg#settings") # test that Manage link is present
|
||||
self.assertContains(response, "sprite.svg#edit") # test that Manage link is present
|
||||
self.assertNotContains(response, "sprite.svg#visibility") # test that View link is not present
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1066,13 +1064,11 @@ class TestPortfolio(WebTest):
|
|||
self.assertContains(response, "Viewer")
|
||||
self.assertContains(response, "Creator")
|
||||
self.assertContains(response, "Manager")
|
||||
self.assertContains(
|
||||
response, 'This member does not manage any domains. To assign this member a domain, click "Manage"'
|
||||
)
|
||||
self.assertContains(response, "This member does not manage any domains.")
|
||||
# Assert buttons and links within the page are correct
|
||||
self.assertContains(response, "wrapper-delete-action") # test that 3 dot is present
|
||||
self.assertContains(response, "sprite.svg#edit") # test that Edit link is present
|
||||
self.assertContains(response, "sprite.svg#settings") # test that Manage link is present
|
||||
self.assertContains(response, "sprite.svg#edit") # test that Manage link is present
|
||||
self.assertNotContains(response, "sprite.svg#visibility") # test that View link is not present
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -2744,7 +2740,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
def test_post_with_no_changes(self):
|
||||
"""Test that no changes message is displayed when no changes are made."""
|
||||
"""Test that success message is displayed when no changes are made."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.post(self.url, {})
|
||||
|
@ -2756,7 +2752,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(str(messages[0]), "No changes detected.")
|
||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
@ -3085,7 +3081,7 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(str(messages[0]), "No changes detected.")
|
||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
|
|
@ -1258,21 +1258,6 @@ class DomainUsersView(DomainBaseView):
|
|||
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Get method for DomainUsersView."""
|
||||
# Call the parent class's `get` method to get the response and context
|
||||
response = super().get(request, *args, **kwargs)
|
||||
|
||||
# Ensure context is available after the parent call
|
||||
context = response.context_data if hasattr(response, "context_data") else {}
|
||||
|
||||
# Check if context contains `domain_managers_roles` and its length is 1
|
||||
if context.get("domain_manager_roles") and len(context["domain_manager_roles"]) == 1:
|
||||
# Add an info message
|
||||
messages.info(request, "This domain has one manager. Adding more can prevent issues.")
|
||||
|
||||
return response
|
||||
|
||||
def _add_domain_manager_roles_to_context(self, context, portfolio):
|
||||
"""Add domain_manager_roles to context separately, as roles need admin indicator."""
|
||||
|
||||
|
|
|
@ -236,6 +236,7 @@ class PortfolioMemberEditView(DetailView, View):
|
|||
{
|
||||
"form": form,
|
||||
"member": user,
|
||||
"portfolio_permission": portfolio_permission,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -355,32 +356,32 @@ class PortfolioMemberDomainsEditView(DetailView, View):
|
|||
if removed_domain_ids is None:
|
||||
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||
|
||||
if added_domain_ids or removed_domain_ids:
|
||||
try:
|
||||
self._process_added_domains(added_domain_ids, member, request.user, portfolio)
|
||||
self._process_removed_domains(removed_domain_ids, member)
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||
except IntegrityError:
|
||||
messages.error(
|
||||
request,
|
||||
"A database error occurred while saving changes. If the issue persists, "
|
||||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error("A database error occurred while saving changes.", exc_info=True)
|
||||
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
f"An unexpected error occurred: {str(e)}. If the issue persists, "
|
||||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error(f"An unexpected error occurred: {str(e)}", exc_info=True)
|
||||
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
|
||||
else:
|
||||
messages.info(request, "No changes detected.")
|
||||
if not (added_domain_ids or removed_domain_ids):
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||
|
||||
try:
|
||||
self._process_added_domains(added_domain_ids, member, request.user, portfolio)
|
||||
self._process_removed_domains(removed_domain_ids, member)
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||
except IntegrityError:
|
||||
messages.error(
|
||||
request,
|
||||
"A database error occurred while saving changes. If the issue persists, "
|
||||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error("A database error occurred while saving changes.", exc_info=True)
|
||||
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
f"An unexpected error occurred: {str(e)}. If the issue persists, "
|
||||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error(f"An unexpected error occurred: {str(e)}", exc_info=True)
|
||||
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
|
||||
|
||||
def _parse_domain_ids(self, domain_data, domain_type):
|
||||
"""
|
||||
Parses the domain IDs from the request and handles JSON errors.
|
||||
|
@ -644,32 +645,32 @@ class PortfolioInvitedMemberDomainsEditView(DetailView, View):
|
|||
if removed_domain_ids is None:
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||
|
||||
if added_domain_ids or removed_domain_ids:
|
||||
try:
|
||||
self._process_added_domains(added_domain_ids, email, request.user, portfolio)
|
||||
self._process_removed_domains(removed_domain_ids, email)
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||
except IntegrityError:
|
||||
messages.error(
|
||||
request,
|
||||
"A database error occurred while saving changes. If the issue persists, "
|
||||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error("A database error occurred while saving changes.", exc_info=True)
|
||||
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
f"An unexpected error occurred: {str(e)}. If the issue persists, "
|
||||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True)
|
||||
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
|
||||
else:
|
||||
messages.info(request, "No changes detected.")
|
||||
if not (added_domain_ids or removed_domain_ids):
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||
|
||||
try:
|
||||
self._process_added_domains(added_domain_ids, email, request.user, portfolio)
|
||||
self._process_removed_domains(removed_domain_ids, email)
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||
except IntegrityError:
|
||||
messages.error(
|
||||
request,
|
||||
"A database error occurred while saving changes. If the issue persists, "
|
||||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error("A database error occurred while saving changes.", exc_info=True)
|
||||
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
f"An unexpected error occurred: {str(e)}. If the issue persists, "
|
||||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True)
|
||||
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
|
||||
|
||||
def _parse_domain_ids(self, domain_data, domain_type):
|
||||
"""
|
||||
Parses the domain IDs from the request and handles JSON errors.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue