mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-23 11:16:07 +02:00
Merge branch 'main' into el/3282-design-review
This commit is contained in:
commit
30cadfa115
97 changed files with 2862 additions and 2166 deletions
|
@ -1447,6 +1447,7 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by first name, last name, email, or portfolio."
|
||||
|
||||
change_form_template = "django/admin/user_portfolio_permission_change_form.html"
|
||||
delete_confirmation_template = "django/admin/user_portfolio_permission_delete_confirmation.html"
|
||||
|
||||
def get_roles(self, obj):
|
||||
readable_roles = obj.get_readable_roles()
|
||||
|
@ -1790,6 +1791,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
autocomplete_fields = ["portfolio"]
|
||||
|
||||
change_form_template = "django/admin/portfolio_invitation_change_form.html"
|
||||
delete_confirmation_template = "django/admin/portfolio_invitation_delete_confirmation.html"
|
||||
|
||||
# Select portfolio invitations to change -> Portfolio invitations
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
|
@ -3858,11 +3860,13 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Using variables to get past the linter
|
||||
message1 = f"Cannot delete Domain when in state {obj.state}"
|
||||
message2 = f"This subdomain is being used as a hostname on another domain: {err.note}"
|
||||
message3 = f"Command failed with note: {err.note}"
|
||||
# Human-readable mappings of ErrorCodes. Can be expanded.
|
||||
error_messages = {
|
||||
# noqa on these items as black wants to reformat to an invalid length
|
||||
ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION: message1,
|
||||
ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION: message2,
|
||||
ErrorCode.COMMAND_FAILED: message3,
|
||||
}
|
||||
|
||||
message = "Cannot connect to the registry"
|
||||
|
|
|
@ -5695,19 +5695,35 @@ const createHeaderButton = (header, headerName) => {
|
|||
buttonEl.setAttribute("tabindex", "0");
|
||||
buttonEl.classList.add(SORT_BUTTON_CLASS);
|
||||
// ICON_SOURCE
|
||||
// ---- END DOTGOV EDIT
|
||||
// Change icons on sort, use source from arro_upward and arrow_downward
|
||||
// buttonEl.innerHTML = Sanitizer.escapeHTML`
|
||||
// <svg class="${PREFIX}-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
// <g class="descending" fill="transparent">
|
||||
// <path d="M17 17L15.59 15.59L12.9999 18.17V2H10.9999V18.17L8.41 15.58L7 17L11.9999 22L17 17Z" />
|
||||
// </g>
|
||||
// <g class="ascending" fill="transparent">
|
||||
// <path transform="rotate(180, 12, 12)" d="M17 17L15.59 15.59L12.9999 18.17V2H10.9999V18.17L8.41 15.58L7 17L11.9999 22L17 17Z" />
|
||||
// </g>
|
||||
// <g class="unsorted" fill="transparent">
|
||||
// <polygon points="15.17 15 13 17.17 13 6.83 15.17 9 16.58 7.59 12 3 7.41 7.59 8.83 9 11 6.83 11 17.17 8.83 15 7.42 16.41 12 21 16.59 16.41 15.17 15"/>
|
||||
// </g>
|
||||
// </svg>
|
||||
// `;
|
||||
buttonEl.innerHTML = Sanitizer.escapeHTML`
|
||||
<svg class="${PREFIX}-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g class="descending" fill="transparent">
|
||||
<path d="M17 17L15.59 15.59L12.9999 18.17V2H10.9999V18.17L8.41 15.58L7 17L11.9999 22L17 17Z" />
|
||||
<path d="m20 12-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/>
|
||||
</g>
|
||||
<g class="ascending" fill="transparent">
|
||||
<path transform="rotate(180, 12, 12)" d="M17 17L15.59 15.59L12.9999 18.17V2H10.9999V18.17L8.41 15.58L7 17L11.9999 22L17 17Z" />
|
||||
<path d="m4 12 1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>
|
||||
</g>
|
||||
<g class="unsorted" fill="transparent">
|
||||
<polygon points="15.17 15 13 17.17 13 6.83 15.17 9 16.58 7.59 12 3 7.41 7.59 8.83 9 11 6.83 11 17.17 8.83 15 7.42 16.41 12 21 16.59 16.41 15.17 15"/>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
// ---- END DOTGOV EDIT
|
||||
header.appendChild(buttonEl);
|
||||
updateSortLabel(header, headerName);
|
||||
};
|
||||
|
|
|
@ -2,11 +2,41 @@ import { submitForm } from './helpers.js';
|
|||
|
||||
export function initDomainRequestForm() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const button = document.getElementById("domain-request-form-submit-button");
|
||||
if (button) {
|
||||
button.addEventListener("click", function () {
|
||||
submitForm("submit-domain-request-form");
|
||||
});
|
||||
}
|
||||
// These are the request steps in DomainRequestWizard, such as current_websites or review
|
||||
initRequestStepCurrentWebsitesListener();
|
||||
initRequestStepReviewListener();
|
||||
});
|
||||
}
|
||||
|
||||
function initRequestStepReviewListener() {
|
||||
const button = document.getElementById("domain-request-form-submit-button");
|
||||
if (button) {
|
||||
button.addEventListener("click", function () {
|
||||
submitForm("submit-domain-request-form");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initRequestStepCurrentWebsitesListener() {
|
||||
//register-form-step
|
||||
const addAnotherSiteButton = document.getElementById("submit-domain-request--site-button");
|
||||
if (addAnotherSiteButton) {
|
||||
// Check for focus state in sessionStorage
|
||||
const focusTarget = sessionStorage.getItem("lastFocusedElement");
|
||||
if (focusTarget) {
|
||||
document.querySelector(focusTarget)?.focus();
|
||||
}
|
||||
// Add form submit handler to store focus state
|
||||
const form = document.querySelector("form");
|
||||
if (form) {
|
||||
form.addEventListener("submit", () => {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
sessionStorage.setItem("lastFocusedElement", "#" + activeElement.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
// We only want to do this action once, so we clear out the session
|
||||
sessionStorage.removeItem("lastFocusedElement");
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -23,10 +23,13 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
this.readonlyModeContainer = document.getElementById('domain-assignments-readonly-view');
|
||||
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();
|
||||
this.saveButton = document.getElementById('save-domain-assignments');
|
||||
}
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -75,7 +75,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 = '';
|
||||
|
@ -96,7 +96,7 @@ export class MembersTable extends BaseTable {
|
|||
</button>
|
||||
`;
|
||||
|
||||
showMoreRow.innerHTML = `<td colspan='3' headers="header-member row-header-${unique_id}" class="padding-top-0"><div class='grid-row'>${domainsHTML} ${permissionsHTML}</div></td>`;
|
||||
showMoreRow.innerHTML = `<td colspan='4' headers="header-member row-header-${unique_id}" class="padding-top-0"><div class='grid-row grid-gap-2'>${domainsHTML} ${permissionsHTML}</div></td>`;
|
||||
showMoreRow.classList.add('show-more-content');
|
||||
showMoreRow.classList.add('display-none');
|
||||
showMoreRow.id = unique_id;
|
||||
|
@ -111,13 +111,13 @@ export class MembersTable extends BaseTable {
|
|||
</td>
|
||||
<td 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>
|
||||
`;
|
||||
|
@ -247,10 +247,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
|
||||
|
@ -259,13 +260,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;
|
||||
}
|
||||
|
||||
|
@ -377,40 +380,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
|
||||
|
|
|
@ -520,15 +520,6 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too
|
|||
}
|
||||
}
|
||||
|
||||
.module--custom {
|
||||
a {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: solid 1px var(--darkened-bg);
|
||||
background: var(--darkened-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-modal--django-admin .usa-prose ul > li {
|
||||
list-style-type: inherit;
|
||||
// Styling based off of the <p> styling in django admin
|
||||
|
@ -839,6 +830,17 @@ div.dja__model-description{
|
|||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.module caption {
|
||||
// Match the old <h2> size for django admin
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
// text-bold doesn't work here due to style overrides, unfortunately.
|
||||
// This is a workaround.
|
||||
caption.text-bold {
|
||||
font-weight: font-weight('bold');
|
||||
}
|
||||
|
||||
.wrapped-button-group {
|
||||
// This button group has too many items
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -41,13 +41,8 @@ th {
|
|||
}
|
||||
}
|
||||
|
||||
// The member table has an extra "expand" row, which looks like a single row.
|
||||
// But the DOM disagrees - so we basically need to hide the border on both rows.
|
||||
#members__table-wrapper .dotgov-table tr:nth-last-child(2) td,
|
||||
#members__table-wrapper .dotgov-table tr:nth-last-child(2) th {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// .dotgov-table allows us to customize .usa-table on the user-facing pages,
|
||||
// while leaving the default styles for use on the admin pages
|
||||
.dotgov-table {
|
||||
width: 100%;
|
||||
|
||||
|
@ -68,7 +63,8 @@ th {
|
|||
border-bottom: 1px solid color('base-lighter');
|
||||
}
|
||||
|
||||
thead th {
|
||||
thead th,
|
||||
thead th[aria-sort] {
|
||||
color: color('primary-darker');
|
||||
border-bottom: 2px solid color('base-light');
|
||||
}
|
||||
|
@ -93,17 +89,46 @@ th {
|
|||
}
|
||||
}
|
||||
|
||||
@include at-media(tablet-lg) {
|
||||
th[data-sortable] .usa-table__header__button {
|
||||
right: auto;
|
||||
|
||||
&[aria-sort=ascending],
|
||||
&[aria-sort=descending],
|
||||
&:not([aria-sort]) {
|
||||
right: auto;
|
||||
// Sortable headers
|
||||
th[data-sortable][aria-sort=ascending],
|
||||
th[data-sortable][aria-sort=descending] {
|
||||
background-color: transparent;
|
||||
.usa-table__header__button {
|
||||
background-color: color('accent-cool-lightest');
|
||||
border-radius: units(.5);
|
||||
color: color('primary-darker');
|
||||
&:hover {
|
||||
background-color: color('accent-cool-lightest');
|
||||
}
|
||||
}
|
||||
}
|
||||
@include at-media(tablet-lg) {
|
||||
th[data-sortable]:not(.left-align-sort-button) .usa-table__header__button {
|
||||
// position next to the copy
|
||||
right: auto;
|
||||
// slide left to mock a margin between the copy and the icon
|
||||
transform: translateX(units(1));
|
||||
// fix vertical alignment
|
||||
top: units(1.5);
|
||||
}
|
||||
th[data-sortable].left-align-sort-button .usa-table__header__button {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently the 'flash' when sort is clicked,
|
||||
// this will become persistent if the double-sort bug is fixed
|
||||
td[data-sort-active],
|
||||
th[data-sort-active] {
|
||||
background-color: color('primary-lightest');
|
||||
}
|
||||
}
|
||||
|
||||
// The member table has an extra "expand" row, which looks like a single row.
|
||||
// But the DOM disagrees - so we basically need to hide the border on both rows.
|
||||
#members__table-wrapper .dotgov-table tr:nth-last-child(2) td,
|
||||
#members__table-wrapper .dotgov-table tr:nth-last-child(2) th {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dotgov-table--cell-padding-2 {
|
||||
|
@ -118,7 +143,7 @@ th {
|
|||
}
|
||||
|
||||
.usa-table--bg-transparent {
|
||||
td, thead th {
|
||||
td, th, thead th {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ in the form $setting: value,
|
|||
----------------------------*/
|
||||
$theme-font-weight-medium: 400,
|
||||
$theme-font-weight-semibold: 600,
|
||||
$theme-font-weight-bold: 700,
|
||||
|
||||
/*---------------------------
|
||||
## Font roles
|
||||
|
|
|
@ -201,6 +201,8 @@ MIDDLEWARE = [
|
|||
"waffle.middleware.WaffleMiddleware",
|
||||
"registrar.registrar_middleware.CheckUserProfileMiddleware",
|
||||
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
||||
# Restrict access using Opt-Out approach
|
||||
"registrar.registrar_middleware.RestrictAccessMiddleware",
|
||||
]
|
||||
|
||||
# application object used by Django's built-in servers (e.g. `runserver`)
|
||||
|
|
|
@ -68,7 +68,7 @@ for step, view in [
|
|||
(PortfolioDomainRequestStep.REQUESTING_ENTITY, views.RequestingEntity),
|
||||
(PortfolioDomainRequestStep.ADDITIONAL_DETAILS, views.PortfolioAdditionalDetails),
|
||||
]:
|
||||
domain_request_urls.append(path(f"<int:id>/{step}/", view.as_view(), name=step))
|
||||
domain_request_urls.append(path(f"<int:domain_request_pk>/{step}/", view.as_view(), name=step))
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -260,27 +260,27 @@ urlpatterns = [
|
|||
name="export_data_type_user",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:id>/edit/",
|
||||
"domain-request/<int:domain_request_pk>/edit/",
|
||||
views.DomainRequestWizard.as_view(),
|
||||
name=views.DomainRequestWizard.EDIT_URL_NAME,
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>",
|
||||
"domain-request/<int:domain_request_pk>",
|
||||
views.DomainRequestStatus.as_view(),
|
||||
name="domain-request-status",
|
||||
),
|
||||
path(
|
||||
"domain-request/viewonly/<int:pk>",
|
||||
"domain-request/viewonly/<int:domain_request_pk>",
|
||||
views.PortfolioDomainRequestStatusViewOnly.as_view(),
|
||||
name="domain-request-status-viewonly",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>/withdraw",
|
||||
"domain-request/<int:domain_request_pk>/withdraw",
|
||||
views.DomainRequestWithdrawConfirmation.as_view(),
|
||||
name="domain-request-withdraw-confirmation",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>/withdrawconfirmed",
|
||||
"domain-request/<int:domain_request_pk>/withdrawconfirmed",
|
||||
views.DomainRequestWithdrawn.as_view(),
|
||||
name="domain-request-withdrawn",
|
||||
),
|
||||
|
@ -296,56 +296,60 @@ urlpatterns = [
|
|||
lambda r: always_404(r, "We forgot to include this link, sorry."),
|
||||
name="todo",
|
||||
),
|
||||
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
|
||||
path("domain/<int:pk>/prototype-dns", views.PrototypeDomainDNSRecordView.as_view(), name="prototype-domain-dns"),
|
||||
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
||||
path("domain/<int:domain_pk>", views.DomainView.as_view(), name="domain"),
|
||||
path(
|
||||
"domain/<int:pk>/dns",
|
||||
"domain/<int:domain_pk>/prototype-dns",
|
||||
views.PrototypeDomainDNSRecordView.as_view(),
|
||||
name="prototype-domain-dns",
|
||||
),
|
||||
path("domain/<int:domain_pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
||||
path(
|
||||
"domain/<int:domain_pk>/dns",
|
||||
views.DomainDNSView.as_view(),
|
||||
name="domain-dns",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/nameservers",
|
||||
"domain/<int:domain_pk>/dns/nameservers",
|
||||
views.DomainNameserversView.as_view(),
|
||||
name="domain-dns-nameservers",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/dnssec",
|
||||
"domain/<int:domain_pk>/dns/dnssec",
|
||||
views.DomainDNSSECView.as_view(),
|
||||
name="domain-dns-dnssec",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/dnssec/dsdata",
|
||||
"domain/<int:domain_pk>/dns/dnssec/dsdata",
|
||||
views.DomainDsDataView.as_view(),
|
||||
name="domain-dns-dnssec-dsdata",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/org-name-address",
|
||||
"domain/<int:domain_pk>/org-name-address",
|
||||
views.DomainOrgNameAddressView.as_view(),
|
||||
name="domain-org-name-address",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/suborganization",
|
||||
"domain/<int:domain_pk>/suborganization",
|
||||
views.DomainSubOrganizationView.as_view(),
|
||||
name="domain-suborganization",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/senior-official",
|
||||
"domain/<int:domain_pk>/senior-official",
|
||||
views.DomainSeniorOfficialView.as_view(),
|
||||
name="domain-senior-official",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/security-email",
|
||||
"domain/<int:domain_pk>/security-email",
|
||||
views.DomainSecurityEmailView.as_view(),
|
||||
name="domain-security-email",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/renewal",
|
||||
"domain/<int:domain_pk>/renewal",
|
||||
views.DomainRenewalView.as_view(),
|
||||
name="domain-renewal",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/users/add",
|
||||
"domain/<int:domain_pk>/users/add",
|
||||
views.DomainAddUserView.as_view(),
|
||||
name="domain-users-add",
|
||||
),
|
||||
|
@ -360,17 +364,17 @@ urlpatterns = [
|
|||
name="user-profile",
|
||||
),
|
||||
path(
|
||||
"invitation/<int:pk>/cancel",
|
||||
"invitation/<int:domain_invitation_pk>/cancel",
|
||||
views.DomainInvitationCancelView.as_view(http_method_names=["post"]),
|
||||
name="invitation-cancel",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>/delete",
|
||||
"domain-request/<int:domain_request_pk>/delete",
|
||||
views.DomainRequestDeleteView.as_view(http_method_names=["post"]),
|
||||
name="domain-request-delete",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/users/<int:user_pk>/delete",
|
||||
"domain/<int:domain_pk>/users/<int:user_pk>/delete",
|
||||
views.DomainDeleteUserView.as_view(http_method_names=["post"]),
|
||||
name="domain-user-delete",
|
||||
),
|
||||
|
@ -392,6 +396,7 @@ urlpatterns = [
|
|||
# This way, we can share a view for djangooidc, and other pages as we see fit.
|
||||
handler500 = "registrar.views.utility.error_views.custom_500_error_view"
|
||||
handler403 = "registrar.views.utility.error_views.custom_403_error_view"
|
||||
handler404 = "registrar.views.utility.error_views.custom_404_error_view"
|
||||
|
||||
# we normally would guard these with `if settings.DEBUG` but tests run with
|
||||
# DEBUG = False even when these apps have been loaded because settings.DEBUG
|
||||
|
|
300
src/registrar/decorators.py
Normal file
300
src/registrar/decorators.py
Normal file
|
@ -0,0 +1,300 @@
|
|||
import functools
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.decorators import method_decorator
|
||||
from registrar.models import Domain, DomainInformation, DomainInvitation, DomainRequest, UserDomainRole
|
||||
|
||||
# Constants for clarity
|
||||
ALL = "all"
|
||||
IS_STAFF = "is_staff"
|
||||
IS_DOMAIN_MANAGER = "is_domain_manager"
|
||||
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
||||
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
||||
IS_PORTFOLIO_MEMBER = "is_portfolio_member"
|
||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER = "is_portfolio_member_and_domain_manager"
|
||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER = "is_domain_manager_and_not_portfolio_member"
|
||||
HAS_PORTFOLIO_DOMAINS_ANY_PERM = "has_portfolio_domains_any_perm"
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM = "has_portfolio_domain_requests_any_perm"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL = "has_portfolio_domain_requests_view_all"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT = "has_portfolio_domain_requests_edit"
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM = "has_portfolio_members_any_perm"
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT = "has_portfolio_members_edit"
|
||||
HAS_PORTFOLIO_MEMBERS_VIEW = "has_portfolio_members_view"
|
||||
|
||||
|
||||
def grant_access(*rules):
|
||||
"""
|
||||
A decorator that enforces access control based on specified rules.
|
||||
|
||||
Usage:
|
||||
- Multiple rules in a single decorator:
|
||||
@grant_access(IS_STAFF, IS_SUPERUSER, IS_DOMAIN_MANAGER)
|
||||
|
||||
- Stacked decorators for separate rules:
|
||||
@grant_access(IS_SUPERUSER)
|
||||
@grant_access(IS_DOMAIN_MANAGER)
|
||||
|
||||
The decorator supports both function-based views (FBVs) and class-based views (CBVs).
|
||||
"""
|
||||
|
||||
def decorator(view):
|
||||
if isinstance(view, type): # Check if decorating a class-based view (CBV)
|
||||
original_dispatch = view.dispatch # Store the original dispatch method
|
||||
|
||||
@method_decorator(grant_access(*rules)) # Apply the decorator to dispatch
|
||||
def wrapped_dispatch(self, request, *args, **kwargs):
|
||||
if not _user_has_permission(request.user, request, rules, **kwargs):
|
||||
raise PermissionDenied # Deny access if the user lacks permission
|
||||
return original_dispatch(self, request, *args, **kwargs)
|
||||
|
||||
view.dispatch = wrapped_dispatch # Replace the dispatch method
|
||||
return view
|
||||
|
||||
else: # If decorating a function-based view (FBV)
|
||||
view.has_explicit_access = True # Mark the view as having explicit access control
|
||||
existing_rules = getattr(view, "_access_rules", set()) # Retrieve existing rules
|
||||
existing_rules.update(rules) # Merge with new rules
|
||||
view._access_rules = existing_rules # Store updated rules
|
||||
|
||||
@functools.wraps(view)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
if not _user_has_permission(request.user, request, rules, **kwargs):
|
||||
raise PermissionDenied # Deny access if the user lacks permission
|
||||
return view(request, *args, **kwargs) # Proceed with the original view
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _user_has_permission(user, request, rules, **kwargs):
|
||||
"""
|
||||
Determines if the user meets the required permission rules.
|
||||
|
||||
This function evaluates a set of predefined permission rules to check whether a user has access
|
||||
to a specific view. It supports various access control conditions, including staff status,
|
||||
domain management roles, and portfolio-related permissions.
|
||||
|
||||
Parameters:
|
||||
- user: The user requesting access.
|
||||
- request: The HTTP request object.
|
||||
- rules: A set of access control rules to evaluate.
|
||||
- **kwargs: Additional keyword arguments used in specific permission checks.
|
||||
|
||||
Returns:
|
||||
- True if the user satisfies any of the specified rules.
|
||||
- False otherwise.
|
||||
"""
|
||||
|
||||
# Skip authentication if @login_not_required is applied
|
||||
if getattr(request, "login_not_required", False):
|
||||
return True
|
||||
|
||||
# Allow everyone if `ALL` is in rules
|
||||
if ALL in rules:
|
||||
return True
|
||||
|
||||
# Ensure user is authenticated and not restricted
|
||||
if not user.is_authenticated or user.is_restricted():
|
||||
return False
|
||||
|
||||
# Define permission checks
|
||||
permission_checks = [
|
||||
(IS_STAFF, lambda: user.is_staff),
|
||||
(IS_DOMAIN_MANAGER, lambda: _is_domain_manager(user, **kwargs)),
|
||||
(IS_STAFF_MANAGING_DOMAIN, lambda: _is_staff_managing_domain(request, **kwargs)),
|
||||
(IS_PORTFOLIO_MEMBER, lambda: user.is_org_user(request)),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
|
||||
lambda: _has_portfolio_view_all_domains(request, kwargs.get("domain_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAINS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_domains_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
(
|
||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
|
||||
lambda: _is_domain_manager(user, **kwargs) and _is_portfolio_member(request),
|
||||
),
|
||||
(
|
||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
|
||||
lambda: _is_domain_manager(user, **kwargs) and not _is_portfolio_member(request),
|
||||
),
|
||||
(
|
||||
IS_DOMAIN_REQUEST_CREATOR,
|
||||
lambda: _is_domain_request_creator(user, kwargs.get("domain_request_pk"))
|
||||
and not _is_portfolio_member(request),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_requests_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_all_domain_requests_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||
lambda: _has_portfolio_domain_requests_edit(user, request, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and (
|
||||
user.has_view_members_portfolio_permission(request.session.get("portfolio"))
|
||||
or user.has_edit_members_portfolio_permission(request.session.get("portfolio"))
|
||||
),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_edit_members_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_VIEW,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_members_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
]
|
||||
|
||||
# Check conditions iteratively
|
||||
return any(check() for rule, check in permission_checks if rule in rules)
|
||||
|
||||
|
||||
def _has_portfolio_domain_requests_edit(user, request, domain_request_id):
|
||||
if domain_request_id and not _is_domain_request_creator(user, domain_request_id):
|
||||
return False
|
||||
return user.is_org_user(request) and user.has_edit_request_portfolio_permission(request.session.get("portfolio"))
|
||||
|
||||
|
||||
def _is_domain_manager(user, **kwargs):
|
||||
"""
|
||||
Determines if the given user is a domain manager for a specified domain.
|
||||
|
||||
- First, it checks if 'domain_pk' is present in the URL parameters.
|
||||
- If 'domain_pk' exists, it verifies if the user has a domain role for that domain.
|
||||
- If 'domain_pk' is absent, it checks for 'domain_invitation_pk' to determine if the user
|
||||
has domain permissions through an invitation.
|
||||
|
||||
Returns:
|
||||
bool: True if the user is a domain manager, False otherwise.
|
||||
"""
|
||||
domain_id = kwargs.get("domain_pk")
|
||||
if domain_id:
|
||||
return UserDomainRole.objects.filter(user=user, domain_id=domain_id).exists()
|
||||
domain_invitation_id = kwargs.get("domain_invitation_pk")
|
||||
if domain_invitation_id:
|
||||
return DomainInvitation.objects.filter(id=domain_invitation_id, domain__permissions__user=user).exists()
|
||||
return False
|
||||
|
||||
|
||||
def _is_domain_request_creator(user, domain_request_pk):
|
||||
"""Checks to see if the user is the creator of a domain request
|
||||
with domain_request_pk."""
|
||||
if domain_request_pk:
|
||||
return DomainRequest.objects.filter(creator=user, id=domain_request_pk).exists()
|
||||
return True
|
||||
|
||||
|
||||
def _is_portfolio_member(request):
|
||||
"""Checks to see if the user in the request is a member of the
|
||||
portfolio in the request's session."""
|
||||
return request.user.is_org_user(request)
|
||||
|
||||
|
||||
def _is_staff_managing_domain(request, **kwargs):
|
||||
"""
|
||||
Determines whether a staff user (analyst or superuser) has permission to manage a domain
|
||||
that they did not create or were not invited to.
|
||||
|
||||
The function enforces:
|
||||
1. **User Authorization** - The user must have `analyst_access_permission` or `full_access_permission`.
|
||||
2. **Valid Session Context** - The user must have explicitly selected the domain for management
|
||||
via an 'analyst action' (e.g., by clicking 'Manage Domain' in the admin interface).
|
||||
3. **Domain Status Check** - Only domains in specific statuses (e.g., APPROVED, IN_REVIEW, etc.)
|
||||
can be managed, except in cases where the domain lacks a status due to errors.
|
||||
|
||||
Process:
|
||||
- First, the function retrieves the `domain_pk` from the URL parameters.
|
||||
- If `domain_pk` is not provided, it attempts to resolve the domain via `domain_invitation_pk`.
|
||||
- It checks if the user has the required permissions.
|
||||
- It verifies that the user has an active 'analyst action' session for the domain.
|
||||
- Finally, it ensures that the domain is in a status that allows management.
|
||||
|
||||
Returns:
|
||||
bool: True if the user is allowed to manage the domain, False otherwise.
|
||||
"""
|
||||
|
||||
domain_id = kwargs.get("domain_pk")
|
||||
if not domain_id:
|
||||
domain_invitation_id = kwargs.get("domain_invitation_pk")
|
||||
domain_invitation = DomainInvitation.objects.filter(id=domain_invitation_id).first()
|
||||
if domain_invitation:
|
||||
domain_id = domain_invitation.domain_id
|
||||
|
||||
# Check if the request user is permissioned...
|
||||
user_is_analyst_or_superuser = request.user.has_perm(
|
||||
"registrar.analyst_access_permission"
|
||||
) or request.user.has_perm("registrar.full_access_permission")
|
||||
|
||||
if not user_is_analyst_or_superuser:
|
||||
return False
|
||||
|
||||
# Check if the user is attempting a valid edit action.
|
||||
# In other words, if the analyst/admin did not click
|
||||
# the 'Manage Domain' button in /admin,
|
||||
# then they cannot access this page.
|
||||
session = request.session
|
||||
can_do_action = (
|
||||
"analyst_action" in session
|
||||
and "analyst_action_location" in session
|
||||
and session["analyst_action_location"] == domain_id
|
||||
)
|
||||
|
||||
if not can_do_action:
|
||||
return False
|
||||
|
||||
# Analysts may manage domains, when they are in these statuses:
|
||||
valid_domain_statuses = [
|
||||
DomainRequest.DomainRequestStatus.APPROVED,
|
||||
DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
# Edge case - some domains do not have
|
||||
# a status or DomainInformation... aka a status of 'None'.
|
||||
# It is necessary to access those to correct errors.
|
||||
None,
|
||||
]
|
||||
|
||||
requested_domain = DomainInformation.objects.filter(domain_id=domain_id).first()
|
||||
|
||||
# if no domain information or domain request exist, the user
|
||||
# should be able to manage the domain; however, if domain information
|
||||
# and domain request exist, and domain request is not in valid status,
|
||||
# user should not be able to manage domain
|
||||
if (
|
||||
requested_domain
|
||||
and requested_domain.domain_request
|
||||
and requested_domain.domain_request.status not in valid_domain_statuses
|
||||
):
|
||||
return False
|
||||
|
||||
# Valid session keys exist,
|
||||
# the user is permissioned,
|
||||
# and it is in a valid status
|
||||
return True
|
||||
|
||||
|
||||
def _has_portfolio_view_all_domains(request, domain_pk):
|
||||
"""Returns whether the user in the request can access the domain
|
||||
via portfolio view all domains permission."""
|
||||
portfolio = request.session.get("portfolio")
|
||||
if request.user.has_view_all_domains_portfolio_permission(portfolio):
|
||||
if Domain.objects.filter(id=domain_pk).exists():
|
||||
domain = Domain.objects.get(id=domain_pk)
|
||||
if domain.domain_info.portfolio == portfolio:
|
||||
return True
|
||||
return False
|
|
@ -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)}
|
||||
|
|
|
@ -2,6 +2,7 @@ from itertools import zip_longest
|
|||
import logging
|
||||
import ipaddress
|
||||
import re
|
||||
import time
|
||||
from datetime import date, timedelta
|
||||
from typing import Optional
|
||||
from django.db import transaction
|
||||
|
@ -750,11 +751,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
|
||||
successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount
|
||||
|
||||
try:
|
||||
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
||||
except Exception as e:
|
||||
# we don't need this part to succeed in order to continue.
|
||||
logger.error("Failed to delete nameserver hosts: %s", e)
|
||||
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
||||
|
||||
if successTotalNameservers < 2:
|
||||
try:
|
||||
|
@ -1038,13 +1035,13 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
logger.error(f"registry error removing client hold: {err}")
|
||||
raise (err)
|
||||
|
||||
def _delete_domain(self):
|
||||
def _delete_domain(self): # noqa
|
||||
"""This domain should be deleted from the registry
|
||||
may raises RegistryError, should be caught or handled correctly by caller"""
|
||||
|
||||
logger.info("Deleting subdomains for %s", self.name)
|
||||
# check if any subdomains are in use by another domain
|
||||
hosts = Host.objects.filter(name__regex=r".+{}".format(self.name))
|
||||
hosts = Host.objects.filter(name__regex=r".+\.{}".format(self.name))
|
||||
for host in hosts:
|
||||
if host.domain != self:
|
||||
logger.error("Unable to delete host: %s is in use by another domain: %s", host.name, host.domain)
|
||||
|
@ -1052,38 +1049,119 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION,
|
||||
note=f"Host {host.name} is in use by {host.domain}",
|
||||
)
|
||||
try:
|
||||
# set hosts to empty list so nameservers are deleted
|
||||
(
|
||||
deleted_values,
|
||||
updated_values,
|
||||
new_values,
|
||||
oldNameservers,
|
||||
) = self.getNameserverChanges(hosts=[])
|
||||
|
||||
(
|
||||
deleted_values,
|
||||
updated_values,
|
||||
new_values,
|
||||
oldNameservers,
|
||||
) = self.getNameserverChanges(hosts=[])
|
||||
|
||||
_ = self._update_host_values(updated_values, oldNameservers) # returns nothing, just need to be run and errors
|
||||
addToDomainList, _ = self.createNewHostList(new_values)
|
||||
deleteHostList, _ = self.createDeleteHostList(deleted_values)
|
||||
responseCode = self.addAndRemoveHostsFromDomain(hostsToAdd=addToDomainList, hostsToDelete=deleteHostList)
|
||||
|
||||
# update the hosts
|
||||
_ = self._update_host_values(
|
||||
updated_values, oldNameservers
|
||||
) # returns nothing, just need to be run and errors
|
||||
addToDomainList, _ = self.createNewHostList(new_values)
|
||||
deleteHostList, _ = self.createDeleteHostList(deleted_values)
|
||||
responseCode = self.addAndRemoveHostsFromDomain(hostsToAdd=addToDomainList, hostsToDelete=deleteHostList)
|
||||
except RegistryError as e:
|
||||
logger.error(f"Error trying to delete hosts from domain {self}: {e}")
|
||||
raise e
|
||||
# if unable to update domain raise error and stop
|
||||
if responseCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY:
|
||||
raise NameserverError(code=nsErrorCodes.BAD_DATA)
|
||||
|
||||
logger.info("Finished removing nameservers from domain")
|
||||
|
||||
# addAndRemoveHostsFromDomain removes the hosts from the domain object,
|
||||
# but we still need to delete the object themselves
|
||||
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
||||
logger.info("Finished _delete_hosts_if_not_used inside _delete_domain()")
|
||||
|
||||
# delete the non-registrant contacts
|
||||
logger.debug("Deleting non-registrant contacts for %s", self.name)
|
||||
contacts = PublicContact.objects.filter(domain=self)
|
||||
for contact in contacts:
|
||||
if contact.contact_type != PublicContact.ContactTypeChoices.REGISTRANT:
|
||||
self._update_domain_with_contact(contact, rem=True)
|
||||
request = commands.DeleteContact(contact.registry_id)
|
||||
registry.send(request, cleaned=True)
|
||||
logger.info(f"retrieved contacts for domain: {contacts}")
|
||||
|
||||
logger.info("Deleting domain %s", self.name)
|
||||
for contact in contacts:
|
||||
try:
|
||||
if contact.contact_type != PublicContact.ContactTypeChoices.REGISTRANT:
|
||||
logger.info(f"Deleting contact: {contact}")
|
||||
try:
|
||||
self._update_domain_with_contact(contact, rem=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Error while updating domain with contact: {contact}, e: {e}", exc_info=True)
|
||||
request = commands.DeleteContact(contact.registry_id)
|
||||
registry.send(request, cleaned=True)
|
||||
logger.info(f"sent DeleteContact for {contact}")
|
||||
except RegistryError as e:
|
||||
logger.error(f"Error deleting contact: {contact}, {e}", exc_info=True)
|
||||
|
||||
logger.info(f"Finished deleting contacts for {self.name}")
|
||||
|
||||
# delete ds data if it exists
|
||||
if self.dnssecdata:
|
||||
logger.debug("Deleting ds data for %s", self.name)
|
||||
try:
|
||||
# set and unset client hold to be able to change ds data
|
||||
logger.info("removing client hold")
|
||||
self._remove_client_hold()
|
||||
self.dnssecdata = None
|
||||
logger.info("placing client hold")
|
||||
self._place_client_hold()
|
||||
except RegistryError as e:
|
||||
logger.error("Error deleting ds data for %s: %s", self.name, e)
|
||||
e.note = "Error deleting ds data for %s" % self.name
|
||||
raise e
|
||||
|
||||
# check if the domain can be deleted
|
||||
if not self._domain_can_be_deleted():
|
||||
note = "Domain has associated objects that prevent deletion."
|
||||
raise RegistryError(code=ErrorCode.COMMAND_FAILED, note=note)
|
||||
|
||||
# delete the domain
|
||||
request = commands.DeleteDomain(name=self.name)
|
||||
registry.send(request, cleaned=True)
|
||||
try:
|
||||
registry.send(request, cleaned=True)
|
||||
logger.info("Domain %s deleted successfully.", self.name)
|
||||
except RegistryError as e:
|
||||
logger.error("Error deleting domain %s: %s", self.name, e)
|
||||
raise e
|
||||
|
||||
def _domain_can_be_deleted(self, max_attempts=5, wait_interval=2) -> bool:
|
||||
"""
|
||||
Polls the registry using InfoDomain calls to confirm that the domain can be deleted.
|
||||
Returns True if the domain can be deleted, False otherwise. Includes a retry mechanism
|
||||
using wait_interval and max_attempts, which may be necessary if subdomains and other
|
||||
associated objects were only recently deleted as the registry may not be immediately updated.
|
||||
"""
|
||||
logger.info("Polling registry to confirm deletion pre-conditions for %s", self.name)
|
||||
last_info_error = None
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
info_response = registry.send(commands.InfoDomain(name=self.name), cleaned=True)
|
||||
domain_info = info_response.res_data[0]
|
||||
hosts_associated = getattr(domain_info, "hosts", None)
|
||||
if hosts_associated is None or len(hosts_associated) == 0:
|
||||
logger.info("InfoDomain reports no associated hosts for %s. Proceeding with deletion.", self.name)
|
||||
return True
|
||||
else:
|
||||
logger.info("Attempt %d: Domain %s still has hosts: %s", attempt + 1, self.name, hosts_associated)
|
||||
except RegistryError as info_e:
|
||||
# If the domain is already gone, we can assume deletion already occurred.
|
||||
if info_e.code == ErrorCode.OBJECT_DOES_NOT_EXIST:
|
||||
logger.info("InfoDomain check indicates domain %s no longer exists.", self.name)
|
||||
raise info_e
|
||||
logger.warning("Attempt %d: Error during InfoDomain check: %s", attempt + 1, info_e)
|
||||
time.sleep(wait_interval)
|
||||
else:
|
||||
logger.error(
|
||||
"Exceeded max attempts waiting for domain %s to clear associated objects; last error: %s",
|
||||
self.name,
|
||||
last_info_error,
|
||||
)
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
@ -1840,8 +1918,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
else:
|
||||
logger.error("Error _delete_hosts_if_not_used, code was %s error was %s" % (e.code, e))
|
||||
|
||||
raise e
|
||||
|
||||
def _fix_unknown_state(self, cleaned):
|
||||
"""
|
||||
_fix_unknown_state: Calls _add_missing_contacts_if_unknown
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,9 +3,13 @@ Contains middleware used in settings.py
|
|||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from urllib.parse import parse_qs
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import resolve
|
||||
from registrar.models import User
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
|
@ -170,3 +174,51 @@ class CheckPortfolioMiddleware:
|
|||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
else:
|
||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
|
||||
|
||||
class RestrictAccessMiddleware:
|
||||
"""
|
||||
Middleware that blocks access to all views unless explicitly permitted.
|
||||
|
||||
This middleware enforces authentication by default. Views must explicitly allow access
|
||||
using access control mechanisms such as the `@grant_access` decorator. Exceptions are made
|
||||
for Django admin views, explicitly ignored paths, and views that opt out of login requirements.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
# Compile regex patterns from settings to identify paths that bypass login requirements
|
||||
self.ignored_paths = [re.compile(pattern) for pattern in getattr(settings, "LOGIN_REQUIRED_IGNORE_PATHS", [])]
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
# Allow requests to Django Debug Toolbar
|
||||
if request.path.startswith("/__debug__/"):
|
||||
return self.get_response(request)
|
||||
|
||||
# Allow requests matching configured ignored paths
|
||||
if any(pattern.match(request.path) for pattern in self.ignored_paths):
|
||||
return self.get_response(request)
|
||||
|
||||
# Attempt to resolve the request path to a view function
|
||||
try:
|
||||
resolver_match = resolve(request.path_info)
|
||||
view_func = resolver_match.func
|
||||
app_name = resolver_match.app_name # Get the app name of the resolved view
|
||||
except Exception:
|
||||
# If resolution fails, allow the request to proceed (avoid blocking non-view routes)
|
||||
return self.get_response(request)
|
||||
|
||||
# Automatically allow access to Django's built-in admin views (excluding custom /admin/* views)
|
||||
if app_name == "admin":
|
||||
return self.get_response(request)
|
||||
|
||||
# Allow access if the view explicitly opts out of login requirements
|
||||
if getattr(view_func, "login_required", True) is False:
|
||||
return self.get_response(request)
|
||||
|
||||
# Restrict access to views that do not explicitly declare access rules
|
||||
if not getattr(view_func, "has_explicit_access", False):
|
||||
raise PermissionDenied # Deny access if the view lacks explicit permission handling
|
||||
|
||||
return self.get_response(request)
|
||||
|
|
|
@ -4,24 +4,22 @@
|
|||
{% for app in app_list %}
|
||||
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}">
|
||||
<table>
|
||||
|
||||
{# .gov override: add headers #}
|
||||
{% if show_changelinks %}
|
||||
<colgroup span="3"></colgroup>
|
||||
{% else %}
|
||||
<colgroup span="2"></colgroup>
|
||||
{% endif %}
|
||||
{# .gov override: display the app name as a caption rather than a table header #}
|
||||
<caption class="text-bold">{{ app.name }}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
{% if show_changelinks %}
|
||||
<th colspan="3" class="primary-th" scope="colgroup">
|
||||
{{ app.name }}
|
||||
</th>
|
||||
{% else %}
|
||||
<th colspan="2" class="primary-th" scope="colgroup">
|
||||
{{ app.name }}
|
||||
</th>
|
||||
{% endif %}
|
||||
{# .gov override: hide headers #}
|
||||
{% comment %}
|
||||
{% if show_changelinks %}
|
||||
<th colspan="3" class="primary-th" scope="colgroup">
|
||||
{{ app.name }}
|
||||
</th>
|
||||
{% else %}
|
||||
<th colspan="2" class="primary-th" scope="colgroup">
|
||||
{{ app.name }}
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Model</th>
|
||||
|
@ -45,16 +43,17 @@
|
|||
{% endif %}
|
||||
|
||||
{% if model.add_url %}
|
||||
<td><a href="{{ model.add_url }}" class="addlink">{% translate 'Add' %}</a></td>
|
||||
{% comment %} Remove the 's' from the end of the string to avoid text like "Add domain requests" {% endcomment %}
|
||||
<td><a href="{{ model.add_url }}" class="addlink" aria-label="Add {{ model.name|slice:":-1" }}">{% translate 'Add' %}</a></td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{% if model.admin_url and show_changelinks %}
|
||||
{% if model.view_only %}
|
||||
<td><a href="{{ model.admin_url }}" class="viewlink">{% translate 'View' %}</a></td>
|
||||
<td><a href="{{ model.admin_url }}" class="viewlink" aria-label="View {{ model.name }}">{% translate 'View' %}</a></td>
|
||||
{% else %}
|
||||
<td><a href="{{ model.admin_url }}" class="changelink">{% translate 'Change' %}</a></td>
|
||||
<td><a href="{{ model.admin_url }}" class="changelink" aria-label="Change {{ model.name }}">{% translate 'Change' %}</a></td>
|
||||
{% endif %}
|
||||
{% elif show_changelinks %}
|
||||
<td></td>
|
||||
|
@ -64,9 +63,20 @@
|
|||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="module module--custom">
|
||||
<h2>Analytics</h2>
|
||||
<a class="display-block padding-y-1 padding-x-1" href="{% url 'analytics' %}">Dashboard</a>
|
||||
<div class="module">
|
||||
<table class="width-full">
|
||||
<caption class="text-bold">Analytics</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Reports</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><a href="{% url 'analytics' %}">Dashboard</a></th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||
|
|
|
@ -48,6 +48,10 @@
|
|||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if opts.model_name %}
|
||||
<a class="usa-skipnav" href="#changelist-filter" aria-label="Skip to the filters section">Skip to filters</a>
|
||||
{% endif %}
|
||||
|
||||
{# Djando update: this div will change to header #}
|
||||
<div id="header">
|
||||
<div id="branding">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n admin_urls static admin_list %}
|
||||
|
||||
{% block content_title %}
|
||||
<h1>{{ title }}</h1>
|
||||
|
@ -37,6 +38,7 @@
|
|||
for {{ search_query }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% comment %} Replace the Django ul markup with a div. We'll replace the li with a p in change_list_object_tools {% endcomment %}
|
||||
|
@ -46,4 +48,25 @@
|
|||
{{ block.super }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% comment %} Replace the Django header markup for clearing all filters with a div. {% endcomment %}
|
||||
{% block filters %}
|
||||
{% if cl.has_filters %}
|
||||
<nav id="changelist-filter" aria-labelledby="changelist-filter-header">
|
||||
<h2 id="changelist-filter-header">{% translate 'Filter' %}</h2>
|
||||
{% if cl.is_facets_optional or cl.has_active_filters %}<div id="changelist-filter-extra-actions">
|
||||
{% if cl.is_facets_optional %}<h3>
|
||||
{% if cl.add_facets %}<a href="{{ cl.remove_facet_link }}" class="hidelink">{% translate "Hide counts" %}</a>
|
||||
{% else %}<a href="{{ cl.add_facet_link }}" class="viewlink">{% translate "Show counts" %}</a>{% endif %}
|
||||
</h3>{% endif %}
|
||||
{% if cl.has_active_filters %}<div class="margin-2">
|
||||
<a href="{{ cl.clear_all_filters_qs }}" role="link">✖ {% translate "Clear all filters" %}</a>
|
||||
</div>{% endif %}
|
||||
</div>{% endif %}
|
||||
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,37 +1,41 @@
|
|||
{% load i18n %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
<details data-filter-title="{{ title }}" open="">
|
||||
<summary aria-label="Show/hide {{ title }} filters" role="button">
|
||||
{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}
|
||||
</summary>
|
||||
<ul class="mulitple-choice">
|
||||
{% for choice in choices %}
|
||||
{% if choice.reset %}
|
||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
|
||||
<ul class="mulitple-choice">
|
||||
{% for choice in choices %}
|
||||
{% if choice.reset %}
|
||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for choice in choices %}
|
||||
{% if not choice.reset %}
|
||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||
{% if choice.selected and choice.exclude_query_string %}
|
||||
<a class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
|
||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||
</svg>
|
||||
<svg class="usa-icon position-absolute z-100 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not choice.selected and choice.include_query_string %}
|
||||
<a class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
|
||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% for choice in choices %}
|
||||
{% if not choice.reset %}
|
||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||
{% if choice.selected and choice.exclude_query_string %}
|
||||
<a role="menuitemcheckbox" class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
|
||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||
</svg>
|
||||
<svg class="usa-icon position-absolute z-100 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not choice.selected and choice.include_query_string %}
|
||||
<a role="menuitemcheckbox" class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
|
||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "admin/delete_confirmation.html" %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you cancel the portfolio invitation here, it won't trigger any emails. It also won't remove the user's
|
||||
portfolio access if they already logged in. Go to the
|
||||
<a href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
||||
User Portfolio Permissions
|
||||
</a>
|
||||
table if you want to remove the user from a portfolio.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "admin/delete_confirmation.html" %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you remove someone from a portfolio here, it will not send any emails when you click "Save".
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-users' pk=domain.id %}" class="usa-breadcrumb__link"><span>Domain managers</span></a>
|
||||
<a href="{% url 'domain-users' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>Domain managers</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Add a domain manager</span>
|
||||
|
@ -27,7 +27,7 @@
|
|||
</ol>
|
||||
</nav>
|
||||
{% else %}
|
||||
{% url 'domain-users' pk=domain.id as url %}
|
||||
{% url 'domain-users' domain_pk=domain.id as url %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain manager breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
|
@ -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 %}
|
||||
|
|
|
@ -48,11 +48,11 @@
|
|||
<p class="margin-y-0 text-primary-darker">
|
||||
{% if domain.is_expired and is_domain_manager %}
|
||||
This domain has expired, but it is still online.
|
||||
{% url 'domain-renewal' pk=domain.id as url %}
|
||||
{% url 'domain-renewal' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}" class="usa-link">Renew to maintain access.</a>
|
||||
{% elif domain.is_expiring and is_domain_manager %}
|
||||
This domain will expire soon.
|
||||
{% url 'domain-renewal' pk=domain.id as url %}
|
||||
{% url 'domain-renewal' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}" class="usa-link">Renew to maintain access.</a>
|
||||
{% elif domain.is_expiring and is_portfolio_user %}
|
||||
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
|
||||
|
@ -82,7 +82,7 @@
|
|||
{% endif %}
|
||||
|
||||
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
{% url 'domain-dns-nameservers' domain_pk=domain.id as url %}
|
||||
{% if domain.nameservers|length > 0 %}
|
||||
{% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
|
@ -95,7 +95,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec' domain_pk=domain.id as url %}
|
||||
{% if domain.dnssecdata is not None %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
|
@ -104,26 +104,26 @@
|
|||
|
||||
{% if portfolio %}
|
||||
{% if has_any_domains_portfolio_permission and has_edit_portfolio_permission %}
|
||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
||||
{% url 'domain-suborganization' domain_pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_portfolio_permission %}
|
||||
{% elif has_any_domains_portfolio_permission and has_view_portfolio_permission %}
|
||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
||||
{% url 'domain-suborganization' domain_pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_view_portfolio_permission view_button=True %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% url 'domain-org-name-address' pk=domain.id as url %}
|
||||
{% url 'domain-org-name-address' domain_pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
||||
{% url 'domain-senior-official' pk=domain.id as url %}
|
||||
{% url 'domain-senior-official' domain_pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
{% url 'domain-security-email' domain_pk=domain.id as url %}
|
||||
{% if security_email is not None and security_email not in hidden_security_emails%}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
{% url 'domain-users' pk=domain.id as url %}
|
||||
{% url 'domain-users' domain_pk=domain.id as url %}
|
||||
{% if portfolio %}
|
||||
{% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>DNS</span>
|
||||
|
@ -30,14 +30,14 @@
|
|||
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
|
||||
|
||||
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
{% url 'domain-dns-nameservers' domain_pk=domain.id as url %}
|
||||
<ul class="usa-list">
|
||||
<li><a href="{{ url }}">Name servers</a></li>
|
||||
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec' domain_pk=domain.id as url %}
|
||||
<li><a href="{{ url }}">DNSSEC</a></li>
|
||||
{% if dns_prototype_flag and is_valid_domain %}
|
||||
<li><a href="{% url 'prototype-domain-dns' pk=domain.id %}">Prototype DNS record creator</a></li>
|
||||
<li><a href="{% url 'prototype-domain-dns' domain_pk=domain.id %}">Prototype DNS record creator</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -14,10 +14,10 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-dns' pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
<a href="{% url 'domain-dns' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>DNSSEC</span>
|
||||
|
@ -69,7 +69,7 @@
|
|||
<p class="margin-y-0">It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'domain-dns-dnssec-dsdata' pk=domain.id %}" class="usa-button">Enable DNSSEC</a>
|
||||
<a href="{% url 'domain-dns-dnssec-dsdata' domain_pk=domain.id %}" class="usa-button">Enable DNSSEC</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-dns' pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
<a href="{% url 'domain-dns' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-dns-dnssec' pk=domain.id %}" class="usa-breadcrumb__link"><span>DNSSEC</span></a>
|
||||
<a href="{% url 'domain-dns-dnssec' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>DNSSEC</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>DS data</span>
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-dns' pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
<a href="{% url 'domain-dns' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>DNS name servers</span>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{domain.name}}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{domain.name}}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Renewal Form</span>
|
||||
|
@ -63,14 +63,14 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
{% url 'domain-security-email' domain_pk=domain.id as url %}
|
||||
{% if security_email is not None and security_email not in hidden_security_emails%}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email custom_text_for_value_none='We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain.' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Security email' value='None provided' custom_text_for_value_none='We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain.' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-users' pk=domain.id as url %}
|
||||
{% url 'domain-users' domain_pk=domain.id as url %}
|
||||
{% if portfolio %}
|
||||
{% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
|
@ -91,7 +91,7 @@
|
|||
Acknowledgement of .gov domain requirements </h3>
|
||||
</legend>
|
||||
|
||||
<form method="post" action="{% url 'domain-renewal' pk=domain.id %}">
|
||||
<form method="post" action="{% url 'domain-renewal' domain_pk=domain.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="usa-checkbox">
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
{% endwith %}
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--with-icon usa-button--unstyled">
|
||||
<button id="submit-domain-request--site-button" type="submit" name="submit_button" value="save" class="usa-button usa-button--with-icon usa-button--unstyled">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add another site</span>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</ol>
|
||||
</nav>
|
||||
{% elif steps.prev %}
|
||||
<a href="{% namespaced_url 'domain-request' steps.prev id=domain_request_id %}" class="breadcrumb__back">
|
||||
<a href="{% namespaced_url 'domain-request' steps.prev domain_request_pk=domain_request_id %}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||
</svg><span class="margin-left-05">Previous step</span>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</svg>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% namespaced_url 'domain-request' this_step id=domain_request_id %}"
|
||||
<a href="{% namespaced_url 'domain-request' this_step domain_request_pk=domain_request_id %}"
|
||||
{% if this_step == steps.current %}
|
||||
class="usa-current"
|
||||
{% else %}
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
<p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
|
||||
|
||||
<p><a href="{% url 'domain-request-withdrawn' DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
|
||||
<a href="{% url 'domain-request-status' DomainRequest.id %}">Cancel</a></p>
|
||||
<p><a href="{% url 'domain-request-withdrawn' domain_request_pk=DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
|
||||
<a href="{% url 'domain-request-status' domain_request_pk=DomainRequest.id %}">Cancel</a></p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Security email</span>
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
{% endif %}
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns' pk=domain.id as url %}
|
||||
{% url 'domain-dns' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}" {% if request.path|startswith:url %}class="usa-current"{% endif %}">
|
||||
DNS
|
||||
</a>
|
||||
{% if request.path|startswith:url %}
|
||||
<ul class="usa-sidenav__sublist">
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
{% url 'domain-dns-nameservers' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
|
@ -33,7 +33,7 @@
|
|||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path|startswith:url %}class="usa-current"{% endif %}
|
||||
>
|
||||
|
@ -43,7 +43,7 @@
|
|||
{% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'dsdata' %}
|
||||
<ul class="usa-sidenav__sublist">
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec-dsdata' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Suborganization</span>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Domain managers</span>
|
||||
|
@ -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">
|
||||
|
@ -93,7 +89,7 @@
|
|||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove myself" modal_button_class="usa-button--secondary" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}" >
|
||||
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" domain_pk=domain.id user_pk=item.permission.user.id %}" >
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
{% else %}
|
||||
|
@ -108,7 +104,7 @@
|
|||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove domain manager" modal_button_class="usa-button--secondary" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
|
||||
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" domain_pk=domain.id user_pk=item.permission.user.id %}">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
{% endif %}
|
||||
|
@ -123,7 +119,7 @@
|
|||
></div>
|
||||
{% endif %}
|
||||
|
||||
<a class="usa-button usa-button--unstyled usa-button--with-icon" href="{% url 'domain-users-add' pk=domain.id %}">
|
||||
<a class="usa-button usa-button--unstyled usa-button--with-icon" href="{% url 'domain-users-add' domain_pk=domain.id %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add a domain manager</span>
|
||||
|
@ -154,7 +150,7 @@
|
|||
{% if not portfolio %}<td data-label="Status">{{ invitation.domain_invitation.status|title }}</td>{% endif %}
|
||||
<td>
|
||||
{% if invitation.domain_invitation.status == invitation.domain_invitation.DomainInvitationStatus.INVITED %}
|
||||
<form method="POST" action="{% url "invitation-cancel" pk=invitation.domain_invitation.id %}">
|
||||
<form method="POST" action="{% url "invitation-cancel" domain_invitation_pk=invitation.domain_invitation.id %}">
|
||||
{% csrf_token %}<input type="submit" class="usa-button--unstyled text-no-underline cursor-pointer" value="Cancel">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
|
|
@ -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
|
||||
|
|
21
src/registrar/templates/emails/portfolio_removal.txt
Normal file
21
src/registrar/templates/emails/portfolio_removal.txt
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %}
|
||||
|
||||
{{ requestor_email }} has removed you from {{ portfolio.organization_name }}.
|
||||
|
||||
You can no longer view this organization or its related domains within the .gov registrar.
|
||||
|
||||
|
||||
SOMETHING WRONG?
|
||||
If you have questions or concerns, reach out to the person who removed you from the
|
||||
organization, or reply to this email.
|
||||
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
You've been removed from a .gov organization
|
|
@ -1,6 +1,6 @@
|
|||
<li class="usa-sidenav__item">
|
||||
{% if url_name %}
|
||||
{% url url_name pk=domain.id as url %}
|
||||
{% url url_name domain_pk=domain.id as url %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<caption class="sr-only">member domains</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortable="checked" scope="col" role="columnheader" class="padding-right-105 width-6"><span class="sr-only">Assigned domains</span></th>
|
||||
<th data-sortable="checked" scope="col" role="columnheader" class="padding-right-105 width-6 left-align-sort-button"><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>
|
||||
|
@ -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>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% for step in steps %}
|
||||
<section class="summary-item margin-top-3">
|
||||
{% if is_editable %}
|
||||
{% namespaced_url 'domain-request' step id=domain_request_id as domain_request_url %}
|
||||
{% namespaced_url 'domain-request' step domain_request_pk=domain_request_id as domain_request_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.REQUESTING_ENTITY %}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% for step in steps %}
|
||||
<section class="summary-item margin-top-3">
|
||||
{% if is_editable %}
|
||||
{% namespaced_url 'domain-request' step id=domain_request_id as domain_request_url %}
|
||||
{% namespaced_url 'domain-request' step domain_request_pk=domain_request_id as domain_request_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
|
||||
{% block modify_request %}
|
||||
{% if DomainRequest.is_withdrawable %}
|
||||
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
<p><a href="{% url 'domain-request-withdraw-confirmation' domain_request_pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
Withdraw request</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -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()"
|
||||
>
|
||||
|
|
|
@ -55,6 +55,7 @@ from .common import (
|
|||
MockDbForSharedTests,
|
||||
AuditedAdminMockData,
|
||||
completed_domain_request,
|
||||
create_test_user,
|
||||
generic_domain_object,
|
||||
less_console_noise,
|
||||
mock_user,
|
||||
|
@ -1135,6 +1136,7 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
|||
"""Create a client object"""
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.superuser = create_superuser()
|
||||
self.testuser = create_test_user()
|
||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -1167,6 +1169,21 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
|||
"If you add someone to a portfolio here, it will not trigger an invitation email.",
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_delete_confirmation_page_contains_static_message(self):
|
||||
"""Ensure the custom message appears in the delete confirmation page."""
|
||||
self.client.force_login(self.superuser)
|
||||
# Create a test portfolio permission
|
||||
self.permission = UserPortfolioPermission.objects.create(
|
||||
user=self.testuser, portfolio=self.portfolio, roles=["organization_member"]
|
||||
)
|
||||
delete_url = reverse("admin:registrar_userportfoliopermission_delete", args=[self.permission.pk])
|
||||
response = self.client.get(delete_url)
|
||||
|
||||
# Check if the response contains the expected static message
|
||||
expected_message = "If you remove someone from a portfolio here, it will not send any emails"
|
||||
self.assertIn(expected_message, response.content.decode("utf-8"))
|
||||
|
||||
|
||||
class TestPortfolioInvitationAdmin(TestCase):
|
||||
"""Tests for the PortfolioInvitationAdmin class as super user
|
||||
|
@ -1605,6 +1622,21 @@ class TestPortfolioInvitationAdmin(TestCase):
|
|||
request, "Could not send email notification to existing organization admins."
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_delete_confirmation_page_contains_static_message(self):
|
||||
"""Ensure the custom message appears in the delete confirmation page."""
|
||||
self.client.force_login(self.superuser)
|
||||
# Create a test portfolio invitation
|
||||
self.invitation = PortfolioInvitation.objects.create(
|
||||
email="testuser@example.com", portfolio=self.portfolio, roles=["organization_member"]
|
||||
)
|
||||
delete_url = reverse("admin:registrar_portfolioinvitation_delete", args=[self.invitation.pk])
|
||||
response = self.client.get(delete_url)
|
||||
|
||||
# Check if the response contains the expected static message
|
||||
expected_message = "If you cancel the portfolio invitation here"
|
||||
self.assertIn(expected_message, response.content.decode("utf-8"))
|
||||
|
||||
|
||||
class TestHostAdmin(TestCase):
|
||||
"""Tests for the HostAdmin class as super user
|
||||
|
@ -3816,7 +3848,7 @@ class TestTransferUser(WebTest):
|
|||
with self.assertRaises(User.DoesNotExist):
|
||||
self.user2.refresh_from_db()
|
||||
|
||||
# @less_console_noise_decorator
|
||||
@less_console_noise_decorator
|
||||
def test_transfer_user_throws_transfer_and_delete_success_messages(self):
|
||||
"""Test that success messages for data transfer and user deletion are displayed."""
|
||||
# Ensure the setup for VerifiedByStaff
|
||||
|
@ -3834,8 +3866,6 @@ class TestTransferUser(WebTest):
|
|||
|
||||
self.assertContains(after_submit, "<h1>Change user</h1>")
|
||||
|
||||
print(mock_success_message.call_args_list)
|
||||
|
||||
mock_success_message.assert_any_call(
|
||||
ANY,
|
||||
(
|
||||
|
|
|
@ -178,7 +178,7 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
Then a user-friendly success message is returned for displaying on the web
|
||||
And `state` is set to `DELETED`
|
||||
"""
|
||||
domain = create_ready_domain()
|
||||
domain, _ = Domain.objects.get_or_create(name="my-nameserver.gov", state=Domain.State.READY)
|
||||
# Put in client hold
|
||||
domain.place_client_hold()
|
||||
# Ensure everything is displaying correctly
|
||||
|
@ -212,7 +212,7 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.INFO,
|
||||
"Domain city.gov has been deleted. Thanks!",
|
||||
"Domain my-nameserver.gov has been deleted. Thanks!",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
)
|
||||
|
@ -224,7 +224,7 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
|
||||
# @less_console_noise_decorator
|
||||
@less_console_noise_decorator
|
||||
def test_deletion_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: Domain deletion is unsuccessful
|
||||
|
@ -266,7 +266,7 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.ERROR,
|
||||
"Error deleting this Domain: This subdomain is being used as a hostname on another domain: ns1.sharedhost.com", # noqa
|
||||
"Error deleting this Domain: Command failed with note: Domain has associated objects that prevent deletion.", # noqa
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
)
|
||||
|
@ -321,7 +321,7 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
Then `commands.DeleteDomain` is sent to the registry
|
||||
And Domain returns normally without an error dialog
|
||||
"""
|
||||
domain = create_ready_domain()
|
||||
domain, _ = Domain.objects.get_or_create(name="my-nameserver.gov", state=Domain.State.READY)
|
||||
# Put in client hold
|
||||
domain.place_client_hold()
|
||||
# Ensure everything is displaying correctly
|
||||
|
@ -340,12 +340,13 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
)
|
||||
request.user = self.client
|
||||
# Delete it once
|
||||
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.INFO,
|
||||
"Domain city.gov has been deleted. Thanks!",
|
||||
"Domain my-nameserver.gov has been deleted. Thanks!",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
)
|
||||
|
@ -881,7 +882,7 @@ class TestDomainAdminWithClient(TestCase):
|
|||
response = self.client.get("/admin/registrar/domain/")
|
||||
# There are 4 template references to Federal (4) plus four references in the table
|
||||
# for our actual domain_request
|
||||
self.assertContains(response, "Federal", count=56)
|
||||
self.assertContains(response, "Federal", count=57)
|
||||
# This may be a bit more robust
|
||||
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
|
||||
# Now let's make sure the long description does not exist
|
||||
|
|
|
@ -662,7 +662,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
|
||||
# There are 2 template references to Federal (4) and two in the results data
|
||||
# of the request
|
||||
self.assertContains(response, "Federal", count=54)
|
||||
self.assertContains(response, "Federal", count=55)
|
||||
# This may be a bit more robust
|
||||
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
|
||||
# Now let's make sure the long description does not exist
|
||||
|
|
|
@ -62,7 +62,7 @@ class GetSeniorOfficialJsonTest(TestCase):
|
|||
p = "password"
|
||||
self.client.login(username="testuser", password=p)
|
||||
response = self.client.get(self.api_url, {"agency_name": "Test Agency"})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_senior_official_json_not_found(self):
|
||||
|
@ -138,7 +138,7 @@ class GetPortfolioJsonTest(TestCase):
|
|||
"""Test that an unauthenticated user receives a 403 with an error message."""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.api_url, {"id": self.portfolio.id})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_portfolio_json_not_found(self):
|
||||
|
@ -181,7 +181,7 @@ class GetFederalPortfolioTypeJsonTest(TestCase):
|
|||
p = "password"
|
||||
self.client.login(username="testuser", password=p)
|
||||
response = self.client.get(self.api_url, {"agency_name": "Test Agency", "organization_type": "federal"})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class GetActionNeededEmailForUserJsonTest(TestCase):
|
||||
|
|
|
@ -3,6 +3,7 @@ from unittest.mock import patch, MagicMock
|
|||
from datetime import date
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user import User
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
|
@ -16,6 +17,8 @@ from registrar.utility.email_invitations import (
|
|||
send_portfolio_admin_addition_emails,
|
||||
send_portfolio_admin_removal_emails,
|
||||
send_portfolio_invitation_email,
|
||||
send_portfolio_invitation_remove_email,
|
||||
send_portfolio_member_permission_remove_email,
|
||||
send_portfolio_member_permission_update_email,
|
||||
)
|
||||
|
||||
|
@ -963,3 +966,149 @@ class TestSendPortfolioMemberPermissionUpdateEmail(unittest.TestCase):
|
|||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_not_called() # Function should fail before logging email failure
|
||||
|
||||
|
||||
class TestSendPortfolioMemberPermissionRemoveEmail(unittest.TestCase):
|
||||
"""Unit tests for send_portfolio_member_permission_remove_email function."""
|
||||
|
||||
@patch("registrar.utility.email_invitations.send_templated_email")
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
def test_send_email_success(self, mock_get_requestor_email, mock_send_email):
|
||||
"""Test that the email is sent successfully when there are no errors."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
permissions = MagicMock(spec=UserPortfolioPermission)
|
||||
permissions.user.email = "user@example.com"
|
||||
permissions.portfolio.organization_name = "Test Portfolio"
|
||||
|
||||
mock_get_requestor_email.return_value = "requestor@example.com"
|
||||
|
||||
# Call function
|
||||
result = send_portfolio_member_permission_remove_email(requestor, permissions)
|
||||
|
||||
# Assertions
|
||||
mock_get_requestor_email.assert_called_once_with(requestor, portfolio=permissions.portfolio)
|
||||
mock_send_email.assert_called_once_with(
|
||||
"emails/portfolio_removal.txt",
|
||||
"emails/portfolio_removal_subject.txt",
|
||||
to_address="user@example.com",
|
||||
context={
|
||||
"requested_user": permissions.user,
|
||||
"portfolio": permissions.portfolio,
|
||||
"requestor_email": "requestor@example.com",
|
||||
},
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
@patch("registrar.utility.email_invitations.send_templated_email", side_effect=EmailSendingError("Email failed"))
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
@patch("registrar.utility.email_invitations.logger")
|
||||
def test_send_email_failure(self, mock_logger, mock_get_requestor_email, mock_send_email):
|
||||
"""Test that the function returns False and logs an error when email sending fails."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
permissions = MagicMock(spec=UserPortfolioPermission)
|
||||
permissions.user.email = "user@example.com"
|
||||
permissions.portfolio.organization_name = "Test Portfolio"
|
||||
|
||||
mock_get_requestor_email.return_value = "requestor@example.com"
|
||||
|
||||
# Call function
|
||||
result = send_portfolio_member_permission_remove_email(requestor, permissions)
|
||||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_called_once_with(
|
||||
"Could not send email organization member removal notification to %s for portfolio: %s",
|
||||
permissions.user.email,
|
||||
permissions.portfolio.organization_name,
|
||||
exc_info=True,
|
||||
)
|
||||
self.assertFalse(result)
|
||||
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email", side_effect=Exception("Unexpected error"))
|
||||
@patch("registrar.utility.email_invitations.logger")
|
||||
def test_requestor_email_retrieval_failure(self, mock_logger, mock_get_requestor_email):
|
||||
"""Test that an exception in retrieving requestor email is logged."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
permissions = MagicMock(spec=UserPortfolioPermission)
|
||||
|
||||
# Call function
|
||||
with self.assertRaises(Exception):
|
||||
send_portfolio_member_permission_remove_email(requestor, permissions)
|
||||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_not_called() # Function should fail before logging email failure
|
||||
|
||||
|
||||
class TestSendPortfolioInvitationRemoveEmail(unittest.TestCase):
|
||||
"""Unit tests for send_portfolio_invitation_remove_email function."""
|
||||
|
||||
@patch("registrar.utility.email_invitations.send_templated_email")
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
def test_send_email_success(self, mock_get_requestor_email, mock_send_email):
|
||||
"""Test that the email is sent successfully when there are no errors."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
invitation = MagicMock(spec=PortfolioInvitation)
|
||||
invitation.email = "user@example.com"
|
||||
invitation.portfolio.organization_name = "Test Portfolio"
|
||||
|
||||
mock_get_requestor_email.return_value = "requestor@example.com"
|
||||
|
||||
# Call function
|
||||
result = send_portfolio_invitation_remove_email(requestor, invitation)
|
||||
|
||||
# Assertions
|
||||
mock_get_requestor_email.assert_called_once_with(requestor, portfolio=invitation.portfolio)
|
||||
mock_send_email.assert_called_once_with(
|
||||
"emails/portfolio_removal.txt",
|
||||
"emails/portfolio_removal_subject.txt",
|
||||
to_address="user@example.com",
|
||||
context={
|
||||
"requested_user": None,
|
||||
"portfolio": invitation.portfolio,
|
||||
"requestor_email": "requestor@example.com",
|
||||
},
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
@patch("registrar.utility.email_invitations.send_templated_email", side_effect=EmailSendingError("Email failed"))
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
@patch("registrar.utility.email_invitations.logger")
|
||||
def test_send_email_failure(self, mock_logger, mock_get_requestor_email, mock_send_email):
|
||||
"""Test that the function returns False and logs an error when email sending fails."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
invitation = MagicMock(spec=PortfolioInvitation)
|
||||
invitation.email = "user@example.com"
|
||||
invitation.portfolio.organization_name = "Test Portfolio"
|
||||
|
||||
mock_get_requestor_email.return_value = "requestor@example.com"
|
||||
|
||||
# Call function
|
||||
result = send_portfolio_invitation_remove_email(requestor, invitation)
|
||||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_called_once_with(
|
||||
"Could not send email organization member removal notification to %s for portfolio: %s",
|
||||
invitation.email,
|
||||
invitation.portfolio.organization_name,
|
||||
exc_info=True,
|
||||
)
|
||||
self.assertFalse(result)
|
||||
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email", side_effect=Exception("Unexpected error"))
|
||||
@patch("registrar.utility.email_invitations.logger")
|
||||
def test_requestor_email_retrieval_failure(self, mock_logger, mock_get_requestor_email):
|
||||
"""Test that an exception in retrieving requestor email is logged."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
invitation = MagicMock(spec=PortfolioInvitation)
|
||||
|
||||
# Call function
|
||||
with self.assertRaises(Exception):
|
||||
send_portfolio_invitation_remove_email(requestor, invitation)
|
||||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_not_called() # Function should fail before logging email failure
|
||||
|
|
|
@ -38,7 +38,6 @@ from epplibwrapper import commands, common
|
|||
|
||||
from .common import (
|
||||
MockEppLib,
|
||||
less_console_noise,
|
||||
completed_domain_request,
|
||||
MockSESClient,
|
||||
MockDbForIndividualTests,
|
||||
|
@ -454,6 +453,7 @@ class TestPopulateFirstReady(TestCase):
|
|||
# Delete domains
|
||||
Domain.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def run_populate_first_ready(self):
|
||||
"""
|
||||
This method executes the populate_first_ready command.
|
||||
|
@ -461,103 +461,102 @@ class TestPopulateFirstReady(TestCase):
|
|||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the populate_first_ready command with the specified arguments.
|
||||
"""
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("populate_first_ready")
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("populate_first_ready")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_populate_first_ready_state_ready(self):
|
||||
"""
|
||||
Tests that the populate_first_ready works as expected for the state 'ready'
|
||||
"""
|
||||
with less_console_noise():
|
||||
# Set the created at date
|
||||
self.ready_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.ready_domain.save()
|
||||
desired_domain = copy.deepcopy(self.ready_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
self.assertEqual(desired_domain, self.ready_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
# Set the created at date
|
||||
self.ready_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.ready_domain.save()
|
||||
desired_domain = copy.deepcopy(self.ready_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
self.assertEqual(desired_domain, self.ready_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_populate_first_ready_state_deleted(self):
|
||||
"""
|
||||
Tests that the populate_first_ready works as expected for the state 'deleted'
|
||||
"""
|
||||
with less_console_noise():
|
||||
# Set the created at date
|
||||
self.deleted_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.deleted_domain.save()
|
||||
desired_domain = copy.deepcopy(self.deleted_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
self.assertEqual(desired_domain, self.deleted_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
# Set the created at date
|
||||
self.deleted_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.deleted_domain.save()
|
||||
desired_domain = copy.deepcopy(self.deleted_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
self.assertEqual(desired_domain, self.deleted_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_populate_first_ready_state_dns_needed(self):
|
||||
"""
|
||||
Tests that the populate_first_ready doesn't make changes when a domain's state is 'dns_needed'
|
||||
"""
|
||||
with less_console_noise():
|
||||
# Set the created at date
|
||||
self.dns_needed_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.dns_needed_domain.save()
|
||||
desired_domain = copy.deepcopy(self.dns_needed_domain)
|
||||
desired_domain.first_ready = None
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.dns_needed_domain
|
||||
# The object should largely be unaltered (does not test first_ready)
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready
|
||||
# Explicitly test the first_ready date
|
||||
self.assertNotEqual(first_ready, self.ready_at_date)
|
||||
self.assertEqual(first_ready, None)
|
||||
# Set the created at date
|
||||
self.dns_needed_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.dns_needed_domain.save()
|
||||
desired_domain = copy.deepcopy(self.dns_needed_domain)
|
||||
desired_domain.first_ready = None
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.dns_needed_domain
|
||||
# The object should largely be unaltered (does not test first_ready)
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready
|
||||
# Explicitly test the first_ready date
|
||||
self.assertNotEqual(first_ready, self.ready_at_date)
|
||||
self.assertEqual(first_ready, None)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_populate_first_ready_state_on_hold(self):
|
||||
"""
|
||||
Tests that the populate_first_ready works as expected for the state 'on_hold'
|
||||
"""
|
||||
with less_console_noise():
|
||||
self.hold_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.hold_domain.save()
|
||||
desired_domain = copy.deepcopy(self.hold_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the update first ready_at script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.hold_domain
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
self.hold_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.hold_domain.save()
|
||||
desired_domain = copy.deepcopy(self.hold_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the update first ready_at script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.hold_domain
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_populate_first_ready_state_unknown(self):
|
||||
"""
|
||||
Tests that the populate_first_ready works as expected for the state 'unknown'
|
||||
"""
|
||||
with less_console_noise():
|
||||
# Set the created at date
|
||||
self.unknown_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.unknown_domain.save()
|
||||
desired_domain = copy.deepcopy(self.unknown_domain)
|
||||
desired_domain.first_ready = None
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.unknown_domain
|
||||
# The object should largely be unaltered (does not test first_ready)
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready
|
||||
self.assertNotEqual(first_ready, self.ready_at_date)
|
||||
self.assertEqual(first_ready, None)
|
||||
# Set the created at date
|
||||
self.unknown_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.unknown_domain.save()
|
||||
desired_domain = copy.deepcopy(self.unknown_domain)
|
||||
desired_domain.first_ready = None
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.unknown_domain
|
||||
# The object should largely be unaltered (does not test first_ready)
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready
|
||||
self.assertNotEqual(first_ready, self.ready_at_date)
|
||||
self.assertEqual(first_ready, None)
|
||||
|
||||
|
||||
class TestPatchAgencyInfo(TestCase):
|
||||
|
@ -578,10 +577,10 @@ class TestPatchAgencyInfo(TestCase):
|
|||
TransitionDomain.objects.all().delete()
|
||||
|
||||
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True)
|
||||
@less_console_noise_decorator
|
||||
def call_patch_federal_agency_info(self, mock_prompt):
|
||||
"""Calls the patch_federal_agency_info command and mimics a keypress"""
|
||||
with less_console_noise():
|
||||
call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True)
|
||||
call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True)
|
||||
|
||||
|
||||
class TestExtendExpirationDates(MockEppLib):
|
||||
|
@ -637,6 +636,7 @@ class TestExtendExpirationDates(MockEppLib):
|
|||
User.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def run_extend_expiration_dates(self):
|
||||
"""
|
||||
This method executes the extend_expiration_dates command.
|
||||
|
@ -644,83 +644,83 @@ class TestExtendExpirationDates(MockEppLib):
|
|||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the extend_expiration_dates command with the specified arguments.
|
||||
"""
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("extend_expiration_dates")
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("extend_expiration_dates")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_extends_expiration_date_correctly(self):
|
||||
"""
|
||||
Tests that the extend_expiration_dates method extends dates as expected
|
||||
"""
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 11, 15)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date
|
||||
self.assertEqual(current_domain.expiration_date, date(2024, 11, 15))
|
||||
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 11, 15)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date
|
||||
self.assertEqual(current_domain.expiration_date, date(2024, 11, 15))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_extends_expiration_date_skips_non_current(self):
|
||||
"""
|
||||
Tests that the extend_expiration_dates method correctly skips domains
|
||||
with an expiration date less than a certain threshold.
|
||||
"""
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="fake.gov").get()
|
||||
desired_domain.expiration_date = date(2022, 5, 25)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="fake.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2022, 5, 25))
|
||||
desired_domain = Domain.objects.filter(name="fake.gov").get()
|
||||
desired_domain.expiration_date = date(2022, 5, 25)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="fake.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2022, 5, 25))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_extends_expiration_date_skips_maximum_date(self):
|
||||
"""
|
||||
Tests that the extend_expiration_dates method correctly skips domains
|
||||
with an expiration date more than a certain threshold.
|
||||
"""
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 12, 31)
|
||||
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 12, 31)
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
|
||||
current_domain = Domain.objects.filter(name="fakemaximum.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
current_domain = Domain.objects.filter(name="fakemaximum.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2024, 12, 31))
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2024, 12, 31))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_extends_expiration_date_skips_non_ready(self):
|
||||
"""
|
||||
Tests that the extend_expiration_dates method correctly skips domains not in the state "ready"
|
||||
"""
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
|
||||
desired_domain.expiration_date = date(2023, 11, 15)
|
||||
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
|
||||
desired_domain.expiration_date = date(2023, 11, 15)
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
|
||||
current_domain = Domain.objects.filter(name="fakeneeded.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
current_domain = Domain.objects.filter(name="fakeneeded.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2023, 11, 15))
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2023, 11, 15))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_extends_expiration_date_idempotent(self):
|
||||
"""
|
||||
Tests the idempotency of the extend_expiration_dates command.
|
||||
|
@ -728,21 +728,20 @@ class TestExtendExpirationDates(MockEppLib):
|
|||
Verifies that running the method multiple times does not change the expiration date
|
||||
of a domain beyond the initial extension.
|
||||
"""
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 11, 15)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date
|
||||
self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
|
||||
# Run the expiration date script again
|
||||
self.run_extend_expiration_dates()
|
||||
# The old domain shouldn't have changed
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date - should be the same
|
||||
self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
|
||||
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 11, 15)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date
|
||||
self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
|
||||
# Run the expiration date script again
|
||||
self.run_extend_expiration_dates()
|
||||
# The old domain shouldn't have changed
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date - should be the same
|
||||
self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
|
||||
|
||||
|
||||
class TestDiscloseEmails(MockEppLib):
|
||||
|
@ -754,6 +753,7 @@ class TestDiscloseEmails(MockEppLib):
|
|||
PublicContact.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def run_disclose_security_emails(self):
|
||||
"""
|
||||
This method executes the disclose_security_emails command.
|
||||
|
@ -761,44 +761,43 @@ class TestDiscloseEmails(MockEppLib):
|
|||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the disclose_security_emails command.
|
||||
"""
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("disclose_security_emails")
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("disclose_security_emails")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_disclose_security_emails(self):
|
||||
"""
|
||||
Tests that command disclose_security_emails runs successfully with
|
||||
appropriate EPP calll to UpdateContact.
|
||||
"""
|
||||
with less_console_noise():
|
||||
domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY)
|
||||
expectedSecContact = PublicContact.get_default_security()
|
||||
expectedSecContact.domain = domain
|
||||
expectedSecContact.email = "123@mail.gov"
|
||||
# set domain security email to 123@mail.gov instead of default email
|
||||
domain.security_contact = expectedSecContact
|
||||
self.run_disclose_security_emails()
|
||||
domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY)
|
||||
expectedSecContact = PublicContact.get_default_security()
|
||||
expectedSecContact.domain = domain
|
||||
expectedSecContact.email = "123@mail.gov"
|
||||
# set domain security email to 123@mail.gov instead of default email
|
||||
domain.security_contact = expectedSecContact
|
||||
self.run_disclose_security_emails()
|
||||
|
||||
# running disclose_security_emails sends EPP call UpdateContact with disclose
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateContact(
|
||||
id=domain.security_contact.registry_id,
|
||||
postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact),
|
||||
email=domain.security_contact.email,
|
||||
voice=domain.security_contact.voice,
|
||||
fax=domain.security_contact.fax,
|
||||
auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
disclose=domain._disclose_fields(contact=domain.security_contact),
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
# running disclose_security_emails sends EPP call UpdateContact with disclose
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateContact(
|
||||
id=domain.security_contact.registry_id,
|
||||
postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact),
|
||||
email=domain.security_contact.email,
|
||||
voice=domain.security_contact.voice,
|
||||
fax=domain.security_contact.fax,
|
||||
auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
disclose=domain._disclose_fields(contact=domain.security_contact),
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TestCleanTables(TestCase):
|
||||
|
@ -813,17 +812,18 @@ class TestCleanTables(TestCase):
|
|||
self.logger_patcher.stop()
|
||||
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
@less_console_noise_decorator
|
||||
def test_command_logs_error_in_production(self):
|
||||
"""Test that the handle method does not process in production"""
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("clean_tables")
|
||||
self.logger_mock.error.assert_called_with("clean_tables cannot be run in production")
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("clean_tables")
|
||||
self.logger_mock.error.assert_called_with("clean_tables cannot be run in production")
|
||||
|
||||
@override_settings(IS_PRODUCTION=False)
|
||||
@less_console_noise_decorator
|
||||
def test_command_cleans_tables(self):
|
||||
"""test that the handle method functions properly to clean tables"""
|
||||
|
||||
|
@ -891,61 +891,61 @@ class TestCleanTables(TestCase):
|
|||
raise
|
||||
|
||||
@override_settings(IS_PRODUCTION=False)
|
||||
@less_console_noise_decorator
|
||||
def test_command_handles_nonexistent_model(self):
|
||||
"""Test that exceptions for non existent models are handled properly within the handle method"""
|
||||
with less_console_noise():
|
||||
with patch("django.apps.apps.get_model", side_effect=LookupError):
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("clean_tables")
|
||||
# Assert that the error message was logged for any of the table names
|
||||
self.logger_mock.error.assert_any_call("Model for table DomainInformation not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table DomainRequest not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table PublicContact not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Domain not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table User not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Contact not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Website not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table DraftDomain not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table HostIp not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Host not found.")
|
||||
with patch("django.apps.apps.get_model", side_effect=LookupError):
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("clean_tables")
|
||||
# Assert that the error message was logged for any of the table names
|
||||
self.logger_mock.error.assert_any_call("Model for table DomainInformation not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table DomainRequest not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table PublicContact not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Domain not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table User not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Contact not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Website not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table DraftDomain not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table HostIp not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Host not found.")
|
||||
|
||||
@override_settings(IS_PRODUCTION=False)
|
||||
@less_console_noise_decorator
|
||||
def test_command_logs_other_exceptions(self):
|
||||
"""Test that generic exceptions are handled properly in the handle method"""
|
||||
with less_console_noise():
|
||||
with patch("django.apps.apps.get_model") as get_model_mock:
|
||||
model_mock = MagicMock()
|
||||
get_model_mock.return_value = model_mock
|
||||
with patch("django.apps.apps.get_model") as get_model_mock:
|
||||
model_mock = MagicMock()
|
||||
get_model_mock.return_value = model_mock
|
||||
|
||||
# Mock the values_list so that DomainInformation attempts a delete
|
||||
pk_batches = [[1, 2, 3, 4, 5, 6], []]
|
||||
# Mock the values_list so that DomainInformation attempts a delete
|
||||
pk_batches = [[1, 2, 3, 4, 5, 6], []]
|
||||
|
||||
def values_list_side_effect(*args, **kwargs):
|
||||
if args == ("pk",) and kwargs.get("flat", False):
|
||||
return pk_batches.pop(0)
|
||||
return []
|
||||
def values_list_side_effect(*args, **kwargs):
|
||||
if args == ("pk",) and kwargs.get("flat", False):
|
||||
return pk_batches.pop(0)
|
||||
return []
|
||||
|
||||
model_mock.objects.values_list.side_effect = values_list_side_effect
|
||||
model_mock.objects.values_list.side_effect = values_list_side_effect
|
||||
|
||||
# Mock delete to raise a generic exception
|
||||
model_mock.objects.filter.return_value.delete.side_effect = Exception("Mocked delete exception")
|
||||
# Mock delete to raise a generic exception
|
||||
model_mock.objects.filter.return_value.delete.side_effect = Exception("Mocked delete exception")
|
||||
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
|
||||
return_value=True,
|
||||
):
|
||||
with self.assertRaises(Exception) as context:
|
||||
# Execute the command
|
||||
call_command("clean_tables")
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
|
||||
return_value=True,
|
||||
):
|
||||
with self.assertRaises(Exception) as context:
|
||||
# Execute the command
|
||||
call_command("clean_tables")
|
||||
|
||||
# Check the exception message
|
||||
self.assertEqual(str(context.exception), "Custom delete error")
|
||||
# Check the exception message
|
||||
self.assertEqual(str(context.exception), "Custom delete error")
|
||||
|
||||
# Assert that delete was called
|
||||
model_mock.objects.filter.return_value.delete.assert_called()
|
||||
# Assert that delete was called
|
||||
model_mock.objects.filter.return_value.delete.assert_called()
|
||||
|
||||
|
||||
class TestExportTables(MockEppLib):
|
||||
|
@ -1030,34 +1030,34 @@ class TestExportTables(MockEppLib):
|
|||
self.logger_mock.info.assert_any_call(f"Removed {table_name}_1.csv")
|
||||
|
||||
@patch("registrar.management.commands.export_tables.getattr")
|
||||
@less_console_noise_decorator
|
||||
def test_export_table_handles_missing_resource_class(self, mock_getattr):
|
||||
"""Test that missing resource classes are handled properly in the handle method"""
|
||||
with less_console_noise():
|
||||
mock_getattr.side_effect = AttributeError
|
||||
mock_getattr.side_effect = AttributeError
|
||||
|
||||
# Import the command to avoid any locale or gettext issues
|
||||
command_class = import_string("registrar.management.commands.export_tables.Command")
|
||||
command_instance = command_class()
|
||||
command_instance.export_table("NonExistentTable")
|
||||
# Import the command to avoid any locale or gettext issues
|
||||
command_class = import_string("registrar.management.commands.export_tables.Command")
|
||||
command_instance = command_class()
|
||||
command_instance.export_table("NonExistentTable")
|
||||
|
||||
self.logger_mock.error.assert_called_with(
|
||||
"Resource class NonExistentTableResource not found in registrar.admin"
|
||||
)
|
||||
self.logger_mock.error.assert_called_with(
|
||||
"Resource class NonExistentTableResource not found in registrar.admin"
|
||||
)
|
||||
|
||||
@patch("registrar.management.commands.export_tables.getattr")
|
||||
@less_console_noise_decorator
|
||||
def test_export_table_handles_generic_exception(self, mock_getattr):
|
||||
"""Test that general exceptions in the handle method are handled correctly"""
|
||||
with less_console_noise():
|
||||
mock_resource_class = MagicMock()
|
||||
mock_resource_class().export.side_effect = Exception("Test Exception")
|
||||
mock_getattr.return_value = mock_resource_class
|
||||
mock_resource_class = MagicMock()
|
||||
mock_resource_class().export.side_effect = Exception("Test Exception")
|
||||
mock_getattr.return_value = mock_resource_class
|
||||
|
||||
# Import the command to avoid any locale or gettext issues
|
||||
command_class = import_string("registrar.management.commands.export_tables.Command")
|
||||
command_instance = command_class()
|
||||
command_instance.export_table("TestTable")
|
||||
# Import the command to avoid any locale or gettext issues
|
||||
command_class = import_string("registrar.management.commands.export_tables.Command")
|
||||
command_instance = command_class()
|
||||
command_instance.export_table("TestTable")
|
||||
|
||||
self.logger_mock.error.assert_called_with("Failed to export TestTable: Test Exception")
|
||||
self.logger_mock.error.assert_called_with("Failed to export TestTable: Test Exception")
|
||||
|
||||
|
||||
class TestImportTables(TestCase):
|
||||
|
@ -1073,6 +1073,7 @@ class TestImportTables(TestCase):
|
|||
@patch("registrar.management.commands.import_tables.getattr")
|
||||
@patch("django.apps.apps.get_model")
|
||||
@patch("os.listdir")
|
||||
@less_console_noise_decorator
|
||||
def test_handle(
|
||||
self,
|
||||
mock_listdir,
|
||||
|
@ -1087,105 +1088,104 @@ class TestImportTables(TestCase):
|
|||
mock_makedirs,
|
||||
):
|
||||
"""Test that the handle method properly imports tables"""
|
||||
with less_console_noise():
|
||||
# Mock os.makedirs to do nothing
|
||||
mock_makedirs.return_value = None
|
||||
# Mock os.makedirs to do nothing
|
||||
mock_makedirs.return_value = None
|
||||
|
||||
# Mock os.path.exists to always return True
|
||||
mock_path_exists.return_value = True
|
||||
# Mock os.path.exists to always return True
|
||||
mock_path_exists.return_value = True
|
||||
|
||||
# Mock the zipfile to have extractall return None
|
||||
mock_zipfile_instance = mock_zipfile.return_value.__enter__.return_value
|
||||
mock_zipfile_instance.extractall.return_value = None
|
||||
# Mock the zipfile to have extractall return None
|
||||
mock_zipfile_instance = mock_zipfile.return_value.__enter__.return_value
|
||||
mock_zipfile_instance.extractall.return_value = None
|
||||
|
||||
# Check that the import_table function was called for each table
|
||||
table_names = [
|
||||
"User",
|
||||
"Contact",
|
||||
"Domain",
|
||||
"DomainRequest",
|
||||
"DomainInformation",
|
||||
"UserDomainRole",
|
||||
"DraftDomain",
|
||||
"Website",
|
||||
"HostIp",
|
||||
"Host",
|
||||
"PublicContact",
|
||||
]
|
||||
# Check that the import_table function was called for each table
|
||||
table_names = [
|
||||
"User",
|
||||
"Contact",
|
||||
"Domain",
|
||||
"DomainRequest",
|
||||
"DomainInformation",
|
||||
"UserDomainRole",
|
||||
"DraftDomain",
|
||||
"Website",
|
||||
"HostIp",
|
||||
"Host",
|
||||
"PublicContact",
|
||||
]
|
||||
|
||||
# Mock directory listing
|
||||
mock_listdir.side_effect = lambda path: [f"{table}_1.csv" for table in table_names]
|
||||
# Mock directory listing
|
||||
mock_listdir.side_effect = lambda path: [f"{table}_1.csv" for table in table_names]
|
||||
|
||||
# Mock the CSV file content
|
||||
csv_content = b"mock_csv_data"
|
||||
# Mock the CSV file content
|
||||
csv_content = b"mock_csv_data"
|
||||
|
||||
# Mock the open function to return a mock file
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = csv_content
|
||||
# Mock the open function to return a mock file
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = csv_content
|
||||
|
||||
# Mock the Dataset class and its load method to return a dataset
|
||||
mock_dataset_instance = MagicMock(spec=tablib.Dataset)
|
||||
with patch(
|
||||
"registrar.management.commands.import_tables.tablib.Dataset.load", return_value=mock_dataset_instance
|
||||
):
|
||||
# Mock the resource class and its import method
|
||||
mock_resource_class = MagicMock()
|
||||
mock_resource_instance = MagicMock()
|
||||
mock_result = MagicMock()
|
||||
mock_result.has_errors.return_value = False
|
||||
mock_resource_instance.import_data.return_value = mock_result
|
||||
mock_resource_class.return_value = mock_resource_instance
|
||||
mock_getattr.return_value = mock_resource_class
|
||||
# Mock the Dataset class and its load method to return a dataset
|
||||
mock_dataset_instance = MagicMock(spec=tablib.Dataset)
|
||||
with patch(
|
||||
"registrar.management.commands.import_tables.tablib.Dataset.load", return_value=mock_dataset_instance
|
||||
):
|
||||
# Mock the resource class and its import method
|
||||
mock_resource_class = MagicMock()
|
||||
mock_resource_instance = MagicMock()
|
||||
mock_result = MagicMock()
|
||||
mock_result.has_errors.return_value = False
|
||||
mock_resource_instance.import_data.return_value = mock_result
|
||||
mock_resource_class.return_value = mock_resource_instance
|
||||
mock_getattr.return_value = mock_resource_class
|
||||
|
||||
# Call the command
|
||||
call_command("import_tables")
|
||||
# Call the command
|
||||
call_command("import_tables")
|
||||
|
||||
# Check that os.makedirs was called once to create the tmp directory
|
||||
mock_makedirs.assert_called_once_with("tmp", exist_ok=True)
|
||||
# Check that os.makedirs was called once to create the tmp directory
|
||||
mock_makedirs.assert_called_once_with("tmp", exist_ok=True)
|
||||
|
||||
# Check that os.path.exists was called once for the zip file
|
||||
mock_path_exists.assert_any_call("tmp/exported_tables.zip")
|
||||
# Check that os.path.exists was called once for the zip file
|
||||
mock_path_exists.assert_any_call("tmp/exported_tables.zip")
|
||||
|
||||
# Check that pyzipper.AESZipFile was called once to open the zip file
|
||||
mock_zipfile.assert_called_once_with("tmp/exported_tables.zip", "r")
|
||||
# Check that pyzipper.AESZipFile was called once to open the zip file
|
||||
mock_zipfile.assert_called_once_with("tmp/exported_tables.zip", "r")
|
||||
|
||||
# Check that extractall was called once to extract the zip file contents
|
||||
mock_zipfile_instance.extractall.assert_called_once_with("tmp")
|
||||
# Check that extractall was called once to extract the zip file contents
|
||||
mock_zipfile_instance.extractall.assert_called_once_with("tmp")
|
||||
|
||||
# Check that os.path.exists was called for each table
|
||||
for table_name in table_names:
|
||||
mock_path_exists.assert_any_call(f"{table_name}_1.csv")
|
||||
# Check that os.path.exists was called for each table
|
||||
for table_name in table_names:
|
||||
mock_path_exists.assert_any_call(f"{table_name}_1.csv")
|
||||
|
||||
# Check that logger.info was called for each successful import
|
||||
for table_name in table_names:
|
||||
mock_logger.info.assert_any_call(f"Successfully imported {table_name}_1.csv into {table_name}")
|
||||
# Check that logger.info was called for each successful import
|
||||
for table_name in table_names:
|
||||
mock_logger.info.assert_any_call(f"Successfully imported {table_name}_1.csv into {table_name}")
|
||||
|
||||
# Check that logger.error was not called for resource class not found
|
||||
mock_logger.error.assert_not_called()
|
||||
# Check that logger.error was not called for resource class not found
|
||||
mock_logger.error.assert_not_called()
|
||||
|
||||
# Check that os.remove was called for each CSV file
|
||||
for table_name in table_names:
|
||||
mock_remove.assert_any_call(f"{table_name}_1.csv")
|
||||
# Check that os.remove was called for each CSV file
|
||||
for table_name in table_names:
|
||||
mock_remove.assert_any_call(f"{table_name}_1.csv")
|
||||
|
||||
# Check that logger.info was called for each CSV file removal
|
||||
for table_name in table_names:
|
||||
mock_logger.info.assert_any_call(f"Removed temporary file {table_name}_1.csv")
|
||||
# Check that logger.info was called for each CSV file removal
|
||||
for table_name in table_names:
|
||||
mock_logger.info.assert_any_call(f"Removed temporary file {table_name}_1.csv")
|
||||
|
||||
@patch("registrar.management.commands.import_tables.logger")
|
||||
@patch("registrar.management.commands.import_tables.os.makedirs")
|
||||
@patch("registrar.management.commands.import_tables.os.path.exists")
|
||||
@less_console_noise_decorator
|
||||
def test_handle_zip_file_not_found(self, mock_path_exists, mock_makedirs, mock_logger):
|
||||
"""Test the handle method when the zip file doesn't exist"""
|
||||
with less_console_noise():
|
||||
# Mock os.makedirs to do nothing
|
||||
mock_makedirs.return_value = None
|
||||
# Mock os.makedirs to do nothing
|
||||
mock_makedirs.return_value = None
|
||||
|
||||
# Mock os.path.exists to return False
|
||||
mock_path_exists.return_value = False
|
||||
# Mock os.path.exists to return False
|
||||
mock_path_exists.return_value = False
|
||||
|
||||
call_command("import_tables")
|
||||
call_command("import_tables")
|
||||
|
||||
# Check that logger.error was called with the correct message
|
||||
mock_logger.error.assert_called_once_with("Zip file tmp/exported_tables.zip does not exist.")
|
||||
# Check that logger.error was called with the correct message
|
||||
mock_logger.error.assert_called_once_with("Zip file tmp/exported_tables.zip does not exist.")
|
||||
|
||||
|
||||
class TestTransferFederalAgencyType(TestCase):
|
||||
|
@ -1255,6 +1255,7 @@ class TestTransferFederalAgencyType(TestCase):
|
|||
id__in=[self.amtrak.id, self.legislative_branch.id, self.library_of_congress.id, self.gov_admin.id]
|
||||
).delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def run_transfer_federal_agency_type(self):
|
||||
"""
|
||||
This method executes the transfer_federal_agency_type command.
|
||||
|
@ -1262,12 +1263,11 @@ class TestTransferFederalAgencyType(TestCase):
|
|||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the populate_first_ready command with the specified arguments.
|
||||
"""
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("transfer_federal_agency_type")
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("transfer_federal_agency_type")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_transfer_federal_agency_type_script(self):
|
||||
|
@ -1630,6 +1630,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
# Test the senior official
|
||||
self.assertEqual(portfolio.senior_official, self.senior_official)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_create_multiple_portfolios_for_branch_judicial(self):
|
||||
"""Tests creating all portfolios under a given branch"""
|
||||
federal_choice = DomainRequest.OrganizationChoices.FEDERAL
|
||||
|
@ -1657,6 +1658,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
self.assertTrue(all([creator == User.get_default_user() for creator in creators]))
|
||||
self.assertTrue(all([note == "Auto-generated record" for note in notes]))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_create_multiple_portfolios_for_branch_legislative(self):
|
||||
"""Tests creating all portfolios under a given branch"""
|
||||
federal_choice = DomainRequest.OrganizationChoices.FEDERAL
|
||||
|
@ -1684,6 +1686,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
self.assertTrue(all([creator == User.get_default_user() for creator in creators]))
|
||||
self.assertTrue(all([note == "Auto-generated record" for note in notes]))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_script_adds_requested_suborganization_information(self):
|
||||
"""Tests that the script adds the requested suborg fields for domain requests"""
|
||||
# Create a new domain request with some errant spacing
|
||||
|
@ -1712,6 +1715,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
custom_suborg_request.suborganization_state_territory, DomainRequest.StateTerritoryChoices.TEXAS
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_create_multiple_portfolios_for_branch_executive(self):
|
||||
"""Tests creating all portfolios under a given branch"""
|
||||
federal_choice = DomainRequest.OrganizationChoices.FEDERAL
|
||||
|
@ -1774,6 +1778,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
self.assertEqual(expected_requests.count(), 2)
|
||||
self.assertEqual(expected_domain_infos.count(), 2)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_handle_portfolio_requests(self):
|
||||
"""Verify portfolio association with domain requests."""
|
||||
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True)
|
||||
|
@ -1783,6 +1788,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
self.assertEqual(self.domain_request.portfolio.federal_agency, self.federal_agency)
|
||||
self.assertEqual(self.domain_request.sub_organization.name, "Testorg")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_handle_portfolio_domains(self):
|
||||
"""Check portfolio association with domain information."""
|
||||
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_domains=True)
|
||||
|
@ -1792,6 +1798,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
self.assertEqual(self.domain_info.portfolio.federal_agency, self.federal_agency)
|
||||
self.assertEqual(self.domain_info.sub_organization.name, "Testorg")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_handle_parse_both(self):
|
||||
"""Ensure correct parsing of both requests and domains."""
|
||||
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True, parse_domains=True)
|
||||
|
@ -1802,6 +1809,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
self.assertIsNotNone(self.domain_info.portfolio)
|
||||
self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_command_error_parse_options(self):
|
||||
"""Verify error when bad parse options are provided."""
|
||||
# The command should enforce either --branch or --agency_name
|
||||
|
@ -1823,6 +1831,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
):
|
||||
self.run_create_federal_portfolio(agency_name="test")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_command_error_agency_not_found(self):
|
||||
"""Check error handling for non-existent agency."""
|
||||
expected_message = (
|
||||
|
@ -1832,6 +1841,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
with self.assertRaisesRegex(CommandError, expected_message):
|
||||
self.run_create_federal_portfolio(agency_name="Non-existent Agency", parse_requests=True)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_does_not_update_existing_portfolio(self):
|
||||
"""Tests that an existing portfolio is not updated when"""
|
||||
# Create an existing portfolio
|
||||
|
@ -2433,6 +2443,7 @@ class TestRemovePortfolios(TestCase):
|
|||
Portfolio.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
|
||||
def test_delete_unlisted_portfolios(self, mock_query_yes_no):
|
||||
"""Test that portfolios not on the allowed list are deleted."""
|
||||
|
@ -2450,6 +2461,7 @@ class TestRemovePortfolios(TestCase):
|
|||
self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists())
|
||||
self.assertTrue(Portfolio.objects.filter(organization_name="Department of Veterans Affairs").exists())
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
|
||||
def test_delete_entries_with_related_objects(self, mock_query_yes_no):
|
||||
"""Test deletion with related objects being handled properly."""
|
||||
|
@ -2473,6 +2485,7 @@ class TestRemovePortfolios(TestCase):
|
|||
# Check that the portfolio was deleted
|
||||
self.assertFalse(Portfolio.objects.filter(organization_name="Test with orphaned objects").exists())
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
|
||||
def test_delete_entries_with_suborganizations(self, mock_query_yes_no):
|
||||
"""Test that suborganizations and their related objects are deleted along with the portfolio."""
|
||||
|
|
|
@ -2039,7 +2039,7 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
self.wizard = DomainRequestWizard()
|
||||
self.wizard._domain_request = self.domain_request
|
||||
self.wizard.request = Mock(user=self.user, session={})
|
||||
self.wizard.kwargs = {"id": self.domain_request.id}
|
||||
self.wizard.kwargs = {"domain_request_pk": self.domain_request.id}
|
||||
|
||||
# We use both of these flags in the test. In the normal app these are generated normally.
|
||||
# The alternative syntax is adding the decorator to each test.
|
||||
|
|
|
@ -35,6 +35,7 @@ from epplibwrapper import (
|
|||
from .common import MockEppLib, MockSESClient, less_console_noise
|
||||
import logging
|
||||
import boto3_mocking # type: ignore
|
||||
import copy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -97,58 +98,59 @@ class TestDomainCache(MockEppLib):
|
|||
|
||||
self.mockedSendFunction.assert_has_calls(expectedCalls)
|
||||
|
||||
# @less_console_noise_decorator
|
||||
def test_cache_nested_elements_not_subdomain(self):
|
||||
"""Cache works correctly with the nested objects cache and hosts"""
|
||||
with less_console_noise():
|
||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
# The contact list will initially contain objects of type 'DomainContact'
|
||||
# this is then transformed into PublicContact, and cache should NOT
|
||||
# hold onto the DomainContact object
|
||||
expectedUnfurledContactsList = [
|
||||
common.DomainContact(contact="123", type="security"),
|
||||
]
|
||||
expectedContactsDict = {
|
||||
PublicContact.ContactTypeChoices.ADMINISTRATIVE: "adminContact",
|
||||
PublicContact.ContactTypeChoices.SECURITY: "securityContact",
|
||||
PublicContact.ContactTypeChoices.TECHNICAL: "technicalContact",
|
||||
}
|
||||
expectedHostsDict = {
|
||||
"name": self.mockDataInfoDomain.hosts[0],
|
||||
"addrs": [], # should return empty bc fake.host.com is not a subdomain of igorville.gov
|
||||
"cr_date": self.mockDataInfoHosts.cr_date,
|
||||
}
|
||||
|
||||
# this can be changed when the getter for contacts is implemented
|
||||
domain._get_property("contacts")
|
||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
# The contact list will initially contain objects of type 'DomainContact'
|
||||
# this is then transformed into PublicContact, and cache should NOT
|
||||
# hold onto the DomainContact object
|
||||
expectedUnfurledContactsList = [
|
||||
common.DomainContact(contact="123", type="security"),
|
||||
]
|
||||
expectedContactsDict = {
|
||||
PublicContact.ContactTypeChoices.ADMINISTRATIVE: "adminContact",
|
||||
PublicContact.ContactTypeChoices.SECURITY: "securityContact",
|
||||
PublicContact.ContactTypeChoices.TECHNICAL: "technicalContact",
|
||||
}
|
||||
expectedHostsDict = {
|
||||
"name": self.mockDataInfoDomain.hosts[0],
|
||||
"addrs": [], # should return empty bc fake.host.com is not a subdomain of igorville.gov
|
||||
"cr_date": self.mockDataInfoHosts.cr_date,
|
||||
}
|
||||
|
||||
# check domain info is still correct and not overridden
|
||||
self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info)
|
||||
self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date)
|
||||
# this can be changed when the getter for contacts is implemented
|
||||
domain._get_property("contacts")
|
||||
|
||||
# check contacts
|
||||
self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomain.contacts)
|
||||
# The contact list should not contain what is sent by the registry by default,
|
||||
# as _fetch_cache will transform the type to PublicContact
|
||||
self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList)
|
||||
# check domain info is still correct and not overridden
|
||||
self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info)
|
||||
self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date)
|
||||
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
# check contacts
|
||||
self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomain.contacts)
|
||||
# The contact list should not contain what is sent by the registry by default,
|
||||
# as _fetch_cache will transform the type to PublicContact
|
||||
self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList)
|
||||
|
||||
# get and check hosts is set correctly
|
||||
domain._get_property("hosts")
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
# invalidate cache
|
||||
domain._cache = {}
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
|
||||
# get host
|
||||
domain._get_property("hosts")
|
||||
# Should return empty bc fake.host.com is not a subdomain of igorville.gov
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
# get and check hosts is set correctly
|
||||
domain._get_property("hosts")
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
# invalidate cache
|
||||
domain._cache = {}
|
||||
|
||||
# get contacts
|
||||
domain._get_property("contacts")
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
# get host
|
||||
domain._get_property("hosts")
|
||||
# Should return empty bc fake.host.com is not a subdomain of igorville.gov
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
|
||||
# get contacts
|
||||
domain._get_property("contacts")
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
|
||||
def test_cache_nested_elements_is_subdomain(self):
|
||||
"""Cache works correctly with the nested objects cache and hosts"""
|
||||
|
@ -1248,6 +1250,13 @@ class TestRegistrantNameservers(MockEppLib):
|
|||
name="threenameserversDomain.gov", state=Domain.State.READY
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
PublicContact.objects.all().delete()
|
||||
HostIP.objects.all().delete()
|
||||
Host.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
def test_get_nameserver_changes_success_deleted_vals(self):
|
||||
"""Testing only deleting and no other changes"""
|
||||
with less_console_noise():
|
||||
|
@ -1797,6 +1806,7 @@ class TestRegistrantNameservers(MockEppLib):
|
|||
mock_host_ip_get_or_create.assert_not_called()
|
||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||
|
||||
# @less_console_noise_decorator
|
||||
def test_nameservers_stored_on_fetch_cache_not_subdomain_with_ip(self):
|
||||
"""
|
||||
Scenario: Nameservers are stored in db when they are retrieved from fetch_cache.
|
||||
|
@ -1808,21 +1818,20 @@ class TestRegistrantNameservers(MockEppLib):
|
|||
#3: Nameserver is not a subdomain, but it does have an IP address returned
|
||||
due to how we set up our defaults
|
||||
"""
|
||||
with less_console_noise():
|
||||
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||
domain, _ = Domain.objects.get_or_create(name="freeman.gov", state=Domain.State.READY)
|
||||
|
||||
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||
HostIP.objects, "get_or_create"
|
||||
) as mock_host_ip_get_or_create:
|
||||
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||
HostIP.objects, "get_or_create"
|
||||
) as mock_host_ip_get_or_create:
|
||||
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||
|
||||
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||
domain.nameservers
|
||||
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||
domain.nameservers
|
||||
|
||||
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com")
|
||||
mock_host_ip_get_or_create.assert_not_called()
|
||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com")
|
||||
mock_host_ip_get_or_create.assert_not_called()
|
||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||
|
||||
def test_nameservers_stored_on_fetch_cache_not_subdomain_without_ip(self):
|
||||
"""
|
||||
|
@ -1861,12 +1870,6 @@ class TestRegistrantNameservers(MockEppLib):
|
|||
with self.assertRaises(RegistryError):
|
||||
domain.nameservers = [("ns1.failednameserver.gov", ["4.5.6"])]
|
||||
|
||||
def tearDown(self):
|
||||
HostIP.objects.all().delete()
|
||||
Host.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
return super().tearDown()
|
||||
|
||||
|
||||
class TestNameserverValidation(TestCase):
|
||||
"""Test the isValidDomain method which validates nameservers"""
|
||||
|
@ -1947,8 +1950,6 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
And a domain exists in the registry
|
||||
"""
|
||||
super().setUp()
|
||||
# for the tests, need a domain in the unknown state
|
||||
self.domain, _ = Domain.objects.get_or_create(name="fake.gov")
|
||||
|
||||
def tearDown(self):
|
||||
PublicContact.objects.all().delete()
|
||||
|
@ -2041,6 +2042,7 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData)
|
||||
patcher.stop()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_dnssec_is_idempotent(self):
|
||||
"""
|
||||
Scenario: Registrant adds DNS data twice, due to a UI glitch
|
||||
|
@ -2126,6 +2128,7 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData)
|
||||
patcher.stop()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_adds_dnssec_data_multiple_dsdata(self):
|
||||
"""
|
||||
Scenario: Registrant adds DNSSEC data with multiple DSData.
|
||||
|
@ -2194,6 +2197,7 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData)
|
||||
patcher.stop()
|
||||
|
||||
# @less_console_noise_decorator
|
||||
def test_user_removes_dnssec_data(self):
|
||||
"""
|
||||
Scenario: Registrant removes DNSSEC ds data.
|
||||
|
@ -2219,28 +2223,27 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
else:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
with less_console_noise():
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
with patch("registrar.models.domain.registry.send") as mocked_send:
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
|
||||
|
||||
# Initial setting of dnssec data
|
||||
domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
|
||||
# Check dsdata_last_change is updated
|
||||
domain = Domain.objects.get(name="dnssec-dsdata.gov")
|
||||
self.assertIsNotNone(domain.dsdata_last_change)
|
||||
|
||||
initial_change = domain.dsdata_last_change
|
||||
|
||||
# Invalidate the cache to force a fresh lookup
|
||||
domain._invalidate_cache()
|
||||
|
||||
# Remove dnssec data
|
||||
domain.dnssecdata = self.dnssecExtensionRemovingDsData
|
||||
|
||||
# Check that dsdata_last_change is updated again
|
||||
domain = Domain.objects.get(name="dnssec-dsdata.gov")
|
||||
self.assertIsNotNone(domain.dsdata_last_change)
|
||||
|
||||
self.assertNotEqual(domain.dsdata_last_change, initial_change)
|
||||
|
||||
# get the DNS SEC extension added to the UpdateDomain command and
|
||||
|
@ -2292,7 +2295,6 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
),
|
||||
]
|
||||
)
|
||||
patcher.stop()
|
||||
|
||||
def test_update_is_unsuccessful(self):
|
||||
"""
|
||||
|
@ -2697,38 +2699,6 @@ class TestAnalystDelete(MockEppLib):
|
|||
Domain.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_deletes_domain(self):
|
||||
"""
|
||||
Scenario: Analyst permanently deletes a domain
|
||||
When `domain.deletedInEpp()` is called
|
||||
Then `commands.DeleteDomain` is sent to the registry
|
||||
And `state` is set to `DELETED`
|
||||
|
||||
The deleted date is set.
|
||||
"""
|
||||
# Put the domain in client hold
|
||||
self.domain.place_client_hold()
|
||||
# Delete it...
|
||||
self.domain.deletedInEpp()
|
||||
self.domain.save()
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.DeleteDomain(name="fake.gov"),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
# Domain itself should not be deleted
|
||||
self.assertNotEqual(self.domain, None)
|
||||
# Domain should have the right state
|
||||
self.assertEqual(self.domain.state, Domain.State.DELETED)
|
||||
# Domain should have a deleted
|
||||
self.assertNotEqual(self.domain.deleted, None)
|
||||
# Cache should be invalidated
|
||||
self.assertEqual(self.domain._cache, {})
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_deletion_is_unsuccessful(self):
|
||||
"""
|
||||
|
@ -2756,18 +2726,44 @@ class TestAnalystDelete(MockEppLib):
|
|||
@less_console_noise_decorator
|
||||
def test_deletion_with_host_and_contacts(self):
|
||||
"""
|
||||
Scenario: Domain with related Host and Contacts is Deleted
|
||||
When a contact and host exists that is tied to this domain
|
||||
Then all the needed commands are sent to the registry
|
||||
And `state` is set to `DELETED`
|
||||
"""
|
||||
# Put the domain in client hold
|
||||
self.domain_with_contacts.place_client_hold()
|
||||
# Delete it
|
||||
self.domain_with_contacts.deletedInEpp()
|
||||
self.domain_with_contacts.save()
|
||||
Scenario: Domain with related Host and Contacts is Deleted.
|
||||
When a contact and host exists that is tied to this domain,
|
||||
then all the needed commands are sent to the registry and
|
||||
the domain's state is set to DELETED.
|
||||
|
||||
# Check that the host and contacts are deleted
|
||||
This test now asserts only the commands that are actually issued
|
||||
during the deletion process.
|
||||
"""
|
||||
# Put the domain in client hold.
|
||||
self.domain_with_contacts.place_client_hold()
|
||||
|
||||
# Invalidate the cache so that deletion fetches fresh data.
|
||||
self.domain_with_contacts._invalidate_cache()
|
||||
|
||||
# We'll use a mutable counter to simulate different responses if needed.
|
||||
info_domain_call_count = [0]
|
||||
|
||||
# TODO: This is a hack, we should refactor the MockEPPLib to be more flexible
|
||||
def side_effect(request, cleaned=True):
|
||||
# For an InfoDomain command for "freeman.gov", simulate behavior:
|
||||
if isinstance(request, commands.InfoDomain) and request.name.lower() == "freeman.gov":
|
||||
info_domain_call_count[0] += 1
|
||||
fake_info = copy.deepcopy(self.InfoDomainWithContacts)
|
||||
# If this branch ever gets hit, you could vary response based on call count.
|
||||
# But note: in our current deletion flow, InfoDomain may not be called.
|
||||
if info_domain_call_count[0] == 1:
|
||||
fake_info.hosts = ["fake.host.com"]
|
||||
else:
|
||||
fake_info.hosts = []
|
||||
return MagicMock(res_data=[fake_info])
|
||||
return self.mockedSendFunction(request, cleaned=cleaned)
|
||||
|
||||
with patch("registrar.models.domain.registry.send", side_effect=side_effect):
|
||||
self.domain_with_contacts.deletedInEpp()
|
||||
self.domain_with_contacts.save()
|
||||
|
||||
# Now assert the expected calls that we know occur.
|
||||
# Note: we no longer assert a call to InfoDomain.
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
|
@ -2782,14 +2778,10 @@ class TestAnalystDelete(MockEppLib):
|
|||
),
|
||||
cleaned=True,
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.InfoDomain(name="freeman.gov", auth_info=None),
|
||||
cleaned=True,
|
||||
),
|
||||
call(
|
||||
commands.InfoHost(name="fake.host.com"),
|
||||
cleaned=True,
|
||||
|
@ -2806,7 +2798,8 @@ class TestAnalystDelete(MockEppLib):
|
|||
),
|
||||
cleaned=True,
|
||||
),
|
||||
]
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
|
@ -2857,12 +2850,55 @@ class TestAnalystDelete(MockEppLib):
|
|||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Domain itself should not be deleted
|
||||
self.assertNotEqual(self.domain_with_contacts, None)
|
||||
# State should have changed
|
||||
self.assertIsNotNone(self.domain_with_contacts)
|
||||
self.assertEqual(self.domain_with_contacts.state, Domain.State.DELETED)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_deletes_domain_with_ds_data(self):
|
||||
"""
|
||||
Scenario: Domain with DS data is deleted
|
||||
When `domain.deletedInEpp()` is called
|
||||
Then `commands.DeleteDomain` is sent to the registry
|
||||
And `state` is set to `DELETED`
|
||||
"""
|
||||
# Create a domain with DS data
|
||||
domain, _ = Domain.objects.get_or_create(name="dsdomain.gov", state=Domain.State.READY)
|
||||
# set domain to be on hold
|
||||
domain.place_client_hold()
|
||||
domain.dnssecdata = extensions.DNSSECExtension(
|
||||
dsData=[extensions.DSData(keyTag=1, alg=1, digestType=1, digest="1234567890")],
|
||||
)
|
||||
domain.save()
|
||||
|
||||
# Mock the InfoDomain command data to return a domain with no hosts
|
||||
# This is needed to simulate the domain being able to be deleted
|
||||
self.mockDataInfoDomain.hosts = []
|
||||
|
||||
# Delete the domain
|
||||
domain.deletedInEpp()
|
||||
domain.save()
|
||||
|
||||
# Check that dsdata is None
|
||||
self.assertEqual(domain.dnssecdata, None)
|
||||
|
||||
# Check that the UpdateDomain command was sent to the registry with the correct extension
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="dsdomain.gov", add=[], rem=[], nsset=None, keyset=None, registrant=None, auth_info=None
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Check that the domain was deleted
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
|
||||
# reset to avoid test pollution
|
||||
self.mockDataInfoDomain.hosts = ["fake.host.com"]
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_deletion_ready_fsm_failure(self):
|
||||
"""
|
||||
|
|
|
@ -18,6 +18,9 @@ from .common import less_console_noise
|
|||
# request on the view.
|
||||
SAMPLE_KWARGS = {
|
||||
"app_label": "registrar",
|
||||
"domain_pk": "1",
|
||||
"domain_request_pk": "1",
|
||||
"domain_invitation_pk": "1",
|
||||
"pk": "1",
|
||||
"id": "1",
|
||||
"content_type_id": "2",
|
||||
|
@ -82,7 +85,6 @@ def iter_sample_urls(urlconf):
|
|||
if not viewname:
|
||||
continue
|
||||
if viewname == "auth_user_password_change":
|
||||
print(route)
|
||||
break
|
||||
named_groups = route.regex.groupindex.keys()
|
||||
kwargs = {}
|
||||
|
|
|
@ -126,7 +126,7 @@ class TestEnvironmentVariablesEffects(TestCase):
|
|||
with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error):
|
||||
with self.assertRaises(ValueError):
|
||||
contact_page_500 = self.client.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": fake_domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": fake_domain.id}),
|
||||
)
|
||||
|
||||
# Check that a 500 response is returned
|
||||
|
@ -147,7 +147,7 @@ class TestEnvironmentVariablesEffects(TestCase):
|
|||
with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error):
|
||||
with self.assertRaises(ValueError):
|
||||
contact_page_500 = self.client.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": fake_domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": fake_domain.id}),
|
||||
)
|
||||
|
||||
# Check that a 500 response is returned
|
||||
|
@ -292,7 +292,9 @@ class HomeTests(TestWithUser):
|
|||
)
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||
response = self.client.post(
|
||||
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}), follow=True
|
||||
)
|
||||
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
||||
|
@ -309,7 +311,9 @@ class HomeTests(TestWithUser):
|
|||
)
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||
response = self.client.post(
|
||||
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}), follow=True
|
||||
)
|
||||
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
||||
|
@ -335,7 +339,8 @@ class HomeTests(TestWithUser):
|
|||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(
|
||||
reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True
|
||||
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
# Check for a 403 error - the end user should not be allowed to do this
|
||||
|
@ -392,7 +397,7 @@ class HomeTests(TestWithUser):
|
|||
self.assertTrue(igorville.exists())
|
||||
|
||||
# Trigger the delete logic
|
||||
self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}))
|
||||
self.client.post(reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
|
||||
# igorville is now deleted
|
||||
igorville = DomainRequest.objects.filter(requested_domain__name="igorville.gov")
|
||||
|
@ -462,7 +467,7 @@ class HomeTests(TestWithUser):
|
|||
self.assertTrue(teaville.exists())
|
||||
|
||||
# Trigger the delete logic
|
||||
self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request_2.pk}))
|
||||
self.client.post(reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request_2.pk}))
|
||||
|
||||
teaville = DomainRequest.objects.filter(requested_domain__name="teaville.gov")
|
||||
self.assertFalse(teaville.exists())
|
||||
|
@ -935,7 +940,7 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
@less_console_noise_decorator
|
||||
def test_domain_detail_contains_your_profile(self):
|
||||
"""Tests that the domain detail view contains 'your profile' rather than 'your contact information'"""
|
||||
response = self.client.get(reverse("domain", args=[self.domain.pk]))
|
||||
response = self.client.get(reverse("domain", kwargs={"domain_pk": self.domain.pk}))
|
||||
self.assertContains(response, "Your profile")
|
||||
self.assertNotContains(response, "Your contact information")
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
"domain-security-email",
|
||||
]:
|
||||
with self.subTest(view_name=view_name):
|
||||
response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id}))
|
||||
response = self.client.get(reverse(view_name, kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -194,7 +194,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
"domain-security-email",
|
||||
]:
|
||||
with self.subTest(view_name=view_name):
|
||||
response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id}))
|
||||
response = self.client.get(reverse(view_name, kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -218,7 +218,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
self.domain_deleted,
|
||||
]:
|
||||
with self.subTest(view_name=view_name, domain=domain):
|
||||
response = self.client.get(reverse(view_name, kwargs={"pk": domain.id}))
|
||||
response = self.client.get(reverse(view_name, kwargs={"domain_pk": domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
|
@ -271,20 +271,20 @@ class TestDomainDetail(TestDomainOverview):
|
|||
with less_console_noise():
|
||||
self.user.status = User.RESTRICTED
|
||||
self.user.save()
|
||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||
response = self.client.get(reverse("domain", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_domain_detail_allowed_for_on_hold(self):
|
||||
"""Test that the domain overview page displays for on hold domain"""
|
||||
with less_console_noise():
|
||||
# View domain overview page
|
||||
detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id}))
|
||||
detail_page = self.client.get(reverse("domain", kwargs={"domain_pk": self.domain_on_hold.id}))
|
||||
self.assertNotContains(detail_page, "Edit")
|
||||
|
||||
def test_domain_detail_see_just_nameserver(self):
|
||||
with less_console_noise():
|
||||
# View nameserver on Domain Overview page
|
||||
detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id}))
|
||||
detail_page = self.app.get(reverse("domain", kwargs={"domain_pk": self.domain_just_nameserver.id}))
|
||||
|
||||
self.assertContains(detail_page, "justnameserver.com")
|
||||
self.assertContains(detail_page, "ns1.justnameserver.com")
|
||||
|
@ -293,7 +293,7 @@ class TestDomainDetail(TestDomainOverview):
|
|||
def test_domain_detail_see_nameserver_and_ip(self):
|
||||
with less_console_noise():
|
||||
# View nameserver on Domain Overview page
|
||||
detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id}))
|
||||
detail_page = self.app.get(reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}))
|
||||
|
||||
self.assertContains(detail_page, "nameserverwithip.gov")
|
||||
|
||||
|
@ -321,7 +321,7 @@ class TestDomainDetail(TestDomainOverview):
|
|||
session["analyst_action_location"] = self.domain_no_information.id
|
||||
session.save()
|
||||
|
||||
detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_no_information.id}))
|
||||
detail_page = self.client.get(reverse("domain", kwargs={"domain_pk": self.domain_no_information.id}))
|
||||
|
||||
self.assertContains(detail_page, "noinformation.gov")
|
||||
self.assertContains(detail_page, "Domain missing domain information")
|
||||
|
@ -341,7 +341,7 @@ class TestDomainDetail(TestDomainOverview):
|
|||
session["analyst_action_location"] = self.domain.id
|
||||
session.save()
|
||||
|
||||
detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||
detail_page = self.client.get(reverse("domain", kwargs={"domain_pk": self.domain.id}))
|
||||
|
||||
self.assertNotContains(
|
||||
detail_page, "If you need to make updates, contact one of the listed domain managers."
|
||||
|
@ -486,7 +486,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
):
|
||||
self.assertEquals(self.domain_to_renew.state, Domain.State.UNKNOWN)
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": self.domain_to_renew.id}),
|
||||
reverse("domain", kwargs={"domain_pk": self.domain_to_renew.id}),
|
||||
)
|
||||
self.assertContains(detail_page, "Expiring soon")
|
||||
|
||||
|
@ -528,7 +528,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
Domain, "is_expired", self.custom_is_expired_false
|
||||
):
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": domain_to_renew2.id}),
|
||||
reverse("domain", kwargs={"domain_pk": domain_to_renew2.id}),
|
||||
)
|
||||
self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.")
|
||||
|
||||
|
@ -548,7 +548,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
Domain, "is_expired", self.custom_is_expired_false
|
||||
):
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": domain_to_renew3.id}),
|
||||
reverse("domain", kwargs={"domain_pk": domain_to_renew3.id}),
|
||||
)
|
||||
self.assertContains(detail_page, "Renew to maintain access")
|
||||
|
||||
|
@ -561,7 +561,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
):
|
||||
# Grab the detail page
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": self.domain_to_renew.id}),
|
||||
reverse("domain", kwargs={"domain_pk": self.domain_to_renew.id}),
|
||||
)
|
||||
|
||||
# Make sure we see the link as a domain manager
|
||||
|
@ -571,7 +571,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
self.assertContains(detail_page, "Renewal form")
|
||||
|
||||
# Grab link to the renewal page
|
||||
renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domain_to_renew.id})
|
||||
renewal_form_url = reverse("domain-renewal", kwargs={"domain_pk": self.domain_to_renew.id})
|
||||
self.assertContains(detail_page, f'href="{renewal_form_url}"')
|
||||
|
||||
# Simulate clicking the link
|
||||
|
@ -590,7 +590,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
):
|
||||
# Grab the detail page
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": self.domain_to_renew.id}),
|
||||
reverse("domain", kwargs={"domain_pk": self.domain_to_renew.id}),
|
||||
)
|
||||
|
||||
# Make sure we see the link as a domain manager
|
||||
|
@ -600,7 +600,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
self.assertContains(detail_page, "Renewal form")
|
||||
|
||||
# Grab link to the renewal page
|
||||
renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domain_to_renew.id})
|
||||
renewal_form_url = reverse("domain-renewal", kwargs={"domain_pk": self.domain_to_renew.id})
|
||||
self.assertContains(detail_page, f'href="{renewal_form_url}"')
|
||||
|
||||
# Simulate clicking the link
|
||||
|
@ -614,7 +614,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
Your Profile portion of the Renewal Form."""
|
||||
with less_console_noise():
|
||||
# Start on the Renewal page for the domain
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"domain_pk": self.domain_with_ip.id}))
|
||||
|
||||
# Verify we see "Your contact information" on the renewal form
|
||||
self.assertContains(renewal_page, "Your contact information")
|
||||
|
@ -633,7 +633,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
Security Email portion of the Renewal Form."""
|
||||
with less_console_noise():
|
||||
# Start on the Renewal page for the domain
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"domain_pk": self.domain_with_ip.id}))
|
||||
|
||||
# Verify we see "Security email" on the renewal form
|
||||
self.assertContains(renewal_page, "Security email")
|
||||
|
@ -642,7 +642,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
self.assertContains(renewal_page, "We strongly recommend that you provide a security email.")
|
||||
|
||||
# Verify that the "Edit" button for Security email is there and links to correct URL
|
||||
edit_button_url = reverse("domain-security-email", kwargs={"pk": self.domain_with_ip.id})
|
||||
edit_button_url = reverse("domain-security-email", kwargs={"domain_pk": self.domain_with_ip.id})
|
||||
self.assertContains(renewal_page, f'href="{edit_button_url}"')
|
||||
|
||||
# Simulate clicking on edit button
|
||||
|
@ -655,26 +655,26 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
Domain Manager portion of the Renewal Form."""
|
||||
with less_console_noise():
|
||||
# Start on the Renewal page for the domain
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"domain_pk": self.domain_with_ip.id}))
|
||||
|
||||
# Verify we see "Domain managers" on the renewal form
|
||||
self.assertContains(renewal_page, "Domain managers")
|
||||
|
||||
# Verify that the "Edit" button for Domain managers is there and links to correct URL
|
||||
edit_button_url = reverse("domain-users", kwargs={"pk": self.domain_with_ip.id})
|
||||
edit_button_url = reverse("domain-users", kwargs={"domain_pk": self.domain_with_ip.id})
|
||||
self.assertContains(renewal_page, f'href="{edit_button_url}"')
|
||||
|
||||
# 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
|
||||
to access /renewal and that it should receive a 403."""
|
||||
with less_console_noise():
|
||||
# Start on the Renewal page for the domain
|
||||
renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_not_expiring.id}))
|
||||
renewal_page = self.client.get(reverse("domain-renewal", kwargs={"domain_pk": self.domain_not_expiring.id}))
|
||||
self.assertEqual(renewal_page.status_code, 403)
|
||||
|
||||
def test_domain_renewal_form_does_not_appear_if_not_domain_manager(self):
|
||||
|
@ -682,13 +682,15 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object(
|
||||
Domain, "is_expired", self.custom_is_expired_true
|
||||
):
|
||||
renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_no_domain_manager.id}))
|
||||
renewal_page = self.client.get(
|
||||
reverse("domain-renewal", kwargs={"domain_pk": self.domain_no_domain_manager.id})
|
||||
)
|
||||
self.assertEqual(renewal_page.status_code, 403)
|
||||
|
||||
def test_ack_checkbox_not_checked(self):
|
||||
"""If user don't check the checkbox, user should receive an error message."""
|
||||
# Grab the renewal URL
|
||||
renewal_url = reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id})
|
||||
renewal_url = reverse("domain-renewal", kwargs={"domain_pk": self.domain_with_ip.id})
|
||||
|
||||
# Test that the checkbox is not checked
|
||||
response = self.client.post(renewal_url, data={"submit_button": "next"})
|
||||
|
@ -701,17 +703,19 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
user should be redirected Domain Over page with an updated by 1 year expiration date"""
|
||||
# Grab the renewal URL
|
||||
with patch.object(Domain, "renew_domain", self.custom_renew_domain):
|
||||
renewal_url = reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id})
|
||||
renewal_url = reverse("domain-renewal", kwargs={"domain_pk": self.domain_with_ip.id})
|
||||
|
||||
# Click the check, and submit
|
||||
response = self.client.post(renewal_url, data={"is_policy_acknowledged": "on", "submit_button": "next"})
|
||||
|
||||
# Check that it redirects after a successfully submits
|
||||
self.assertRedirects(response, reverse("domain", kwargs={"pk": self.domain_with_ip.id}))
|
||||
self.assertRedirects(response, reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}))
|
||||
|
||||
# Check for the updated expiration
|
||||
formatted_new_expiration_date = self.expiration_date_one_year_out().strftime("%b. %-d, %Y")
|
||||
redirect_response = self.client.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id}), follow=True)
|
||||
redirect_response = self.client.get(
|
||||
reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}), follow=True
|
||||
)
|
||||
self.assertContains(redirect_response, formatted_new_expiration_date)
|
||||
|
||||
|
||||
|
@ -754,28 +758,28 @@ class TestDomainManagers(TestDomainOverview):
|
|||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_managers(self):
|
||||
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||
response = self.client.get(reverse("domain-users", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(response, "Domain managers")
|
||||
self.assertContains(response, "Add a domain manager")
|
||||
# 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)
|
||||
def test_domain_managers_portfolio_view(self):
|
||||
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||
response = self.client.get(reverse("domain-users", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(response, "Domain managers")
|
||||
self.assertContains(response, "Add a domain manager")
|
||||
# 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):
|
||||
response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
response = self.client.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(response, "Add a domain manager")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -784,7 +788,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
"""Adding an existing user works."""
|
||||
get_user_model().objects.get_or_create(email="mayor@igorville.gov")
|
||||
user = User.objects.filter(email="mayor@igorville.gov").first()
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
add_page.form["email"] = "mayor@igorville.gov"
|
||||
|
@ -804,7 +808,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self.assertEqual(success_result.status_code, 302)
|
||||
self.assertEqual(
|
||||
success_result["Location"],
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-users", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -820,7 +824,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
"""Adding an existing user works and sends portfolio invitation when
|
||||
user is not member of portfolio."""
|
||||
get_user_model().objects.get_or_create(email="mayor@igorville.gov")
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
add_page.form["email"] = "mayor@igorville.gov"
|
||||
|
@ -832,7 +836,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self.assertEqual(success_result.status_code, 302)
|
||||
self.assertEqual(
|
||||
success_result["Location"],
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-users", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
|
||||
# Verify that the invitation emails were sent
|
||||
|
@ -877,7 +881,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self, mock_send_domain_email, mock_send_portfolio_email
|
||||
):
|
||||
"""Adding an email not associated with a user works and sends portfolio invitation."""
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
add_page.form["email"] = "notauser@igorville.gov"
|
||||
|
@ -889,7 +893,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self.assertEqual(success_result.status_code, 302)
|
||||
self.assertEqual(
|
||||
success_result["Location"],
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-users", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
|
||||
# Verify that the invitation emails were sent
|
||||
|
@ -928,7 +932,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
):
|
||||
"""Adding an email not associated with a user works and sends portfolio invitation,
|
||||
and when domain managers email(s) fail to send, assert proper warning displayed."""
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
add_page.form["email"] = "notauser@igorville.gov"
|
||||
|
@ -942,7 +946,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self.assertEqual(success_result.status_code, 302)
|
||||
self.assertEqual(
|
||||
success_result["Location"],
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-users", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
|
||||
# Verify that the invitation emails were sent
|
||||
|
@ -967,7 +971,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=other_user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
add_page.form["email"] = "mayor@igorville.gov"
|
||||
|
@ -979,7 +983,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self.assertEqual(success_result.status_code, 302)
|
||||
self.assertEqual(
|
||||
success_result["Location"],
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-users", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
|
||||
# Verify that the invitation emails were sent
|
||||
|
@ -1015,7 +1019,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
user is not member of portfolio and send raises an error."""
|
||||
mock_send_portfolio_email.side_effect = EmailSendingError("Failed to send email.")
|
||||
get_user_model().objects.get_or_create(email="mayor@igorville.gov")
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
add_page.form["email"] = "mayor@igorville.gov"
|
||||
|
@ -1027,7 +1031,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self.assertEqual(success_result.status_code, 302)
|
||||
self.assertEqual(
|
||||
success_result["Location"],
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-users", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
|
||||
# Verify that the invitation emails were sent
|
||||
|
@ -1058,7 +1062,9 @@ class TestDomainManagers(TestDomainOverview):
|
|||
"""Removing a domain manager sends notification email to other domain managers."""
|
||||
self.manager, _ = User.objects.get_or_create(email="mayor@igorville.com", first_name="Hello", last_name="World")
|
||||
self.manager_domain_permission, _ = UserDomainRole.objects.get_or_create(user=self.manager, domain=self.domain)
|
||||
self.client.post(reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.manager.id}))
|
||||
self.client.post(
|
||||
reverse("domain-user-delete", kwargs={"domain_pk": self.domain.id, "user_pk": self.manager.id})
|
||||
)
|
||||
|
||||
# Verify that the notification emails were sent to domain manager
|
||||
mock_send_templated_email.assert_called_once_with(
|
||||
|
@ -1082,7 +1088,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = email_address
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1115,7 +1121,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = caps_email_address
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1147,7 +1153,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = email_address
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1175,7 +1181,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = email_address
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1214,7 +1220,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = email_address
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1258,7 +1264,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = email_address
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1289,7 +1295,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
email_address = "mayor"
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = email_address
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1313,7 +1319,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||
|
||||
with patch("django.contrib.messages.error") as mock_error:
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = email_address
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1332,7 +1338,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
"""Posting to the delete view deletes an invitation."""
|
||||
email_address = "mayor@igorville.gov"
|
||||
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
|
||||
self.client.post(reverse("invitation-cancel", kwargs={"pk": invitation.id}))
|
||||
self.client.post(reverse("invitation-cancel", kwargs={"domain_invitation_pk": invitation.id}))
|
||||
invitation = DomainInvitation.objects.get(id=invitation.id)
|
||||
self.assertEqual(invitation.status, DomainInvitation.DomainInvitationStatus.CANCELED)
|
||||
|
||||
|
@ -1343,7 +1349,9 @@ class TestDomainManagers(TestDomainOverview):
|
|||
invitation, _ = DomainInvitation.objects.get_or_create(
|
||||
domain=self.domain, email=email_address, status=DomainInvitation.DomainInvitationStatus.RETRIEVED
|
||||
)
|
||||
response = self.client.post(reverse("invitation-cancel", kwargs={"pk": invitation.id}), follow=True)
|
||||
response = self.client.post(
|
||||
reverse("invitation-cancel", kwargs={"domain_invitation_pk": invitation.id}), follow=True
|
||||
)
|
||||
# Assert that an error message is displayed to the user
|
||||
self.assertContains(response, f"Invitation to {email_address} has already been retrieved.")
|
||||
# Assert that the Cancel link (form) is not displayed
|
||||
|
@ -1363,7 +1371,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
self.client.force_login(other_user)
|
||||
mock_client = MagicMock()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
result = self.client.post(reverse("invitation-cancel", kwargs={"pk": invitation.id}))
|
||||
result = self.client.post(reverse("invitation-cancel", kwargs={"domain_invitation_pk": invitation.id}))
|
||||
|
||||
self.assertEqual(result.status_code, 403)
|
||||
|
||||
|
@ -1380,7 +1388,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
title = "title"
|
||||
User.objects.filter(email=email_address).delete()
|
||||
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"domain_pk": self.domain.id}))
|
||||
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||
|
||||
|
@ -1418,7 +1426,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
)
|
||||
UserDomainRole.objects.create(user=new_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
||||
response = self.client.post(
|
||||
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": new_user.id}), follow=True
|
||||
reverse("domain-user-delete", kwargs={"domain_pk": self.domain.id, "user_pk": new_user.id}), follow=True
|
||||
)
|
||||
# Assert that a success message is displayed to the user
|
||||
self.assertContains(response, f"Removed {email_address} as a manager for this domain.")
|
||||
|
@ -1432,7 +1440,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
"""Posting to the delete view attempts to delete a user domain role when there is only one manager."""
|
||||
# self.user is the only domain manager, so attempt to delete it
|
||||
response = self.client.post(
|
||||
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True
|
||||
reverse("domain-user-delete", kwargs={"domain_pk": self.domain.id, "user_pk": self.user.id}), follow=True
|
||||
)
|
||||
# Assert that an error message is displayed to the user
|
||||
self.assertContains(response, "Domains must have at least one domain manager.")
|
||||
|
@ -1449,7 +1457,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
new_user = User.objects.create(email=email_address, username="mayor")
|
||||
UserDomainRole.objects.create(user=new_user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
||||
response = self.client.post(
|
||||
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True
|
||||
reverse("domain-user-delete", kwargs={"domain_pk": self.domain.id, "user_pk": self.user.id}), follow=True
|
||||
)
|
||||
# Assert that a success message is displayed to the user
|
||||
self.assertContains(response, f"You are no longer managing the domain {self.domain}.")
|
||||
|
@ -1461,7 +1469,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
@less_console_noise_decorator
|
||||
def test_domain_nameservers(self):
|
||||
"""Can load domain's nameservers page."""
|
||||
page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
page = self.client.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(page, "DNS name servers")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1471,7 +1479,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form with only one nameserver, should error
|
||||
|
@ -1494,7 +1502,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without two hosts, both subdomains,
|
||||
|
@ -1519,7 +1527,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without two hosts, both subdomains,
|
||||
|
@ -1543,7 +1551,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form with duplicate host names of fake.host.com
|
||||
|
@ -1571,7 +1579,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
valid_ip = "1.1. 1.1"
|
||||
valid_ip_2 = "2.2. 2.2"
|
||||
# have to throw an error in order to test that the whitespace has been stripped from ip
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without one host and an ip with whitespace
|
||||
|
@ -1585,7 +1593,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
|
@ -1604,7 +1612,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
nameserver2 = "ns2.igorville.com"
|
||||
valid_ip = "127.0.0.1"
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without two hosts, both subdomains,
|
||||
|
@ -1632,7 +1640,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
nameserver = "ns2.igorville.gov"
|
||||
invalid_ip = "123"
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without two hosts, both subdomains,
|
||||
|
@ -1659,7 +1667,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
nameserver = "invalid-nameserver.gov"
|
||||
valid_ip = "123.2.45.111"
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without two hosts, both subdomains,
|
||||
|
@ -1687,7 +1695,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
nameserver2 = "ns2.igorville.gov"
|
||||
valid_ip = "127.0.0.1"
|
||||
valid_ip_2 = "128.0.0.2"
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
|
@ -1699,7 +1707,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
|
@ -1719,7 +1727,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
valid_ip = ""
|
||||
valid_ip_2 = "128.0.0.2"
|
||||
valid_ip_3 = "128.0.0.3"
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
|
@ -1734,7 +1742,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page = result.follow()
|
||||
|
@ -1760,7 +1768,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page = result.follow()
|
||||
|
@ -1785,7 +1793,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
valid_ip_3 = ""
|
||||
valid_ip_4 = ""
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id})
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_four_nameservers.id})
|
||||
)
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
@ -1809,7 +1817,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_four_nameservers.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page = result.follow()
|
||||
|
@ -1821,7 +1829,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
|
@ -1843,7 +1851,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
|
|||
@less_console_noise_decorator
|
||||
def test_domain_senior_official(self):
|
||||
"""Can load domain's senior official page."""
|
||||
page = self.client.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
|
||||
page = self.client.get(reverse("domain-senior-official", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(page, "Senior official", count=4)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1852,7 +1860,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
|
|||
self.domain_information.senior_official = Contact(first_name="Testy")
|
||||
self.domain_information.senior_official.save()
|
||||
self.domain_information.save()
|
||||
page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
|
||||
page = self.app.get(reverse("domain-senior-official", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(page, "Testy")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1864,7 +1872,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
|
|||
)
|
||||
self.domain_information.senior_official.save()
|
||||
self.domain_information.save()
|
||||
so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
|
||||
so_page = self.app.get(reverse("domain-senior-official", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
so_form = so_page.forms[0]
|
||||
|
@ -1922,7 +1930,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
|
|||
self.domain_information.senior_official.save()
|
||||
self.domain_information.save()
|
||||
|
||||
so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
|
||||
so_page = self.app.get(reverse("domain-senior-official", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(so_page, "Apple Tester")
|
||||
self.assertContains(so_page, "CIO")
|
||||
self.assertContains(so_page, "nobody@igorville.gov")
|
||||
|
@ -1943,7 +1951,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
|
|||
self.domain_information.senior_official.save()
|
||||
self.domain_information.save()
|
||||
|
||||
so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
|
||||
so_page = self.app.get(reverse("domain-senior-official", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(so_page, "Apple Tester")
|
||||
self.assertContains(so_page, "CIO")
|
||||
self.assertContains(so_page, "nobody@igorville.gov")
|
||||
|
@ -1962,7 +1970,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
|
|||
self.domain_information.other_contacts.add(self.domain_information.senior_official)
|
||||
self.domain_information.save()
|
||||
# load the Senior Official in the web form
|
||||
so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
|
||||
so_page = self.app.get(reverse("domain-senior-official", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
so_form = so_page.forms[0]
|
||||
|
@ -1990,7 +1998,7 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
@less_console_noise_decorator
|
||||
def test_domain_org_name_address(self):
|
||||
"""Can load domain's org name and mailing address page."""
|
||||
page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
page = self.client.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
||||
# once on the sidebar, once in the page title, once as H1
|
||||
self.assertContains(page, "/org-name-address")
|
||||
self.assertContains(page, "Organization name and mailing address")
|
||||
|
@ -2001,7 +2009,7 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
"""Org name and address information appears on the page."""
|
||||
self.domain_information.organization_name = "Town of Igorville"
|
||||
self.domain_information.save()
|
||||
page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(page, "Town of Igorville")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -2009,7 +2017,7 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
"""Submitting changes works on the org name address page."""
|
||||
self.domain_information.organization_name = "Town of Igorville"
|
||||
self.domain_information.save()
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
org_name_page.form["organization_name"] = "Not igorville"
|
||||
|
@ -2041,7 +2049,7 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
|
||||
self.assertEqual(self.domain_information.generic_org_type, tribal_org_type)
|
||||
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
||||
|
||||
form = org_name_page.forms[0]
|
||||
# Check the value of the input field
|
||||
|
@ -2098,7 +2106,7 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
|
||||
self.assertEqual(self.domain_information.generic_org_type, fed_org_type)
|
||||
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
||||
|
||||
form = org_name_page.forms[0]
|
||||
# Check the value of the input field
|
||||
|
@ -2160,7 +2168,7 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
|
||||
new_value = ("Department of State", "Department of State")
|
||||
self.client.post(
|
||||
reverse("domain-org-name-address", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}),
|
||||
{
|
||||
"federal_agency": new_value,
|
||||
},
|
||||
|
@ -2202,7 +2210,7 @@ class TestDomainSuborganization(TestDomainOverview):
|
|||
self.assertEqual(self.domain_information.sub_organization, suborg)
|
||||
|
||||
# Navigate to the suborganization page
|
||||
page = self.app.get(reverse("domain-suborganization", kwargs={"pk": self.domain.id}))
|
||||
page = self.app.get(reverse("domain-suborganization", kwargs={"domain_pk": self.domain.id}))
|
||||
|
||||
# The page should contain the choices Vanilla and Chocolate
|
||||
self.assertContains(page, "Vanilla")
|
||||
|
@ -2260,7 +2268,7 @@ class TestDomainSuborganization(TestDomainOverview):
|
|||
self.assertEqual(self.domain_information.sub_organization, suborg)
|
||||
|
||||
# Navigate to the suborganization page
|
||||
page = self.app.get(reverse("domain-suborganization", kwargs={"pk": self.domain.id}))
|
||||
page = self.app.get(reverse("domain-suborganization", kwargs={"domain_pk": self.domain.id}))
|
||||
|
||||
# The page should display the readonly option
|
||||
self.assertContains(page, "Vanilla")
|
||||
|
@ -2299,7 +2307,7 @@ class TestDomainSuborganization(TestDomainOverview):
|
|||
self.user.refresh_from_db()
|
||||
|
||||
# Navigate to the domain overview page
|
||||
page = self.app.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||
page = self.app.get(reverse("domain", kwargs={"domain_pk": self.domain.id}))
|
||||
|
||||
# Test for the title change
|
||||
self.assertContains(page, "Suborganization")
|
||||
|
@ -2328,7 +2336,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov")
|
||||
# Add current user to this domain
|
||||
_ = UserDomainRole(user=self.user, domain=domain_contact, role="admin").save()
|
||||
page = self.client.get(reverse("domain-security-email", kwargs={"pk": domain_contact.id}))
|
||||
page = self.client.get(reverse("domain-security-email", kwargs={"domain_pk": domain_contact.id}))
|
||||
|
||||
# Loads correctly
|
||||
self.assertContains(page, "Security email")
|
||||
|
@ -2343,7 +2351,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
self.mockedSendFunction = self.mockSendPatch.start()
|
||||
self.mockedSendFunction.side_effect = self.mockSend
|
||||
|
||||
page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id}))
|
||||
page = self.client.get(reverse("domain-security-email", kwargs={"domain_pk": self.domain.id}))
|
||||
|
||||
# Loads correctly
|
||||
self.assertContains(page, "Security email")
|
||||
|
@ -2353,7 +2361,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
def test_domain_security_email(self):
|
||||
"""Can load domain's security email page."""
|
||||
with less_console_noise():
|
||||
page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id}))
|
||||
page = self.client.get(reverse("domain-security-email", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(page, "Security email")
|
||||
|
||||
def test_domain_security_email_form(self):
|
||||
|
@ -2361,7 +2369,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
with less_console_noise():
|
||||
security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id}))
|
||||
security_email_page = self.app.get(reverse("domain-security-email", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
security_email_page.form["security_email"] = "mayor@igorville.gov"
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -2372,7 +2380,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-security-email", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-security-email", kwargs={"domain_pk": self.domain.id}),
|
||||
)
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -2415,7 +2423,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
]
|
||||
for test_name, data, expected_message in test_cases:
|
||||
response = self.client.post(
|
||||
reverse("domain-security-email", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-security-email", kwargs={"domain_pk": self.domain.id}),
|
||||
data=data,
|
||||
follow=True,
|
||||
)
|
||||
|
@ -2443,7 +2451,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
management pages share the same permissions class"""
|
||||
self.user.status = User.RESTRICTED
|
||||
self.user.save()
|
||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||
response = self.client.get(reverse("domain", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
|
@ -2455,7 +2463,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
"""DNSSEC overview page loads when domain has no DNSSEC data
|
||||
and shows a 'Enable DNSSEC' button."""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}))
|
||||
page = self.client.get(reverse("domain-dns-dnssec", kwargs={"domain_pk": self.domain.id}))
|
||||
self.assertContains(page, "Enable DNSSEC")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -2463,7 +2471,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
"""DNSSEC overview page loads when domain has DNSSEC data
|
||||
and the template contains a button to disable DNSSEC."""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id}))
|
||||
page = self.client.get(reverse("domain-dns-dnssec", kwargs={"domain_pk": self.domain_multdsdata.id}))
|
||||
self.assertContains(page, "Disable DNSSEC")
|
||||
|
||||
# Prepare the data for the POST request
|
||||
|
@ -2471,7 +2479,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
"disable_dnssec": "Disable DNSSEC",
|
||||
}
|
||||
updated_page = self.client.post(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-dns-dnssec", kwargs={"domain_pk": self.domain.id}),
|
||||
post_data,
|
||||
follow=True,
|
||||
)
|
||||
|
@ -2485,7 +2493,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
"""DNSSEC Add DS data page loads when there is no
|
||||
domain DNSSEC data and shows a button to Add new record"""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id}))
|
||||
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dnssec_none.id}))
|
||||
self.assertContains(page, "You have no DS data added")
|
||||
self.assertContains(page, "Add new record")
|
||||
|
||||
|
@ -2494,13 +2502,13 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
"""DNSSEC Add DS data page loads when there is
|
||||
domain DNSSEC DS data and shows the data"""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
self.assertContains(page, "DS data record 1")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_ds_data_form_modal(self):
|
||||
"""When user clicks on save, a modal pops up."""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
# Assert that a hidden trigger for the modal does not exist.
|
||||
# This hidden trigger will pop on the page when certain condition are met:
|
||||
# 1) Initial form contained DS data, 2) All data is deleted and form is
|
||||
|
@ -2509,7 +2517,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
# Simulate a delete all data
|
||||
form_data = {}
|
||||
response = self.client.post(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}),
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}),
|
||||
data=form_data,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200) # Adjust status code as needed
|
||||
|
@ -2522,7 +2530,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
result = add_data_page.forms[0].submit()
|
||||
|
@ -2530,7 +2538,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}),
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
|
@ -2542,7 +2550,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# all four form fields are required, so will test with each blank
|
||||
|
@ -2565,7 +2573,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
|
@ -2588,7 +2596,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
|
@ -2611,7 +2619,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
|
@ -2634,7 +2642,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
|
@ -2688,7 +2696,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
self.domain_information.zipcode = "62052"
|
||||
self.domain_information.save()
|
||||
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
org_name_page.form["organization_name"] = "Not igorville"
|
||||
|
@ -2729,7 +2737,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
self.domain_information.portfolio = portfolio
|
||||
self.domain_information.save()
|
||||
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
org_name_page.form["organization_name"] = "Not igorville"
|
||||
|
@ -2756,7 +2764,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
self.domain_information.portfolio = portfolio
|
||||
self.domain_information.save()
|
||||
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
session = self.app.session
|
||||
|
@ -2778,7 +2786,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
def test_notification_on_security_email_change(self):
|
||||
"""Test that an email is sent when the security email is changed."""
|
||||
|
||||
security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id}))
|
||||
security_email_page = self.app.get(reverse("domain-security-email", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
security_email_page.form["security_email"] = "new_security@example.com"
|
||||
|
@ -2801,7 +2809,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
def test_notification_on_dnssec_enable(self):
|
||||
"""Test that an email is sent when DNSSEC is enabled."""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id}))
|
||||
page = self.client.get(reverse("domain-dns-dnssec", kwargs={"domain_pk": self.domain_multdsdata.id}))
|
||||
self.assertContains(page, "Disable DNSSEC")
|
||||
|
||||
# Prepare the data for the POST request
|
||||
|
@ -2811,7 +2819,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
updated_page = self.client.post(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-dns-dnssec", kwargs={"domain_pk": self.domain.id}),
|
||||
post_data,
|
||||
follow=True,
|
||||
)
|
||||
|
@ -2834,7 +2842,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
def test_notification_on_ds_data_change(self):
|
||||
"""Test that an email is sent when DS data is changed."""
|
||||
|
||||
ds_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain.id}))
|
||||
ds_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
# Add DS data
|
||||
|
@ -2868,7 +2876,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
)
|
||||
self.domain_information.save()
|
||||
|
||||
senior_official_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
|
||||
senior_official_page = self.app.get(reverse("domain-senior-official", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
senior_official_page.form["first_name"] = "New"
|
||||
|
@ -2905,7 +2913,7 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
self.domain_information.portfolio = portfolio
|
||||
self.domain_information.save()
|
||||
|
||||
senior_official_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
|
||||
senior_official_page = self.app.get(reverse("domain-senior-official", kwargs={"domain_pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
senior_official_page.form["first_name"] = "New"
|
||||
|
@ -2924,7 +2932,9 @@ class TestDomainChangeNotifications(TestDomainOverview):
|
|||
def test_no_notification_when_dns_needed(self):
|
||||
"""Test that an email is not sent when nameservers are changed while the state is DNS_NEEDED."""
|
||||
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain_dns_needed.id}))
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_dns_needed.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
# add nameservers
|
||||
|
|
|
@ -104,7 +104,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
self.assertEqual(expected_domain.state_display(), state_displays[i])
|
||||
self.assertEqual(expected_domain.get_state_help_text(), get_state_help_texts[i])
|
||||
|
||||
self.assertEqual(reverse("domain", kwargs={"pk": expected_domain.id}), action_urls[i])
|
||||
self.assertEqual(reverse("domain", kwargs={"domain_pk": expected_domain.id}), action_urls[i])
|
||||
|
||||
# Check action_label
|
||||
action_label_expected = (
|
||||
|
@ -185,7 +185,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
self.assertEqual(expected_domain.state_display(), state_displays[i])
|
||||
self.assertEqual(expected_domain.get_state_help_text(), get_state_help_texts[i])
|
||||
|
||||
self.assertEqual(reverse("domain", kwargs={"pk": expected_domain.id}), action_urls[i])
|
||||
self.assertEqual(reverse("domain", kwargs={"domain_pk": expected_domain.id}), action_urls[i])
|
||||
|
||||
# Check action_label
|
||||
user_domain_role_exists = UserDomainRole.objects.filter(
|
||||
|
@ -272,7 +272,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
self.assertEqual(expected_domain.state_display(), state_displays[i])
|
||||
self.assertEqual(expected_domain.get_state_help_text(), get_state_help_texts[i])
|
||||
|
||||
self.assertEqual(reverse("domain", kwargs={"pk": expected_domain.id}), action_urls[i])
|
||||
self.assertEqual(reverse("domain", kwargs={"domain_pk": expected_domain.id}), action_urls[i])
|
||||
|
||||
# Check action_label
|
||||
user_domain_role_exists = UserDomainRole.objects.filter(
|
||||
|
|
|
@ -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
|
||||
|
@ -1320,7 +1316,9 @@ class TestPortfolio(WebTest):
|
|||
|
||||
self.client.force_login(self.user)
|
||||
# Perform delete
|
||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||
response = self.client.post(
|
||||
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}), follow=True
|
||||
)
|
||||
|
||||
# Check that the response is 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -1354,7 +1352,9 @@ class TestPortfolio(WebTest):
|
|||
|
||||
self.client.force_login(self.user)
|
||||
# Attempt to delete
|
||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||
response = self.client.post(
|
||||
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}), follow=True
|
||||
)
|
||||
|
||||
# Check response is 403 Forbidden
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
@ -1389,7 +1389,9 @@ class TestPortfolio(WebTest):
|
|||
|
||||
self.client.force_login(self.user)
|
||||
# Perform delete as self.user
|
||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||
response = self.client.post(
|
||||
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}), follow=True
|
||||
)
|
||||
|
||||
# Check response is 403 Forbidden
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
@ -1669,7 +1671,8 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_delete_view_members_table_active_requests(self, send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_delete_view_members_table_active_requests(self, send_member_removal, send_removal_emails):
|
||||
"""Error state w/ deleting a member with active request on Members Table"""
|
||||
# I'm a user
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
|
@ -1709,12 +1712,15 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# assert that send_portfolio_admin_removal_emails is not called
|
||||
send_removal_emails.assert_not_called()
|
||||
# assert that send_portfolio_member_permission_remove_email is not called
|
||||
send_member_removal.assert_not_called()
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_delete_view_members_table_only_admin(self, send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_delete_view_members_table_only_admin(self, send_member_removal, send_removal_emails):
|
||||
"""Error state w/ deleting a member that's the only admin on Members Table"""
|
||||
|
||||
# I'm a user with admin permission
|
||||
|
@ -1744,12 +1750,15 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# assert that send_portfolio_admin_removal_emails is not called
|
||||
send_removal_emails.assert_not_called()
|
||||
# assert that send_portfolio_member_permission_remove_email is not called
|
||||
send_member_removal.assert_not_called()
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_table_delete_member_success(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_table_delete_member_success(self, send_member_removal, mock_send_removal_emails):
|
||||
"""Success state with deleting on Members Table page bc no active request AND not only admin"""
|
||||
|
||||
# I'm a user
|
||||
|
@ -1774,6 +1783,9 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||
)
|
||||
|
||||
# Member removal email sent successfully
|
||||
send_member_removal.return_value = True
|
||||
|
||||
# And set that the member has no active requests AND it's not the only admin
|
||||
with patch.object(User, "get_active_requests_count_in_portfolio", return_value=0), patch.object(
|
||||
User, "is_only_admin_of_portfolio", return_value=False
|
||||
|
@ -1796,12 +1808,23 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
# assert that send_portfolio_admin_removal_emails is not called
|
||||
# because member being removed is not an admin
|
||||
mock_send_removal_emails.assert_not_called()
|
||||
# assert that send_portfolio_member_permission_remove_email is called
|
||||
send_member_removal.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_member_permission_remove_email
|
||||
_, called_kwargs = send_member_removal.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["permissions"].user, upp.user)
|
||||
self.assertEqual(called_kwargs["permissions"].portfolio, upp.portfolio)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_table_delete_admin_success(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_table_delete_admin_success(self, send_member_removal, mock_send_removal_emails):
|
||||
"""Success state with deleting on Members Table page bc no active request AND
|
||||
not only admin. Because admin, removal emails are sent."""
|
||||
|
||||
|
@ -1828,6 +1851,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
)
|
||||
|
||||
mock_send_removal_emails.return_value = True
|
||||
send_member_removal.return_value = True
|
||||
|
||||
# And set that the member has no active requests AND it's not the only admin
|
||||
with patch.object(User, "get_active_requests_count_in_portfolio", return_value=0), patch.object(
|
||||
|
@ -1850,6 +1874,8 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# assert that send_portfolio_admin_removal_emails is called
|
||||
mock_send_removal_emails.assert_called_once()
|
||||
# assert that send_portfolio_member_permission_remove_email is called
|
||||
send_member_removal.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_admin_addition_emails
|
||||
_, called_kwargs = mock_send_removal_emails.call_args
|
||||
|
@ -1859,13 +1885,25 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
|
||||
|
||||
# Get the arguments passed to send_portfolio_member_permission_remove_email
|
||||
_, called_kwargs = send_member_removal.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["permissions"].user, upp.user)
|
||||
self.assertEqual(called_kwargs["permissions"].portfolio, upp.portfolio)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_table_delete_admin_success_removal_email_fail(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_table_delete_admin_success_removal_email_fail(
|
||||
self, send_member_removal, mock_send_removal_emails
|
||||
):
|
||||
"""Success state with deleting on Members Table page bc no active request AND
|
||||
not only admin. Because admin, removal emails are sent, but fail to send."""
|
||||
not only admin. Because admin, removal emails are sent, but fail to send.
|
||||
Email to removed member also fails to send."""
|
||||
|
||||
# I'm a user
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
|
@ -1890,6 +1928,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
)
|
||||
|
||||
mock_send_removal_emails.return_value = False
|
||||
send_member_removal.return_value = False
|
||||
|
||||
# And set that the member has no active requests AND it's not the only admin
|
||||
with patch.object(User, "get_active_requests_count_in_portfolio", return_value=0), patch.object(
|
||||
|
@ -1912,6 +1951,8 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# assert that send_portfolio_admin_removal_emails is called
|
||||
mock_send_removal_emails.assert_called_once()
|
||||
# assert that send_portfolio_member_permission_remove_email is called
|
||||
send_member_removal.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_admin_addition_emails
|
||||
_, called_kwargs = mock_send_removal_emails.call_args
|
||||
|
@ -1921,6 +1962,14 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
|
||||
|
||||
# Get the arguments passed to send_portfolio_member_permission_remove_email
|
||||
_, called_kwargs = send_member_removal.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["permissions"].user, upp.user)
|
||||
self.assertEqual(called_kwargs["permissions"].portfolio, upp.portfolio)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
|
@ -2045,7 +2094,10 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_delete_view_manage_members_page_invitedmember(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_invitation_remove_email")
|
||||
def test_portfolio_member_delete_view_manage_members_page_invitedmember(
|
||||
self, send_invited_member_removal, mock_send_removal_emails
|
||||
):
|
||||
"""Success state w/ deleting invited member on Manage Members page should redirect back to Members Table"""
|
||||
|
||||
# I'm a user
|
||||
|
@ -2066,6 +2118,10 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||
)
|
||||
|
||||
# Invited member removal email sent successfully
|
||||
send_invited_member_removal.return_value = True
|
||||
|
||||
with patch("django.contrib.messages.success") as mock_success:
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
|
@ -2089,12 +2145,25 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
# assert send_portfolio_admin_removal_emails not called since invitation
|
||||
# is for a basic member
|
||||
mock_send_removal_emails.assert_not_called()
|
||||
# assert that send_portfolio_invitation_remove_email is called
|
||||
send_invited_member_removal.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_invitation_removal_email
|
||||
_, called_kwargs = send_invited_member_removal.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["invitation"].email, invitation.email)
|
||||
self.assertEqual(called_kwargs["invitation"].portfolio, invitation.portfolio)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_delete_view_manage_members_page_invitedadmin(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_invitation_remove_email")
|
||||
def test_portfolio_member_delete_view_manage_members_page_invitedadmin(
|
||||
self, send_invited_member_email, mock_send_removal_emails
|
||||
):
|
||||
"""Success state w/ deleting invited admin on Manage Members page should redirect back to Members Table"""
|
||||
|
||||
# I'm a user
|
||||
|
@ -2109,6 +2178,7 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
)
|
||||
|
||||
mock_send_removal_emails.return_value = True
|
||||
send_invited_member_email.return_value = True
|
||||
|
||||
# Invite an admin under same portfolio
|
||||
invited_member_email = "invited_member@example.com"
|
||||
|
@ -2140,6 +2210,8 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
# assert send_portfolio_admin_removal_emails is called since invitation
|
||||
# is for an admin
|
||||
mock_send_removal_emails.assert_called_once()
|
||||
# assert that send_portfolio_invitation_remove_email is called
|
||||
send_invited_member_email.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_admin_addition_emails
|
||||
_, called_kwargs = mock_send_removal_emails.call_args
|
||||
|
@ -2149,11 +2221,22 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
|
||||
|
||||
# Get the arguments passed to send_portfolio_invitation_remove_email
|
||||
_, called_kwargs = send_invited_member_email.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["invitation"].email, invitation.email)
|
||||
self.assertEqual(called_kwargs["invitation"].portfolio, invitation.portfolio)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_delete_view_manage_members_page_invitedadmin_email_fails(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_invitation_remove_email")
|
||||
def test_portfolio_member_delete_view_manage_members_page_invitedadmin_email_fails(
|
||||
self, send_invited_member_email, mock_send_removal_emails
|
||||
):
|
||||
"""Success state w/ deleting invited admin on Manage Members page should redirect back to Members Table"""
|
||||
|
||||
# I'm a user
|
||||
|
@ -2168,6 +2251,7 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
)
|
||||
|
||||
mock_send_removal_emails.return_value = False
|
||||
send_invited_member_email.return_value = False
|
||||
|
||||
# Invite an admin under same portfolio
|
||||
invited_member_email = "invited_member@example.com"
|
||||
|
@ -2199,6 +2283,8 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
# assert send_portfolio_admin_removal_emails is called since invitation
|
||||
# is for an admin
|
||||
mock_send_removal_emails.assert_called_once()
|
||||
# assert that send_portfolio_invitation_remove_email is called
|
||||
send_invited_member_email.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_admin_addition_emails
|
||||
_, called_kwargs = mock_send_removal_emails.call_args
|
||||
|
@ -2208,6 +2294,14 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
|
||||
|
||||
# Get the arguments passed to send_portfolio_invitation_remove_email
|
||||
_, called_kwargs = send_invited_member_email.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["invitation"].email, invitation.email)
|
||||
self.assertEqual(called_kwargs["invitation"].portfolio, invitation.portfolio)
|
||||
|
||||
|
||||
class TestPortfolioMemberDomainsView(TestWithUser, WebTest):
|
||||
@classmethod
|
||||
|
@ -2646,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, {})
|
||||
|
@ -2658,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)
|
||||
|
@ -2987,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)
|
||||
|
@ -3244,7 +3338,9 @@ class TestRequestingEntity(WebTest):
|
|||
def test_requesting_entity_page_errors(self):
|
||||
"""Tests that we get the expected form errors on requesting entity"""
|
||||
domain_request = completed_domain_request(user=self.user, portfolio=self.portfolio)
|
||||
response = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
|
||||
response = self.app.get(
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk})
|
||||
).follow()
|
||||
form = response.forms[0]
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -3334,7 +3430,9 @@ class TestRequestingEntity(WebTest):
|
|||
|
||||
domain_request.submit()
|
||||
|
||||
response = self.app.get(reverse("domain-request-status-viewonly", kwargs={"pk": domain_request.pk}))
|
||||
response = self.app.get(
|
||||
reverse("domain-request-status-viewonly", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.assertContains(response, "Requesting entity")
|
||||
self.assertContains(response, "moon")
|
||||
self.assertContains(response, "kepler, AL")
|
||||
|
@ -3359,7 +3457,7 @@ class TestRequestingEntity(WebTest):
|
|||
|
||||
domain_request.submit()
|
||||
|
||||
response = self.app.get(reverse("domain-request-status", kwargs={"pk": domain_request.pk}))
|
||||
response = self.app.get(reverse("domain-request-status", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
self.assertContains(response, "Requesting entity")
|
||||
self.assertContains(response, "moon")
|
||||
self.assertContains(response, "kepler, AL")
|
||||
|
|
|
@ -809,7 +809,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# type_page = home_page.click("Edit")
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
url = reverse("edit-domain-request", kwargs={"id": domain_request.pk})
|
||||
url = reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk})
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# TODO: The following line results in a django error on middleware
|
||||
|
@ -1106,7 +1106,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
|
||||
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
|
||||
new domain requests"""
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": 0}))
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"domain_request_pk": 0}))
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
|
||||
|
||||
|
@ -1114,7 +1114,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
def test_yes_no_additional_form_inits_blank_for_new_domain_request(self):
|
||||
"""On the Additional Details page, the yes/no form gets initialized with nothing selected for
|
||||
new domain requests"""
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details", kwargs={"id": 0}))
|
||||
additional_details_page = self.app.get(
|
||||
reverse("domain-request:additional_details", kwargs={"domain_request_pk": 0})
|
||||
)
|
||||
additional_form = additional_details_page.forms[0]
|
||||
|
||||
# Check the cisa representative yes/no field
|
||||
|
@ -1130,7 +1132,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# Domain Request has other contacts by default
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1138,7 +1140,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.pk}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -1155,7 +1159,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request.save()
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1164,7 +1168,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(
|
||||
reverse("domain-request:additional_details", kwargs={"id": domain_request.pk})
|
||||
reverse("domain-request:additional_details", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
|
@ -1187,7 +1191,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request.no_other_contacts_rationale = "Hello!"
|
||||
domain_request.save()
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1195,7 +1199,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.pk}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -1216,7 +1222,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request.save()
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1225,7 +1231,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(
|
||||
reverse("domain-request:additional_details", kwargs={"id": domain_request.pk})
|
||||
reverse("domain-request:additional_details", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
|
@ -1256,7 +1262,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov")
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1265,7 +1271,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(
|
||||
reverse("domain-request:additional_details", kwargs={"id": domain_request.pk})
|
||||
reverse("domain-request:additional_details", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
|
@ -1320,7 +1326,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.assertEqual(domain_request.has_cisa_representative, None)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1329,7 +1335,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(
|
||||
reverse("domain-request:additional_details", kwargs={"id": domain_request.pk})
|
||||
reverse("domain-request:additional_details", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
|
@ -1367,7 +1373,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1376,7 +1382,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(
|
||||
reverse("domain-request:additional_details", kwargs={"id": domain_request.pk})
|
||||
reverse("domain-request:additional_details", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
|
@ -1400,7 +1406,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1409,7 +1415,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(
|
||||
reverse("domain-request:additional_details", kwargs={"id": domain_request.pk})
|
||||
reverse("domain-request:additional_details", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
|
@ -1439,7 +1445,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.assertEqual(domain_request.has_cisa_representative, None)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1448,7 +1454,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(
|
||||
reverse("domain-request:additional_details", kwargs={"id": domain_request.id})
|
||||
reverse("domain-request:additional_details", kwargs={"domain_request_pk": domain_request.id})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
|
@ -1472,7 +1478,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request.no_other_contacts_rationale = "Hello!"
|
||||
domain_request.save()
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1480,7 +1486,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.pk}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -1520,7 +1528,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# Domain request has other contacts by default
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1528,7 +1536,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.pk}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -1604,7 +1614,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_info.other_contacts.set([other])
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1612,7 +1622,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.pk}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -1653,7 +1665,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
@less_console_noise_decorator
|
||||
def test_if_yes_no_form_is_no_then_no_other_contacts_required(self):
|
||||
"""Applicants with no other contacts have to give a reason."""
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": 0}))
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"domain_request_pk": 0}))
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "False"
|
||||
response = other_contacts_page.forms[0].submit()
|
||||
|
@ -1669,7 +1681,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
@less_console_noise_decorator
|
||||
def test_if_yes_no_form_is_yes_then_other_contacts_required(self):
|
||||
"""Applicants with other contacts do not have to give a reason."""
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": 0}))
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"domain_request_pk": 0}))
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "True"
|
||||
response = other_contacts_page.forms[0].submit()
|
||||
|
@ -1737,7 +1749,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request.other_contacts.add(other2)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1745,7 +1757,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.id}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.id})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -1810,7 +1824,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request.other_contacts.add(other)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1818,7 +1832,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.id}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.id})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -1887,7 +1903,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request.other_contacts.add(other)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1895,7 +1911,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.id}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.id})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -1967,7 +1985,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
other_contact_pk = other.id
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -1975,7 +1993,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.pk}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -2043,7 +2063,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
other_contact_pk = so.id
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -2051,7 +2071,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts", kwargs={"id": domain_request.pk}))
|
||||
other_contacts_page = self.app.get(
|
||||
reverse("domain-request:other_contacts", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
@ -2113,7 +2135,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
so_pk = so.id
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -2121,7 +2143,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
so_page = self.app.get(reverse("domain-request:senior_official", kwargs={"id": domain_request.pk}))
|
||||
so_page = self.app.get(
|
||||
reverse("domain-request:senior_official", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
so_form = so_page.forms[0]
|
||||
|
@ -2182,7 +2206,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
so_pk = so.id
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -2190,7 +2214,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
so_page = self.app.get(reverse("domain-request:senior_official", kwargs={"id": domain_request.pk}))
|
||||
so_page = self.app.get(
|
||||
reverse("domain-request:senior_official", kwargs={"domain_request_pk": domain_request.pk})
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
so_form = so_page.forms[0]
|
||||
|
@ -2240,7 +2266,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
creator_pk = self.user.id
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -2539,7 +2565,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
new_domain_request_id = all_domain_requests.first().id
|
||||
|
||||
# Skip to the current sites page
|
||||
current_sites_page = self.app.get(reverse("domain-request:current_sites", kwargs={"id": new_domain_request_id}))
|
||||
current_sites_page = self.app.get(
|
||||
reverse("domain-request:current_sites", kwargs={"domain_request_pk": new_domain_request_id})
|
||||
)
|
||||
# fill in the form field
|
||||
current_sites_form = current_sites_page.forms[0]
|
||||
self.assertIn("current_sites-0-website", current_sites_form.fields)
|
||||
|
@ -2614,7 +2642,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
domain_request.alternative_domains.add(alt)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
url = reverse("edit-domain-request", kwargs={"id": domain_request.pk})
|
||||
url = reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk})
|
||||
response = self.client.get(url)
|
||||
|
||||
# TODO: this is a sketch of each page in the wizard which needs to be tested
|
||||
|
@ -2724,7 +2752,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
NOTE: This may be a moot point if we implement a more solid pattern in the
|
||||
future, like not a submit action at all on the review page."""
|
||||
|
||||
review_page = self.app.get(reverse("domain-request:review", kwargs={"id": 0}))
|
||||
review_page = self.app.get(reverse("domain-request:review", kwargs={"domain_request_pk": 0}))
|
||||
self.assertContains(review_page, "toggle-submit-domain-request")
|
||||
self.assertContains(review_page, "Your request form is incomplete")
|
||||
|
||||
|
@ -2742,7 +2770,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
# This user should also be forbidden from editing existing ones
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}), expect_errors=True)
|
||||
edit_page = self.app.get(
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}), expect_errors=True
|
||||
)
|
||||
self.assertEqual(edit_page.status_code, 403)
|
||||
|
||||
# Cleanup
|
||||
|
@ -2767,7 +2797,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
# This user should also be allowed to edit existing ones
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
|
||||
edit_page = self.app.get(
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk})
|
||||
).follow()
|
||||
self.assertEqual(edit_page.status_code, 200)
|
||||
|
||||
def test_non_creator_access(self):
|
||||
|
@ -2776,14 +2808,18 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
other_user = User.objects.create_user(username="other_user", password=p)
|
||||
domain_request = completed_domain_request(user=other_user)
|
||||
|
||||
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}), expect_errors=True)
|
||||
edit_page = self.app.get(
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk}), expect_errors=True
|
||||
)
|
||||
self.assertEqual(edit_page.status_code, 403)
|
||||
|
||||
def test_creator_access(self):
|
||||
"""Tests that a user can edit a domain request they created"""
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
|
||||
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
|
||||
edit_page = self.app.get(
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk})
|
||||
).follow()
|
||||
self.assertEqual(edit_page.status_code, 200)
|
||||
|
||||
|
||||
|
@ -2898,12 +2934,8 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||
domain_request.save()
|
||||
|
||||
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "city1.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Status:")
|
||||
detail_page = self.client.get(f"/domain-request/{domain_request.id}")
|
||||
self.assertEqual(detail_page.status_code, 403)
|
||||
# Restricted user trying to withdraw results in 403 error
|
||||
with less_console_noise():
|
||||
for url_name in [
|
||||
|
@ -2911,7 +2943,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
"domain-request-withdrawn",
|
||||
]:
|
||||
with self.subTest(url_name=url_name):
|
||||
page = self.client.get(reverse(url_name, kwargs={"pk": domain_request.pk}))
|
||||
page = self.client.get(reverse(url_name, kwargs={"domain_request_pk": domain_request.pk}))
|
||||
self.assertEqual(page.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -2931,7 +2963,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
"domain-request-withdrawn",
|
||||
]:
|
||||
with self.subTest(url_name=url_name):
|
||||
page = self.client.get(reverse(url_name, kwargs={"pk": domain_request.pk}))
|
||||
page = self.client.get(reverse(url_name, kwargs={"domain_request_pk": domain_request.pk}))
|
||||
self.assertEqual(page.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -3206,7 +3238,9 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
|
|||
self.assertContains(detail_page, "usa-current", count=2)
|
||||
|
||||
# We default to the requesting entity page
|
||||
expected_url = reverse("domain-request:portfolio_requesting_entity", kwargs={"id": domain_request.id})
|
||||
expected_url = reverse(
|
||||
"domain-request:portfolio_requesting_entity", kwargs={"domain_request_pk": domain_request.id}
|
||||
)
|
||||
# This returns the entire url, thus "in"
|
||||
self.assertIn(expected_url, detail_page.request.url)
|
||||
else:
|
||||
|
|
|
@ -199,14 +199,14 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
|
||||
# Check action_url
|
||||
action_url_expected = (
|
||||
reverse("edit-domain-request", kwargs={"id": self.domain_requests[i].id})
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": self.domain_requests[i].id})
|
||||
if self.domain_requests[i].status
|
||||
in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
else reverse("domain-request-status", kwargs={"pk": self.domain_requests[i].id})
|
||||
else reverse("domain-request-status", kwargs={"domain_request_pk": self.domain_requests[i].id})
|
||||
)
|
||||
self.assertEqual(action_url_expected, action_urls[i])
|
||||
|
||||
|
@ -342,7 +342,8 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
# Test case where action is View
|
||||
self.assertEqual("View", action_labels[i])
|
||||
self.assertEqual(
|
||||
reverse("domain-request-status-viewonly", kwargs={"pk": expected_domain_request.id}), action_urls[i]
|
||||
reverse("domain-request-status-viewonly", kwargs={"domain_request_pk": expected_domain_request.id}),
|
||||
action_urls[i],
|
||||
)
|
||||
self.assertEqual("visibility", svg_icons[i])
|
||||
elif status[i] in [
|
||||
|
@ -360,7 +361,8 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
# Test case where action is Manage
|
||||
self.assertEqual("Manage", action_labels[i])
|
||||
self.assertEqual(
|
||||
reverse("domain-request-status", kwargs={"pk": expected_domain_request.id}), action_urls[i]
|
||||
reverse("domain-request-status", kwargs={"domain_request_pk": expected_domain_request.id}),
|
||||
action_urls[i],
|
||||
)
|
||||
self.assertEqual("settings", svg_icons[i])
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ from datetime import date
|
|||
from django.conf import settings
|
||||
from registrar.models import Domain, DomainInvitation, UserDomainRole
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||
from registrar.utility.errors import (
|
||||
|
@ -269,6 +270,88 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo
|
|||
return True
|
||||
|
||||
|
||||
def send_portfolio_member_permission_remove_email(requestor, permissions: UserPortfolioPermission):
|
||||
"""
|
||||
Sends an email notification to a portfolio member when their permissions are deleted.
|
||||
|
||||
This function retrieves the requestor's email and sends a templated email to the affected user,
|
||||
notifying them of the removal of their portfolio permissions.
|
||||
|
||||
Args:
|
||||
requestor (User): The user initiating the permission update.
|
||||
permissions (UserPortfolioPermission): The updated permissions object containing the affected user
|
||||
and the portfolio details.
|
||||
|
||||
Returns:
|
||||
bool: True if the email was sent successfully, False if an EmailSendingError occurred.
|
||||
|
||||
Raises:
|
||||
MissingEmailError: If the requestor has no email associated with their account.
|
||||
"""
|
||||
requestor_email = _get_requestor_email(requestor, portfolio=permissions.portfolio)
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/portfolio_removal.txt",
|
||||
"emails/portfolio_removal_subject.txt",
|
||||
to_address=permissions.user.email,
|
||||
context={
|
||||
"requested_user": permissions.user,
|
||||
"portfolio": permissions.portfolio,
|
||||
"requestor_email": requestor_email,
|
||||
},
|
||||
)
|
||||
except EmailSendingError:
|
||||
logger.warning(
|
||||
"Could not send email organization member removal notification to %s " "for portfolio: %s",
|
||||
permissions.user.email,
|
||||
permissions.portfolio.organization_name,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def send_portfolio_invitation_remove_email(requestor, invitation: PortfolioInvitation):
|
||||
"""
|
||||
Sends an email notification to a portfolio invited member when their permissions are deleted.
|
||||
|
||||
This function retrieves the requestor's email and sends a templated email to the affected email,
|
||||
notifying them of the removal of their invited portfolio permissions.
|
||||
|
||||
Args:
|
||||
requestor (User): The user initiating the permission update.
|
||||
invitation (PortfolioInvitation): The invitation object containing the affected user
|
||||
and the portfolio details.
|
||||
|
||||
Returns:
|
||||
bool: True if the email was sent successfully, False if an EmailSendingError occurred.
|
||||
|
||||
Raises:
|
||||
MissingEmailError: If the requestor has no email associated with their account.
|
||||
"""
|
||||
requestor_email = _get_requestor_email(requestor, portfolio=invitation.portfolio)
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/portfolio_removal.txt",
|
||||
"emails/portfolio_removal_subject.txt",
|
||||
to_address=invitation.email,
|
||||
context={
|
||||
"requested_user": None,
|
||||
"portfolio": invitation.portfolio,
|
||||
"requestor_email": requestor_email,
|
||||
},
|
||||
)
|
||||
except EmailSendingError:
|
||||
logger.warning(
|
||||
"Could not send email organization member removal notification to %s " "for portfolio: %s",
|
||||
invitation.email,
|
||||
invitation.portfolio.organization_name,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def send_portfolio_admin_addition_emails(email: str, requestor, portfolio: Portfolio):
|
||||
"""
|
||||
Notifies all portfolio admins of the provided portfolio of a newly invited portfolio admin
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
"""Views for a single Domain.
|
||||
|
||||
Authorization is handled by the `DomainPermissionView`. To ensure that only
|
||||
authorized users can see information on a domain, every view here should
|
||||
inherit from `DomainPermissionView` (or DomainInvitationPermissionCancelView).
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
import logging
|
||||
import requests
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.views.generic import DeleteView, DetailView, UpdateView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django.conf import settings
|
||||
from registrar.decorators import (
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
|
||||
IS_DOMAIN_MANAGER,
|
||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
|
||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
|
||||
IS_STAFF_MANAGING_DOMAIN,
|
||||
grant_access,
|
||||
)
|
||||
from registrar.forms.domain import DomainSuborganizationForm, DomainRenewalForm
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
|
@ -40,7 +43,6 @@ from registrar.utility.errors import (
|
|||
SecurityEmailErrorCodes,
|
||||
)
|
||||
from registrar.models.utility.contact_error import ContactError
|
||||
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
||||
from registrar.utility.waffle import flag_is_active_for_user
|
||||
from registrar.views.utility.invitation_helper import (
|
||||
get_org_membership,
|
||||
|
@ -67,19 +69,22 @@ from epplibwrapper import (
|
|||
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
from ..utility.email_invitations import send_domain_invitation_email, send_portfolio_invitation_email
|
||||
from .utility import DomainPermissionView, DomainInvitationPermissionCancelView
|
||||
from django import forms
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainBaseView(DomainPermissionView):
|
||||
class DomainBaseView(PermissionRequiredMixin, DetailView):
|
||||
"""
|
||||
Base View for the Domain. Handles getting and setting the domain
|
||||
in session cache on GETs. Also provides methods for getting
|
||||
and setting the domain in cache
|
||||
"""
|
||||
|
||||
model = Domain
|
||||
pk_url_kwarg = "domain_pk"
|
||||
context_object_name = "domain"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self._get_domain(request)
|
||||
context = self.get_context_data(object=self.object)
|
||||
|
@ -95,7 +100,7 @@ class DomainBaseView(DomainPermissionView):
|
|||
self.session = request.session
|
||||
# domain:private_key is the session key to use for
|
||||
# caching the domain in the session
|
||||
domain_pk = "domain:" + str(self.kwargs.get("pk"))
|
||||
domain_pk = "domain:" + str(self.kwargs.get("domain_pk"))
|
||||
cached_domain = self.session.get(domain_pk)
|
||||
|
||||
if cached_domain:
|
||||
|
@ -108,9 +113,136 @@ class DomainBaseView(DomainPermissionView):
|
|||
"""
|
||||
update domain in the session cache
|
||||
"""
|
||||
domain_pk = "domain:" + str(self.kwargs.get("pk"))
|
||||
domain_pk = "domain:" + str(self.kwargs.get("domain_pk"))
|
||||
self.session[domain_pk] = self.object
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = self.request.user
|
||||
context["is_analyst_or_superuser"] = user.has_perm("registrar.analyst_access_permission") or user.has_perm(
|
||||
"registrar.full_access_permission"
|
||||
)
|
||||
context["is_domain_manager"] = UserDomainRole.objects.filter(user=user, domain=self.object).exists()
|
||||
context["is_portfolio_user"] = self.can_access_domain_via_portfolio(self.object.pk)
|
||||
context["is_editable"] = self.is_editable()
|
||||
# Stored in a variable for the linter
|
||||
action = "analyst_action"
|
||||
action_location = "analyst_action_location"
|
||||
# Flag to see if an analyst is attempting to make edits
|
||||
if action in self.request.session:
|
||||
context[action] = self.request.session[action]
|
||||
if action_location in self.request.session:
|
||||
context[action_location] = self.request.session[action_location]
|
||||
|
||||
return context
|
||||
|
||||
def is_editable(self):
|
||||
"""Returns whether domain is editable in the context of the view"""
|
||||
domain_editable = self.object.is_editable()
|
||||
if not domain_editable:
|
||||
return False
|
||||
|
||||
# if user is domain manager or analyst or admin, return True
|
||||
if (
|
||||
self.can_access_other_user_domains(self.object.id)
|
||||
or UserDomainRole.objects.filter(user=self.request.user, domain=self.object).exists()
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_access_domain_via_portfolio(self, pk):
|
||||
"""Most views should not allow permission to portfolio users.
|
||||
If particular views allow access to the domain pages, they will need to override
|
||||
this function.
|
||||
"""
|
||||
return False
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["domain_pk"]
|
||||
"""
|
||||
pk = self.kwargs["domain_pk"]
|
||||
|
||||
# test if domain in editable state
|
||||
if not self.in_editable_state(pk):
|
||||
return False
|
||||
|
||||
# if we need to check more about the nature of role, do it here.
|
||||
return True
|
||||
|
||||
def in_editable_state(self, pk):
|
||||
"""Is the domain in an editable state"""
|
||||
requested_domain = None
|
||||
if Domain.objects.filter(id=pk).exists():
|
||||
requested_domain = Domain.objects.get(id=pk)
|
||||
|
||||
# if domain is editable return true
|
||||
if requested_domain and requested_domain.is_editable():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_access_other_user_domains(self, pk):
|
||||
"""Checks to see if an authorized user (staff or superuser)
|
||||
can access a domain that they did not create or was invited to.
|
||||
"""
|
||||
|
||||
# Check if the user is permissioned...
|
||||
user_is_analyst_or_superuser = self.request.user.has_perm(
|
||||
"registrar.analyst_access_permission"
|
||||
) or self.request.user.has_perm("registrar.full_access_permission")
|
||||
|
||||
if not user_is_analyst_or_superuser:
|
||||
return False
|
||||
|
||||
# Check if the user is attempting a valid edit action.
|
||||
# In other words, if the analyst/admin did not click
|
||||
# the 'Manage Domain' button in /admin,
|
||||
# then they cannot access this page.
|
||||
session = self.request.session
|
||||
can_do_action = (
|
||||
"analyst_action" in session
|
||||
and "analyst_action_location" in session
|
||||
and session["analyst_action_location"] == pk
|
||||
)
|
||||
|
||||
if not can_do_action:
|
||||
return False
|
||||
|
||||
# Analysts may manage domains, when they are in these statuses:
|
||||
valid_domain_statuses = [
|
||||
DomainRequest.DomainRequestStatus.APPROVED,
|
||||
DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
# Edge case - some domains do not have
|
||||
# a status or DomainInformation... aka a status of 'None'.
|
||||
# It is necessary to access those to correct errors.
|
||||
None,
|
||||
]
|
||||
|
||||
requested_domain = None
|
||||
if DomainInformation.objects.filter(id=pk).exists():
|
||||
requested_domain = DomainInformation.objects.get(id=pk)
|
||||
|
||||
# if no domain information or domain request exist, the user
|
||||
# should be able to manage the domain; however, if domain information
|
||||
# and domain request exist, and domain request is not in valid status,
|
||||
# user should not be able to manage domain
|
||||
if (
|
||||
requested_domain
|
||||
and requested_domain.domain_request
|
||||
and requested_domain.domain_request.status not in valid_domain_statuses
|
||||
):
|
||||
return False
|
||||
|
||||
# Valid session keys exist,
|
||||
# the user is permissioned,
|
||||
# and it is in a valid status
|
||||
return True
|
||||
|
||||
|
||||
class DomainFormBaseView(DomainBaseView, FormMixin):
|
||||
"""
|
||||
|
@ -257,6 +389,7 @@ class DomainFormBaseView(DomainBaseView, FormMixin):
|
|||
)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN, HAS_PORTFOLIO_DOMAINS_VIEW_ALL)
|
||||
class DomainView(DomainBaseView):
|
||||
"""Domain detail overview page."""
|
||||
|
||||
|
@ -311,6 +444,7 @@ class DomainView(DomainBaseView):
|
|||
self._update_session_with_domain()
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainRenewalView(DomainBaseView):
|
||||
"""Domain detail overview page."""
|
||||
|
||||
|
@ -344,9 +478,9 @@ class DomainRenewalView(DomainBaseView):
|
|||
and (requested_domain.is_expiring() or requested_domain.is_expired())
|
||||
)
|
||||
|
||||
def post(self, request, pk):
|
||||
def post(self, request, domain_pk):
|
||||
|
||||
domain = get_object_or_404(Domain, id=pk)
|
||||
domain = get_object_or_404(Domain, id=domain_pk)
|
||||
|
||||
form = DomainRenewalForm(request.POST)
|
||||
|
||||
|
@ -363,7 +497,7 @@ class DomainRenewalView(DomainBaseView):
|
|||
"This domain has not been renewed for one year, "
|
||||
"please email help@get.gov if this problem persists.",
|
||||
)
|
||||
return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk}))
|
||||
return HttpResponseRedirect(reverse("domain", kwargs={"domain_pk": domain_pk}))
|
||||
|
||||
# if not valid, render the template with error messages
|
||||
# passing editable and is_editable for re-render
|
||||
|
@ -379,6 +513,7 @@ class DomainRenewalView(DomainBaseView):
|
|||
)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainOrgNameAddressView(DomainFormBaseView):
|
||||
"""Organization view"""
|
||||
|
||||
|
@ -395,7 +530,7 @@ class DomainOrgNameAddressView(DomainFormBaseView):
|
|||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
return reverse("domain-org-name-address", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-org-name-address", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, save the organization name and mailing address."""
|
||||
|
@ -420,6 +555,7 @@ class DomainOrgNameAddressView(DomainFormBaseView):
|
|||
return super().has_permission()
|
||||
|
||||
|
||||
@grant_access(IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainSubOrganizationView(DomainFormBaseView):
|
||||
"""Suborganization view"""
|
||||
|
||||
|
@ -454,7 +590,7 @@ class DomainSubOrganizationView(DomainFormBaseView):
|
|||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
return reverse("domain-suborganization", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-suborganization", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, save the organization name and mailing address."""
|
||||
|
@ -466,6 +602,7 @@ class DomainSubOrganizationView(DomainFormBaseView):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainSeniorOfficialView(DomainFormBaseView):
|
||||
"""Domain senior official editing view."""
|
||||
|
||||
|
@ -493,7 +630,7 @@ class DomainSeniorOfficialView(DomainFormBaseView):
|
|||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
return reverse("domain-senior-official", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-senior-official", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, save the senior official."""
|
||||
|
@ -523,6 +660,7 @@ class DomainSeniorOfficialView(DomainFormBaseView):
|
|||
return super().has_permission()
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainDNSView(DomainBaseView):
|
||||
"""DNS Information View."""
|
||||
|
||||
|
@ -586,7 +724,7 @@ class PrototypeDomainDNSRecordView(DomainFormBaseView):
|
|||
return True
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("prototype-domain-dns", kwargs={"pk": self.object.pk})
|
||||
return reverse("prototype-domain-dns", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def find_by_name(self, items, name):
|
||||
"""Find an item by name in a list of dictionaries."""
|
||||
|
@ -739,6 +877,7 @@ class PrototypeDomainDNSRecordView(DomainFormBaseView):
|
|||
return super().post(request)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainNameserversView(DomainFormBaseView):
|
||||
"""Domain nameserver editing view."""
|
||||
|
||||
|
@ -763,7 +902,7 @@ class DomainNameserversView(DomainFormBaseView):
|
|||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the nameservers page for the domain."""
|
||||
return reverse("domain-dns-nameservers", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-dns-nameservers", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Adjust context from FormMixin for formsets."""
|
||||
|
@ -866,6 +1005,7 @@ class DomainNameserversView(DomainFormBaseView):
|
|||
return super().form_valid(formset)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainDNSSECView(DomainFormBaseView):
|
||||
"""Domain DNSSEC editing view."""
|
||||
|
||||
|
@ -884,7 +1024,7 @@ class DomainDNSSECView(DomainFormBaseView):
|
|||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the DNSSEC page for the domain."""
|
||||
return reverse("domain-dns-dnssec", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-dns-dnssec", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view."""
|
||||
|
@ -903,6 +1043,7 @@ class DomainDNSSECView(DomainFormBaseView):
|
|||
return self.form_valid(form)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainDsDataView(DomainFormBaseView):
|
||||
"""Domain DNSSEC ds data editing view."""
|
||||
|
||||
|
@ -935,7 +1076,7 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the DS data page for the domain."""
|
||||
return reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Adjust context from FormMixin for formsets."""
|
||||
|
@ -1021,6 +1162,7 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
return super().form_valid(formset)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainSecurityEmailView(DomainFormBaseView):
|
||||
"""Domain security email editing view."""
|
||||
|
||||
|
@ -1041,7 +1183,7 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
|||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the security email page for the domain."""
|
||||
return reverse("domain-security-email", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-security-email", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, call setter in model."""
|
||||
|
@ -1092,6 +1234,7 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
|||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainUsersView(DomainBaseView):
|
||||
"""Domain managers page in the domain details."""
|
||||
|
||||
|
@ -1115,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."""
|
||||
|
||||
|
@ -1187,6 +1315,7 @@ class DomainUsersView(DomainBaseView):
|
|||
return context
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainAddUserView(DomainFormBaseView):
|
||||
"""Inside of a domain's user management, a form for adding users.
|
||||
|
||||
|
@ -1198,7 +1327,7 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
form_class = DomainAddUserForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("domain-users", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-users", kwargs={"domain_pk": self.object.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Add the specified user to this domain."""
|
||||
|
@ -1284,8 +1413,10 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
messages.success(self.request, f"Added user {email}.")
|
||||
|
||||
|
||||
class DomainInvitationCancelView(SuccessMessageMixin, DomainInvitationPermissionCancelView):
|
||||
object: DomainInvitation
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainInvitationCancelView(SuccessMessageMixin, UpdateView):
|
||||
model = DomainInvitation
|
||||
pk_url_kwarg = "domain_invitation_pk"
|
||||
fields = []
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
@ -1303,26 +1434,29 @@ class DomainInvitationCancelView(SuccessMessageMixin, DomainInvitationPermission
|
|||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("domain-users", kwargs={"pk": self.object.domain.id})
|
||||
return reverse("domain-users", kwargs={"domain_pk": self.object.domain.id})
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return f"Canceled invitation to {self.object.email}."
|
||||
|
||||
|
||||
class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
|
||||
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
|
||||
class DomainDeleteUserView(DeleteView):
|
||||
"""Inside of a domain's user management, a form for deleting users."""
|
||||
|
||||
object: UserDomainRole # workaround for type mismatch in DeleteView
|
||||
object: UserDomainRole
|
||||
model = UserDomainRole
|
||||
context_object_name = "userdomainrole"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
"""Custom get_object definition to grab a UserDomainRole object from a domain_id and user_id"""
|
||||
domain_id = self.kwargs.get("pk")
|
||||
domain_id = self.kwargs.get("domain_pk")
|
||||
user_id = self.kwargs.get("user_pk")
|
||||
return UserDomainRole.objects.get(domain=domain_id, user=user_id)
|
||||
|
||||
def get_success_url(self):
|
||||
"""Refreshes the page after a delete is successful"""
|
||||
return reverse("domain-users", kwargs={"pk": self.object.domain.id})
|
||||
return reverse("domain-users", kwargs={"domain_pk": self.object.domain.id})
|
||||
|
||||
def get_success_message(self):
|
||||
"""Returns confirmation content for the deletion event"""
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
import logging
|
||||
from collections import defaultdict
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
from django.contrib import messages
|
||||
from django.views.generic import DeleteView, DetailView, TemplateView
|
||||
from registrar.decorators import (
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
IS_DOMAIN_REQUEST_CREATOR,
|
||||
grant_access,
|
||||
)
|
||||
from registrar.forms import domain_request_wizard as forms
|
||||
from registrar.forms.utility.wizard_form_helper import request_step_list
|
||||
from registrar.models import DomainRequest
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
from registrar.views.utility import StepsHelper
|
||||
from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView
|
||||
from registrar.utility.enums import Step, PortfolioDomainRequestStep
|
||||
|
||||
from .utility import (
|
||||
DomainRequestPermissionView,
|
||||
DomainRequestPermissionWithdrawView,
|
||||
DomainRequestWizardPermissionView,
|
||||
DomainRequestPortfolioViewonlyView,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||
class DomainRequestWizard(TemplateView):
|
||||
"""
|
||||
A common set of methods and configuration.
|
||||
|
||||
|
@ -51,7 +51,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
# NB: this is included here for reference. Do not change it without
|
||||
# also changing the many places it is hardcoded in the HTML templates
|
||||
URL_NAMESPACE = "domain-request"
|
||||
# name for accessing /domain-request/<id>/edit
|
||||
# name for accessing /domain-request/<domain_request_pk>/edit
|
||||
EDIT_URL_NAME = "edit-domain-request"
|
||||
NEW_URL_NAME = "start"
|
||||
FINISHED_URL_NAME = "finished"
|
||||
|
@ -174,7 +174,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
|
||||
def has_pk(self):
|
||||
"""Does this wizard know about a DomainRequest database record?"""
|
||||
return bool(self.kwargs.get("id") is not None)
|
||||
return bool(self.kwargs.get("domain_request_pk") is not None)
|
||||
|
||||
def get_step_enum(self):
|
||||
"""Determines which step enum we should use for the wizard"""
|
||||
|
@ -209,11 +209,11 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
try:
|
||||
self._domain_request = DomainRequest.objects.get(
|
||||
creator=creator,
|
||||
pk=self.kwargs.get("id"),
|
||||
pk=self.kwargs.get("domain_request_pk"),
|
||||
)
|
||||
return self._domain_request
|
||||
except DomainRequest.DoesNotExist:
|
||||
logger.debug("DomainRequest id %s did not have a DomainRequest" % id)
|
||||
logger.debug("DomainRequest id %s did not have a DomainRequest" % self.kwargs.get("domain_request_pk"))
|
||||
|
||||
# If a user is creating a request, we assume that perms are handled upstream
|
||||
if self.request.user.is_org_user(self.request):
|
||||
|
@ -292,10 +292,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
|
||||
current_url = resolve(request.path_info).url_name
|
||||
|
||||
# if user visited via an "edit" url, associate the id of the
|
||||
# if user visited via an "edit" url, associate the pk of the
|
||||
# domain request they are trying to edit to this wizard instance
|
||||
# and remove any prior wizard data from their session
|
||||
if current_url == self.EDIT_URL_NAME and "id" in kwargs:
|
||||
if current_url == self.EDIT_URL_NAME and "domain_request_pk" in kwargs:
|
||||
del self.storage
|
||||
|
||||
# if accessing this class directly, redirect to either to an acknowledgement
|
||||
|
@ -474,7 +474,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
|
||||
def goto(self, step):
|
||||
self.steps.current = step
|
||||
return redirect(reverse(f"{self.URL_NAMESPACE}:{step}", kwargs={"id": self.domain_request.id}))
|
||||
return redirect(reverse(f"{self.URL_NAMESPACE}:{step}", kwargs={"domain_request_pk": self.domain_request.id}))
|
||||
|
||||
def goto_next_step(self):
|
||||
"""Redirects to the next step."""
|
||||
|
@ -823,23 +823,12 @@ class Finished(DomainRequestWizard):
|
|||
return render(self.request, self.template_name, context)
|
||||
|
||||
|
||||
class DomainRequestStatus(DomainRequestPermissionView):
|
||||
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||
class DomainRequestStatus(DetailView):
|
||||
template_name = "domain_request_status.html"
|
||||
|
||||
def has_permission(self):
|
||||
"""
|
||||
Override of the base has_permission class to account for portfolio permissions
|
||||
"""
|
||||
has_base_perms = super().has_permission()
|
||||
if not has_base_perms:
|
||||
return False
|
||||
|
||||
if self.request.user.is_org_user(self.request):
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return True
|
||||
model = DomainRequest
|
||||
pk_url_kwarg = "domain_request_pk"
|
||||
context_object_name = "DomainRequest"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Context override to add a step list to the context"""
|
||||
|
@ -854,19 +843,27 @@ class DomainRequestStatus(DomainRequestPermissionView):
|
|||
return context
|
||||
|
||||
|
||||
class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView):
|
||||
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||
class DomainRequestWithdrawConfirmation(DetailView):
|
||||
"""This page will ask user to confirm if they want to withdraw
|
||||
|
||||
The DomainRequestPermissionView restricts access so that only the
|
||||
Access is restricted so that only the
|
||||
`creator` of the domain request may withdraw it.
|
||||
"""
|
||||
|
||||
template_name = "domain_request_withdraw_confirmation.html"
|
||||
template_name = "domain_request_withdraw_confirmation.html" # DetailView property for what model this is viewing
|
||||
model = DomainRequest
|
||||
pk_url_kwarg = "domain_request_pk"
|
||||
context_object_name = "DomainRequest"
|
||||
|
||||
|
||||
class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
||||
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||
class DomainRequestWithdrawn(DetailView):
|
||||
# this view renders no template
|
||||
template_name = ""
|
||||
model = DomainRequest
|
||||
pk_url_kwarg = "domain_request_pk"
|
||||
context_object_name = "DomainRequest"
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""View class that does the actual withdrawing.
|
||||
|
@ -874,7 +871,7 @@ class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
|||
If user click on withdraw confirm button, this view updates the status
|
||||
to withdraw and send back to homepage.
|
||||
"""
|
||||
domain_request = DomainRequest.objects.get(id=self.kwargs["pk"])
|
||||
domain_request = DomainRequest.objects.get(id=self.kwargs["domain_request_pk"])
|
||||
domain_request.withdraw()
|
||||
domain_request.save()
|
||||
if self.request.user.is_org_user(self.request):
|
||||
|
@ -883,16 +880,16 @@ class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
|||
return HttpResponseRedirect(reverse("home"))
|
||||
|
||||
|
||||
class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||
class DomainRequestDeleteView(PermissionRequiredMixin, DeleteView):
|
||||
"""Delete view for home that allows the end user to delete DomainRequests"""
|
||||
|
||||
object: DomainRequest # workaround for type mismatch in DeleteView
|
||||
model = DomainRequest
|
||||
pk_url_kwarg = "domain_request_pk"
|
||||
|
||||
def has_permission(self):
|
||||
"""Custom override for has_permission to exclude all statuses, except WITHDRAWN and STARTED"""
|
||||
has_perm = super().has_permission()
|
||||
if not has_perm:
|
||||
return False
|
||||
|
||||
status = self.get_object().status
|
||||
valid_statuses = [DomainRequest.DomainRequestStatus.WITHDRAWN, DomainRequest.DomainRequestStatus.STARTED]
|
||||
|
@ -989,8 +986,12 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
|||
|
||||
|
||||
# region Portfolio views
|
||||
class PortfolioDomainRequestStatusViewOnly(DomainRequestPortfolioViewonlyView):
|
||||
@grant_access(HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL)
|
||||
class PortfolioDomainRequestStatusViewOnly(DetailView):
|
||||
template_name = "portfolio_domain_request_status_viewonly.html"
|
||||
model = DomainRequest
|
||||
pk_url_kwarg = "domain_request_pk"
|
||||
context_object_name = "DomainRequest"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
from django.http import JsonResponse
|
||||
from django.core.paginator import Paginator
|
||||
from registrar.decorators import grant_access, ALL
|
||||
from registrar.models import DomainRequest
|
||||
from django.utils.dateformat import format
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
@login_required
|
||||
@grant_access(ALL)
|
||||
def get_domain_requests_json(request):
|
||||
"""Given the current request,
|
||||
get all domain requests that are associated with the request user and exclude the APPROVED ones.
|
||||
If we are on the portfolio requests page, limit the response to only those requests associated with
|
||||
the given portfolio."""
|
||||
|
||||
domain_request_ids = get_domain_request_ids_from_request(request)
|
||||
domain_request_ids = _get_domain_request_ids_from_request(request)
|
||||
|
||||
objects = DomainRequest.objects.filter(id__in=domain_request_ids)
|
||||
unfiltered_total = objects.count()
|
||||
|
||||
objects = apply_search(objects, request)
|
||||
objects = apply_status_filter(objects, request)
|
||||
objects = apply_sorting(objects, request)
|
||||
objects = _apply_search(objects, request)
|
||||
objects = _apply_status_filter(objects, request)
|
||||
objects = _apply_sorting(objects, request)
|
||||
|
||||
paginator = Paginator(objects, 10)
|
||||
page_number = request.GET.get("page", 1)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
domain_requests = [
|
||||
serialize_domain_request(request, domain_request, request.user) for domain_request in page_obj.object_list
|
||||
_serialize_domain_request(request, domain_request, request.user) for domain_request in page_obj.object_list
|
||||
]
|
||||
|
||||
return JsonResponse(
|
||||
|
@ -43,7 +43,7 @@ def get_domain_requests_json(request):
|
|||
)
|
||||
|
||||
|
||||
def get_domain_request_ids_from_request(request):
|
||||
def _get_domain_request_ids_from_request(request):
|
||||
"""Get domain request ids from request.
|
||||
|
||||
If portfolio specified, return domain request ids associated with portfolio.
|
||||
|
@ -62,7 +62,7 @@ def get_domain_request_ids_from_request(request):
|
|||
return domain_requests.values_list("id", flat=True)
|
||||
|
||||
|
||||
def apply_search(queryset, request):
|
||||
def _apply_search(queryset, request):
|
||||
search_term = request.GET.get("search_term")
|
||||
is_portfolio = request.GET.get("portfolio")
|
||||
|
||||
|
@ -90,7 +90,7 @@ def apply_search(queryset, request):
|
|||
return queryset
|
||||
|
||||
|
||||
def apply_status_filter(queryset, request):
|
||||
def _apply_status_filter(queryset, request):
|
||||
status_param = request.GET.get("status")
|
||||
if status_param:
|
||||
status_list = status_param.split(",")
|
||||
|
@ -105,7 +105,7 @@ def apply_status_filter(queryset, request):
|
|||
return queryset
|
||||
|
||||
|
||||
def apply_sorting(queryset, request):
|
||||
def _apply_sorting(queryset, request):
|
||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||
|
||||
|
@ -118,7 +118,7 @@ def apply_sorting(queryset, request):
|
|||
return queryset.order_by(sort_by)
|
||||
|
||||
|
||||
def serialize_domain_request(request, domain_request, user):
|
||||
def _serialize_domain_request(request, domain_request, user):
|
||||
|
||||
deletable_statuses = [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
|
@ -154,9 +154,9 @@ def serialize_domain_request(request, domain_request, user):
|
|||
|
||||
# Map the action label to corresponding URLs and icons
|
||||
action_url_map = {
|
||||
"Edit": reverse("edit-domain-request", kwargs={"id": domain_request.id}),
|
||||
"Manage": reverse("domain-request-status", kwargs={"pk": domain_request.id}),
|
||||
"View": reverse("domain-request-status-viewonly", kwargs={"pk": domain_request.id}),
|
||||
"Edit": reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.id}),
|
||||
"Manage": reverse("domain-request-status", kwargs={"domain_request_pk": domain_request.id}),
|
||||
"View": reverse("domain-request-status-viewonly", kwargs={"domain_request_pk": domain_request.id}),
|
||||
}
|
||||
|
||||
svg_icon_map = {"Edit": "edit", "Manage": "settings", "View": "visibility"}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.core.paginator import Paginator
|
||||
from registrar.decorators import grant_access, ALL
|
||||
from registrar.models import UserDomainRole, Domain, DomainInformation, User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
@grant_access(ALL)
|
||||
def get_domains_json(request):
|
||||
"""Given the current request,
|
||||
get all domains that are associated with the UserDomainRole object"""
|
||||
|
@ -142,7 +142,7 @@ def serialize_domain(domain, request):
|
|||
"state": domain.state,
|
||||
"state_display": domain.state_display(request),
|
||||
"get_state_help_text": domain.get_state_help_text(),
|
||||
"action_url": reverse("domain", kwargs={"pk": domain.id}),
|
||||
"action_url": reverse("domain", kwargs={"domain_pk": domain.id}),
|
||||
"action_label": ("View" if view_only else "Manage"),
|
||||
"svg_icon": ("visibility" if view_only else "settings"),
|
||||
"domain_info__sub_organization": suborganization_name,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
from registrar.decorators import grant_access, ALL
|
||||
|
||||
|
||||
@grant_access(ALL)
|
||||
def index(request):
|
||||
"""This page is available to anyone without logging in."""
|
||||
context = {}
|
||||
|
|
|
@ -4,37 +4,38 @@ from django.http import JsonResponse
|
|||
from django.core.paginator import Paginator
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
from registrar.decorators import HAS_PORTFOLIO_MEMBERS_ANY_PERM, grant_access
|
||||
from registrar.models import UserDomainRole, Domain, DomainInformation, User
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
||||
from registrar.models.domain_invitation import DomainInvitation
|
||||
from registrar.views.utility.mixins import PortfolioMemberDomainsPermission
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioMemberDomainsJson(View):
|
||||
|
||||
def get(self, request):
|
||||
"""Given the current request,
|
||||
get all domains that are associated with the portfolio, or
|
||||
associated with the member/invited member"""
|
||||
|
||||
domain_ids = self.get_domain_ids_from_request(request)
|
||||
domain_ids = self._get_domain_ids_from_request(request)
|
||||
|
||||
objects = Domain.objects.filter(id__in=domain_ids).select_related("domain_info__sub_organization")
|
||||
unfiltered_total = objects.count()
|
||||
|
||||
objects = self.apply_search(objects, request)
|
||||
objects = self.apply_sorting(objects, request)
|
||||
objects = self._apply_search(objects, request)
|
||||
objects = self._apply_sorting(objects, request)
|
||||
|
||||
paginator = Paginator(objects, self.get_page_size(request))
|
||||
paginator = Paginator(objects, self._get_page_size(request))
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
member_id = request.GET.get("member_id")
|
||||
domains = [self.serialize_domain(domain, member_id, request.user) for domain in page_obj.object_list]
|
||||
domains = [self._serialize_domain(domain, member_id, request.user) for domain in page_obj.object_list]
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
|
@ -48,7 +49,7 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
|||
}
|
||||
)
|
||||
|
||||
def get_page_size(self, request):
|
||||
def _get_page_size(self, request):
|
||||
"""Gets the page size.
|
||||
|
||||
If member_only, need to return the entire result set every time, so need
|
||||
|
@ -65,7 +66,7 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
|||
# later
|
||||
return 1000
|
||||
|
||||
def get_domain_ids_from_request(self, request):
|
||||
def _get_domain_ids_from_request(self, request):
|
||||
"""Get domain ids from request.
|
||||
|
||||
request.get.email - email address of invited member
|
||||
|
@ -100,13 +101,13 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
|||
logger.warning("Invalid search criteria, returning empty results list")
|
||||
return []
|
||||
|
||||
def apply_search(self, queryset, request):
|
||||
def _apply_search(self, queryset, request):
|
||||
search_term = request.GET.get("search_term")
|
||||
if search_term:
|
||||
queryset = queryset.filter(Q(name__icontains=search_term))
|
||||
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")
|
||||
order = request.GET.get("order", "asc")
|
||||
|
@ -141,7 +142,7 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
|||
|
||||
return queryset
|
||||
|
||||
def serialize_domain(self, domain, member_id, user):
|
||||
def _serialize_domain(self, domain, member_id, user):
|
||||
suborganization_name = None
|
||||
try:
|
||||
domain_info = domain.domain_info
|
||||
|
@ -176,7 +177,7 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View):
|
|||
"state": domain.state,
|
||||
"state_display": domain.state_display(),
|
||||
"get_state_help_text": domain.get_state_help_text(),
|
||||
"action_url": reverse("domain", kwargs={"pk": domain.id}),
|
||||
"action_url": reverse("domain", kwargs={"domain_pk": domain.id}),
|
||||
"action_label": ("View" if view_only else "Manage"),
|
||||
"svg_icon": ("visibility" if view_only else "settings"),
|
||||
"domain_info__sub_organization": suborganization_name,
|
||||
|
|
|
@ -6,16 +6,17 @@ from django.contrib.postgres.aggregates import ArrayAgg
|
|||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from registrar.decorators import HAS_PORTFOLIO_MEMBERS_ANY_PERM, grant_access
|
||||
from registrar.models.domain_invitation import DomainInvitation
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.views.utility.mixins import PortfolioMembersPermission
|
||||
from registrar.models.utility.orm_helper import ArrayRemoveNull
|
||||
from django.contrib.postgres.aggregates import StringAgg
|
||||
|
||||
|
||||
class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioMembersJson(View):
|
||||
|
||||
def get(self, request):
|
||||
"""Fetch members (permissions and invitations) for the given portfolio."""
|
||||
|
@ -236,7 +237,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
|||
),
|
||||
# split domain_info array values into ids to form urls, and names
|
||||
"domain_urls": [
|
||||
reverse("domain", kwargs={"pk": domain_info.split(":")[0]}) for domain_info in domain_info_list
|
||||
reverse("domain", kwargs={"domain_pk": domain_info.split(":")[0]}) for domain_info in domain_info_list
|
||||
],
|
||||
"domain_names": [domain_info.split(":")[1] for domain_info in domain_info_list],
|
||||
"is_admin": is_admin,
|
||||
|
|
|
@ -5,14 +5,26 @@ from django.http import Http404, JsonResponse
|
|||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import DetailView
|
||||
from django.contrib import messages
|
||||
from registrar.decorators import (
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
|
||||
HAS_PORTFOLIO_DOMAINS_ANY_PERM,
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM,
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT,
|
||||
IS_PORTFOLIO_MEMBER,
|
||||
grant_access,
|
||||
)
|
||||
from registrar.forms import portfolio as portfolioForms
|
||||
from registrar.models import Portfolio, User
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.domain_invitation import DomainInvitation
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
DomainInvitation,
|
||||
Portfolio,
|
||||
PortfolioInvitation,
|
||||
User,
|
||||
UserDomainRole,
|
||||
UserPortfolioPermission,
|
||||
)
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.utility.email import EmailSendingError
|
||||
from registrar.utility.email_invitations import (
|
||||
|
@ -20,22 +32,12 @@ from registrar.utility.email_invitations import (
|
|||
send_portfolio_admin_addition_emails,
|
||||
send_portfolio_admin_removal_emails,
|
||||
send_portfolio_invitation_email,
|
||||
send_portfolio_invitation_remove_email,
|
||||
send_portfolio_member_permission_remove_email,
|
||||
send_portfolio_member_permission_update_email,
|
||||
)
|
||||
from registrar.utility.errors import MissingEmailError
|
||||
from registrar.utility.enums import DefaultUserValues
|
||||
from registrar.views.utility.mixins import PortfolioMemberPermission
|
||||
from registrar.views.utility.permission_views import (
|
||||
PortfolioDomainRequestsPermissionView,
|
||||
PortfolioDomainsPermissionView,
|
||||
PortfolioBasePermissionView,
|
||||
NoPortfolioDomainsPermissionView,
|
||||
PortfolioMemberDomainsPermissionView,
|
||||
PortfolioMemberDomainsEditPermissionView,
|
||||
PortfolioMemberEditPermissionView,
|
||||
PortfolioMemberPermissionView,
|
||||
PortfolioMembersPermissionView,
|
||||
)
|
||||
from django.views.generic import View
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django.db import IntegrityError
|
||||
|
@ -46,7 +48,8 @@ from registrar.views.utility.invitation_helper import get_org_membership
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
|
||||
@grant_access(HAS_PORTFOLIO_DOMAINS_ANY_PERM)
|
||||
class PortfolioDomainsView(View):
|
||||
|
||||
template_name = "portfolio_domains.html"
|
||||
|
||||
|
@ -59,7 +62,8 @@ class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
|
|||
return render(request, "portfolio_domains.html", context)
|
||||
|
||||
|
||||
class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
||||
@grant_access(HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM)
|
||||
class PortfolioDomainRequestsView(View):
|
||||
|
||||
template_name = "portfolio_requests.html"
|
||||
|
||||
|
@ -67,8 +71,10 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
|||
return render(request, "portfolio_requests.html")
|
||||
|
||||
|
||||
class PortfolioMemberView(PortfolioMemberPermissionView, View):
|
||||
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioMemberView(DetailView, View):
|
||||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member.html"
|
||||
|
||||
def get(self, request, pk):
|
||||
|
@ -109,7 +115,8 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View):
|
|||
)
|
||||
|
||||
|
||||
class PortfolioMemberDeleteView(PortfolioMemberPermission, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioMemberDeleteView(View):
|
||||
|
||||
def post(self, request, pk):
|
||||
"""
|
||||
|
@ -118,60 +125,84 @@ class PortfolioMemberDeleteView(PortfolioMemberPermission, View):
|
|||
"""
|
||||
portfolio_member_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
member = portfolio_member_permission.user
|
||||
portfolio = portfolio_member_permission.portfolio
|
||||
|
||||
# Validate if the member can be removed
|
||||
error_message = self._validate_member_removal(request, member, portfolio)
|
||||
if error_message:
|
||||
return self._handle_error_response(request, error_message, pk)
|
||||
|
||||
# Attempt to send notification emails
|
||||
self._send_removal_notifications(request, portfolio_member_permission)
|
||||
|
||||
# Passed all error conditions, proceed with deletion
|
||||
portfolio_member_permission.delete()
|
||||
|
||||
# Return success response
|
||||
return self._handle_success_response(request, member.email)
|
||||
|
||||
def _validate_member_removal(self, request, member, portfolio):
|
||||
"""
|
||||
Check whether the member can be removed from the portfolio.
|
||||
Returns an error message if removal is not allowed; otherwise, returns None.
|
||||
"""
|
||||
active_requests_count = member.get_active_requests_count_in_portfolio(request)
|
||||
|
||||
support_url = "https://get.gov/contact/"
|
||||
|
||||
error_message = ""
|
||||
|
||||
if active_requests_count > 0:
|
||||
# If they have any in progress requests
|
||||
error_message = mark_safe( # nosec
|
||||
return mark_safe( # nosec
|
||||
"This member can't be removed from the organization because they have an active domain request. "
|
||||
f"Please <a class='usa-link' href='{support_url}' target='_blank'>contact us</a> to remove this member."
|
||||
)
|
||||
elif member.is_only_admin_of_portfolio(portfolio_member_permission.portfolio):
|
||||
# If they are the last manager of a domain
|
||||
error_message = (
|
||||
if member.is_only_admin_of_portfolio(portfolio):
|
||||
return (
|
||||
"There must be at least one admin in your organization. Give another member admin "
|
||||
"permissions, make sure they log into the registrar, and then remove this member."
|
||||
)
|
||||
return None
|
||||
|
||||
# From the Members Table page Else the Member Page
|
||||
if error_message:
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse(
|
||||
{"error": error_message},
|
||||
status=400,
|
||||
)
|
||||
else:
|
||||
messages.error(request, error_message)
|
||||
return redirect(reverse("member", kwargs={"pk": pk}))
|
||||
def _handle_error_response(self, request, error_message, pk):
|
||||
"""
|
||||
Return an error response (JSON or redirect with messages).
|
||||
"""
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse({"error": error_message}, status=400)
|
||||
messages.error(request, error_message)
|
||||
return redirect(reverse("member", kwargs={"pk": pk}))
|
||||
|
||||
# if member being removed is an admin
|
||||
if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_member_permission.roles:
|
||||
try:
|
||||
# attempt to send notification emails of the removal to other portfolio admins
|
||||
def _send_removal_notifications(self, request, portfolio_member_permission):
|
||||
"""
|
||||
Attempt to send notification emails about the member's removal.
|
||||
"""
|
||||
try:
|
||||
# Notify other portfolio admins if removing an admin
|
||||
if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_member_permission.roles:
|
||||
if not send_portfolio_admin_removal_emails(
|
||||
email=portfolio_member_permission.user.email,
|
||||
requestor=request.user,
|
||||
portfolio=portfolio_member_permission.portfolio,
|
||||
):
|
||||
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
||||
except Exception as e:
|
||||
self._handle_exceptions(e)
|
||||
messages.warning(request, "Could not send email notification to existing organization admins.")
|
||||
|
||||
# passed all error conditions
|
||||
portfolio_member_permission.delete()
|
||||
# Notify the member being removed
|
||||
if not send_portfolio_member_permission_remove_email(
|
||||
requestor=request.user, permissions=portfolio_member_permission
|
||||
):
|
||||
messages.warning(
|
||||
request, f"Could not send email notification to {portfolio_member_permission.user.email}"
|
||||
)
|
||||
except Exception as e:
|
||||
self._handle_exceptions(e)
|
||||
|
||||
# From the Members Table page Else the Member Page
|
||||
success_message = f"You've removed {member.email} from the organization."
|
||||
def _handle_success_response(self, request, member_email):
|
||||
"""
|
||||
Return a success response (JSON or redirect with messages).
|
||||
"""
|
||||
success_message = f"You've removed {member_email} from the organization."
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse({"success": success_message}, status=200)
|
||||
else:
|
||||
messages.success(request, success_message)
|
||||
return redirect(reverse("members"))
|
||||
messages.success(request, success_message)
|
||||
return redirect(reverse("members"))
|
||||
|
||||
def _handle_exceptions(self, exception):
|
||||
"""Handle exceptions raised during the process."""
|
||||
|
@ -186,8 +217,10 @@ class PortfolioMemberDeleteView(PortfolioMemberPermission, View):
|
|||
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
||||
|
||||
|
||||
class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
|
||||
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT)
|
||||
class PortfolioMemberEditView(DetailView, View):
|
||||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member_permissions.html"
|
||||
form_class = portfolioForms.PortfolioMemberForm
|
||||
|
||||
|
@ -203,6 +236,7 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
|
|||
{
|
||||
"form": form,
|
||||
"member": user,
|
||||
"portfolio_permission": portfolio_permission,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -266,7 +300,8 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
|
|||
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
||||
|
||||
|
||||
class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioMemberDomainsView(View):
|
||||
|
||||
template_name = "portfolio_member_domains.html"
|
||||
|
||||
|
@ -284,8 +319,10 @@ class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View):
|
|||
)
|
||||
|
||||
|
||||
class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, View):
|
||||
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT)
|
||||
class PortfolioMemberDomainsEditView(DetailView, View):
|
||||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member_domains_edit.html"
|
||||
|
||||
def get(self, request, pk):
|
||||
|
@ -319,32 +356,32 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
|
|||
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.
|
||||
|
@ -394,8 +431,10 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
|
|||
UserDomainRole.objects.filter(domain_id__in=removed_domain_ids, user=member).delete()
|
||||
|
||||
|
||||
class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
|
||||
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioInvitedMemberView(DetailView, View):
|
||||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member.html"
|
||||
# form_class = PortfolioInvitedMemberForm
|
||||
|
||||
|
@ -436,7 +475,8 @@ class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
|
|||
)
|
||||
|
||||
|
||||
class PortfolioInvitedMemberDeleteView(PortfolioMemberPermission, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioInvitedMemberDeleteView(View):
|
||||
|
||||
def post(self, request, pk):
|
||||
"""
|
||||
|
@ -445,16 +485,18 @@ class PortfolioInvitedMemberDeleteView(PortfolioMemberPermission, View):
|
|||
"""
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
|
||||
# if invitation being removed is an admin
|
||||
if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_invitation.roles:
|
||||
try:
|
||||
try:
|
||||
# if invitation being removed is an admin
|
||||
if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_invitation.roles:
|
||||
# attempt to send notification emails of the removal to portfolio admins
|
||||
if not send_portfolio_admin_removal_emails(
|
||||
email=portfolio_invitation.email, requestor=request.user, portfolio=portfolio_invitation.portfolio
|
||||
):
|
||||
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
||||
except Exception as e:
|
||||
self._handle_exceptions(e)
|
||||
if not send_portfolio_invitation_remove_email(requestor=request.user, invitation=portfolio_invitation):
|
||||
messages.warning(request, f"Could not send email notification to {portfolio_invitation.email}")
|
||||
except Exception as e:
|
||||
self._handle_exceptions(e)
|
||||
|
||||
portfolio_invitation.delete()
|
||||
|
||||
|
@ -479,8 +521,10 @@ class PortfolioInvitedMemberDeleteView(PortfolioMemberPermission, View):
|
|||
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
||||
|
||||
|
||||
class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
|
||||
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT)
|
||||
class PortfolioInvitedMemberEditView(DetailView, View):
|
||||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member_permissions.html"
|
||||
form_class = portfolioForms.PortfolioInvitedMemberForm
|
||||
|
||||
|
@ -548,7 +592,8 @@ class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
|
|||
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
||||
|
||||
|
||||
class PortfolioInvitedMemberDomainsView(PortfolioMemberDomainsPermissionView, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioInvitedMemberDomainsView(View):
|
||||
|
||||
template_name = "portfolio_member_domains.html"
|
||||
|
||||
|
@ -564,8 +609,11 @@ class PortfolioInvitedMemberDomainsView(PortfolioMemberDomainsPermissionView, Vi
|
|||
)
|
||||
|
||||
|
||||
class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT)
|
||||
class PortfolioInvitedMemberDomainsEditView(DetailView, View):
|
||||
|
||||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member_domains_edit.html"
|
||||
|
||||
def get(self, request, pk):
|
||||
|
@ -597,32 +645,32 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
|
|||
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.
|
||||
|
@ -690,7 +738,8 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
|
|||
).update(status=DomainInvitation.DomainInvitationStatus.CANCELED)
|
||||
|
||||
|
||||
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
||||
@grant_access(IS_PORTFOLIO_MEMBER)
|
||||
class PortfolioNoDomainsView(View):
|
||||
"""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.
|
||||
"""
|
||||
|
@ -719,7 +768,8 @@ class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
|||
return context
|
||||
|
||||
|
||||
class PortfolioNoDomainRequestsView(NoPortfolioDomainsPermissionView, View):
|
||||
@grant_access(IS_PORTFOLIO_MEMBER)
|
||||
class PortfolioNoDomainRequestsView(View):
|
||||
"""Some users have access to the underlying portfolio, but not any domain requests.
|
||||
This is a custom view which explains that to the user - and denotes who to contact.
|
||||
"""
|
||||
|
@ -748,7 +798,8 @@ class PortfolioNoDomainRequestsView(NoPortfolioDomainsPermissionView, View):
|
|||
return context
|
||||
|
||||
|
||||
class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin):
|
||||
@grant_access(IS_PORTFOLIO_MEMBER)
|
||||
class PortfolioOrganizationView(DetailView, FormMixin):
|
||||
"""
|
||||
View to handle displaying and updating the portfolio's organization details.
|
||||
"""
|
||||
|
@ -810,7 +861,8 @@ class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin):
|
|||
return reverse("organization")
|
||||
|
||||
|
||||
class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin):
|
||||
@grant_access(IS_PORTFOLIO_MEMBER)
|
||||
class PortfolioSeniorOfficialView(DetailView, FormMixin):
|
||||
"""
|
||||
View to handle displaying and updating the portfolio's senior official details.
|
||||
For now, this view is readonly.
|
||||
|
@ -841,7 +893,8 @@ class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin):
|
|||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
|
||||
class PortfolioMembersView(PortfolioMembersPermissionView, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioMembersView(View):
|
||||
|
||||
template_name = "portfolio_members.html"
|
||||
|
||||
|
@ -850,10 +903,13 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View):
|
|||
return render(request, "portfolio_members.html")
|
||||
|
||||
|
||||
class PortfolioAddMemberView(PortfolioMembersPermissionView, FormMixin):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
class PortfolioAddMemberView(DetailView, FormMixin):
|
||||
|
||||
template_name = "portfolio_members_add_new.html"
|
||||
form_class = portfolioForms.PortfolioNewMemberForm
|
||||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests to display the form."""
|
||||
|
|
|
@ -6,19 +6,17 @@ from django.shortcuts import render
|
|||
from django.contrib import admin
|
||||
from django.db.models import Avg, F
|
||||
|
||||
from registrar.views.utility.mixins import DomainAndRequestsReportsPermission, PortfolioReportsPermission
|
||||
from registrar.decorators import ALL, HAS_PORTFOLIO_MEMBERS_VIEW, IS_STAFF, grant_access
|
||||
from .. import models
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.utils.decorators import method_decorator
|
||||
from registrar.utility import csv_export
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class AnalyticsView(View):
|
||||
def get(self, request):
|
||||
thirty_days_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
||||
|
@ -178,7 +176,7 @@ class AnalyticsView(View):
|
|||
return render(request, "admin/analytics.html", context)
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class ExportDataType(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# match the CSV example with all the fields
|
||||
|
@ -188,7 +186,8 @@ class ExportDataType(View):
|
|||
return response
|
||||
|
||||
|
||||
class ExportDataTypeUser(DomainAndRequestsReportsPermission, View):
|
||||
@grant_access(ALL)
|
||||
class ExportDataTypeUser(View):
|
||||
"""Returns a domain report for a given user on the request"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -199,7 +198,8 @@ class ExportDataTypeUser(DomainAndRequestsReportsPermission, View):
|
|||
return response
|
||||
|
||||
|
||||
class ExportMembersPortfolio(PortfolioReportsPermission, View):
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_VIEW)
|
||||
class ExportMembersPortfolio(View):
|
||||
"""Returns a members report for a given portfolio"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -227,7 +227,7 @@ class ExportMembersPortfolio(PortfolioReportsPermission, View):
|
|||
return response
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class ExportDataFull(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Smaller export based on 1
|
||||
|
@ -237,7 +237,7 @@ class ExportDataFull(View):
|
|||
return response
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class ExportDataFederal(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Federal only
|
||||
|
@ -247,7 +247,7 @@ class ExportDataFederal(View):
|
|||
return response
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class ExportDomainRequestDataFull(View):
|
||||
"""Generates a downloaded report containing all Domain Requests (except started)"""
|
||||
|
||||
|
@ -259,7 +259,7 @@ class ExportDomainRequestDataFull(View):
|
|||
return response
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class ExportDataDomainsGrowth(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -272,7 +272,7 @@ class ExportDataDomainsGrowth(View):
|
|||
return response
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class ExportDataRequestsGrowth(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -285,7 +285,7 @@ class ExportDataRequestsGrowth(View):
|
|||
return response
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class ExportDataManagedDomains(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -297,7 +297,7 @@ class ExportDataManagedDomains(View):
|
|||
return response
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
@grant_access(IS_STAFF)
|
||||
class ExportDataUnmanagedDomains(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.db.models import ForeignKey, OneToOneField, ManyToManyField, ManyToO
|
|||
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views import View
|
||||
from registrar.decorators import IS_STAFF, grant_access
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.user import User
|
||||
|
@ -18,6 +19,7 @@ from registrar.utility.db_helpers import ignore_unique_violation
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
class TransferUserView(View):
|
||||
"""Transfer user methods that set up the transfer_user template and handle the forms on it."""
|
||||
|
||||
|
|
|
@ -4,22 +4,25 @@ import logging
|
|||
|
||||
from django.contrib import messages
|
||||
from django.http import QueryDict
|
||||
from django.views.generic import DetailView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from registrar.decorators import ALL, grant_access
|
||||
from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from registrar.models.user import User
|
||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||
from registrar.views.utility.permission_views import UserProfilePermissionView
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||
@grant_access(ALL)
|
||||
class UserProfileView(DetailView, FormMixin):
|
||||
"""
|
||||
Base View for the User Profile. Handles getting and setting the User Profile
|
||||
"""
|
||||
|
||||
model = User
|
||||
context_object_name = "user"
|
||||
template_name = "profile.html"
|
||||
form_class = UserProfileForm
|
||||
base_view_name = "user-profile"
|
||||
|
|
|
@ -1,13 +1,4 @@
|
|||
from .steps_helper import StepsHelper
|
||||
from .always_404 import always_404
|
||||
|
||||
from .permission_views import (
|
||||
DomainPermissionView,
|
||||
DomainRequestPermissionView,
|
||||
DomainRequestPermissionWithdrawView,
|
||||
DomainRequestWizardPermissionView,
|
||||
PortfolioMembersPermission,
|
||||
DomainRequestPortfolioViewonlyView,
|
||||
DomainInvitationPermissionCancelView,
|
||||
)
|
||||
from .api_views import get_senior_official_from_federal_agency_json
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.forms.models import model_to_dict
|
||||
from registrar.decorators import IS_STAFF, grant_access
|
||||
from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from registrar.utility.admin_helpers import get_action_needed_reason_default_email, get_rejection_reason_default_email
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.utility.constants import BranchChoices
|
||||
|
@ -11,8 +10,7 @@ from registrar.utility.constants import BranchChoices
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_member_required
|
||||
@grant_access(IS_STAFF)
|
||||
def get_senior_official_from_federal_agency_json(request):
|
||||
"""Returns federal_agency information as a JSON"""
|
||||
|
||||
|
@ -39,8 +37,7 @@ def get_senior_official_from_federal_agency_json(request):
|
|||
return JsonResponse({"error": "Senior Official not found"}, status=404)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_member_required
|
||||
@grant_access(IS_STAFF)
|
||||
def get_portfolio_json(request):
|
||||
"""Returns portfolio information as a JSON"""
|
||||
|
||||
|
@ -96,8 +93,7 @@ def get_portfolio_json(request):
|
|||
return JsonResponse(portfolio_dict)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_member_required
|
||||
@grant_access(IS_STAFF)
|
||||
def get_suborganization_list_json(request):
|
||||
"""Returns suborganization list information for a portfolio as a JSON"""
|
||||
|
||||
|
@ -119,8 +115,7 @@ def get_suborganization_list_json(request):
|
|||
return JsonResponse({"results": results, "pagination": {"more": False}})
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_member_required
|
||||
@grant_access(IS_STAFF)
|
||||
def get_federal_and_portfolio_types_from_federal_agency_json(request):
|
||||
"""Returns specific portfolio information as a JSON. Request must have
|
||||
both agency_name and organization_type."""
|
||||
|
@ -148,8 +143,7 @@ def get_federal_and_portfolio_types_from_federal_agency_json(request):
|
|||
return JsonResponse(response_data)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_member_required
|
||||
@grant_access(IS_STAFF)
|
||||
def get_action_needed_email_for_user_json(request):
|
||||
"""Returns a default action needed email for a given user"""
|
||||
|
||||
|
@ -173,8 +167,7 @@ def get_action_needed_email_for_user_json(request):
|
|||
return JsonResponse({"email": email}, status=200)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_member_required
|
||||
@grant_access(IS_STAFF)
|
||||
def get_rejection_email_for_user_json(request):
|
||||
"""Returns a default rejection email for a given user"""
|
||||
|
||||
|
|
|
@ -35,3 +35,10 @@ def custom_403_error_view(request, exception=None, context=None):
|
|||
if context is None:
|
||||
context = {}
|
||||
return render(request, "403.html", context=context, status=403)
|
||||
|
||||
|
||||
def custom_404_error_view(request, exception=None, context=None):
|
||||
"""Used to redirect 404 errors to a custom view"""
|
||||
if context is None:
|
||||
context = {}
|
||||
return render(request, "404.html", context=context, status=404)
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
"""Permissions-related mixin classes."""
|
||||
"""Mixin classes."""
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
DomainRequest,
|
||||
DomainInvitation,
|
||||
DomainInformation,
|
||||
UserDomainRole,
|
||||
)
|
||||
import logging
|
||||
|
||||
|
||||
|
@ -143,486 +134,3 @@ class OrderableFieldsMixin:
|
|||
# Infer the column name in a similar manner to how Django does
|
||||
method.short_description = field.replace("_", " ")
|
||||
return method
|
||||
|
||||
|
||||
class PermissionsLoginMixin(PermissionRequiredMixin):
|
||||
"""Mixin that redirects to login page if not logged in, otherwise 403."""
|
||||
|
||||
def handle_no_permission(self):
|
||||
self.raise_exception = self.request.user.is_authenticated
|
||||
return super().handle_no_permission()
|
||||
|
||||
|
||||
class DomainAndRequestsReportsPermission(PermissionsLoginMixin):
|
||||
"""Permission mixin for domain and requests csv downloads"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if self.request.user.is_restricted():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class PortfolioReportsPermission(PermissionsLoginMixin):
|
||||
"""Permission mixin for portfolio csv downloads"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if self.request.user.is_restricted():
|
||||
return False
|
||||
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if not self.request.user.has_view_members_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return self.request.user.is_org_user(self.request)
|
||||
|
||||
|
||||
class DomainPermission(PermissionsLoginMixin):
|
||||
"""Permission mixin that redirects to domain if user has access,
|
||||
otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if self.request.user.is_restricted():
|
||||
return False
|
||||
|
||||
pk = self.kwargs["pk"]
|
||||
# If pk is none then something went very wrong...
|
||||
if pk is None:
|
||||
raise ValueError("Primary key is None")
|
||||
|
||||
# test if domain in editable state
|
||||
if not self.in_editable_state(pk):
|
||||
return False
|
||||
|
||||
if self.can_access_other_user_domains(pk):
|
||||
return True
|
||||
|
||||
# user needs to have a role on the domain
|
||||
if not UserDomainRole.objects.filter(user=self.request.user, domain__id=pk).exists():
|
||||
return self.can_access_domain_via_portfolio(pk)
|
||||
|
||||
# if we need to check more about the nature of role, do it here.
|
||||
return True
|
||||
|
||||
def can_access_domain_via_portfolio(self, pk):
|
||||
"""Most views should not allow permission to portfolio users.
|
||||
If particular views allow access to the domain pages, they will need to override
|
||||
this function.
|
||||
"""
|
||||
return False
|
||||
|
||||
def in_editable_state(self, pk):
|
||||
"""Is the domain in an editable state"""
|
||||
|
||||
requested_domain = None
|
||||
if Domain.objects.filter(id=pk).exists():
|
||||
requested_domain = Domain.objects.get(id=pk)
|
||||
|
||||
# if domain is editable return true
|
||||
if requested_domain and requested_domain.is_editable():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_access_other_user_domains(self, pk):
|
||||
"""Checks to see if an authorized user (staff or superuser)
|
||||
can access a domain that they did not create or was invited to.
|
||||
"""
|
||||
|
||||
# Check if the user is permissioned...
|
||||
user_is_analyst_or_superuser = self.request.user.has_perm(
|
||||
"registrar.analyst_access_permission"
|
||||
) or self.request.user.has_perm("registrar.full_access_permission")
|
||||
|
||||
if not user_is_analyst_or_superuser:
|
||||
return False
|
||||
|
||||
# Check if the user is attempting a valid edit action.
|
||||
# In other words, if the analyst/admin did not click
|
||||
# the 'Manage Domain' button in /admin,
|
||||
# then they cannot access this page.
|
||||
session = self.request.session
|
||||
can_do_action = (
|
||||
"analyst_action" in session
|
||||
and "analyst_action_location" in session
|
||||
and session["analyst_action_location"] == pk
|
||||
)
|
||||
|
||||
if not can_do_action:
|
||||
return False
|
||||
|
||||
# Analysts may manage domains, when they are in these statuses:
|
||||
valid_domain_statuses = [
|
||||
DomainRequest.DomainRequestStatus.APPROVED,
|
||||
DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
# Edge case - some domains do not have
|
||||
# a status or DomainInformation... aka a status of 'None'.
|
||||
# It is necessary to access those to correct errors.
|
||||
None,
|
||||
]
|
||||
|
||||
requested_domain = None
|
||||
if DomainInformation.objects.filter(id=pk).exists():
|
||||
requested_domain = DomainInformation.objects.get(id=pk)
|
||||
|
||||
# if no domain information or domain request exist, the user
|
||||
# should be able to manage the domain; however, if domain information
|
||||
# and domain request exist, and domain request is not in valid status,
|
||||
# user should not be able to manage domain
|
||||
if (
|
||||
requested_domain
|
||||
and requested_domain.domain_request
|
||||
and requested_domain.domain_request.status not in valid_domain_statuses
|
||||
):
|
||||
return False
|
||||
|
||||
# Valid session keys exist,
|
||||
# the user is permissioned,
|
||||
# and it is in a valid status
|
||||
return True
|
||||
|
||||
|
||||
class DomainRequestPermission(PermissionsLoginMixin):
|
||||
"""Permission mixin that redirects to domain request if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain request.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# user needs to be the creator of the domain request
|
||||
# this query is empty if there isn't a domain request with this
|
||||
# id and this user as creator
|
||||
if not DomainRequest.objects.filter(creator=self.request.user, id=self.kwargs["pk"]).exists():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DomainRequestPortfolioViewonlyPermission(PermissionsLoginMixin):
|
||||
"""Permission mixin that redirects to domain request if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain request.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if not self.request.user.is_org_user(self.request):
|
||||
return False
|
||||
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if not self.request.user.has_view_all_requests_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class UserDeleteDomainRolePermission(PermissionsLoginMixin):
|
||||
"""Permission mixin for UserDomainRole if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain request.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
domain_pk = self.kwargs["pk"]
|
||||
user_pk = self.kwargs["user_pk"]
|
||||
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# Check if the UserDomainRole object exists, then check
|
||||
# if the user requesting the delete has permissions to do so
|
||||
has_delete_permission = UserDomainRole.objects.filter(
|
||||
user=user_pk,
|
||||
domain=domain_pk,
|
||||
domain__permissions__user=self.request.user,
|
||||
).exists()
|
||||
|
||||
user_is_analyst_or_superuser = self.request.user.has_perm(
|
||||
"registrar.analyst_access_permission"
|
||||
) or self.request.user.has_perm("registrar.full_access_permission")
|
||||
|
||||
if not (has_delete_permission or user_is_analyst_or_superuser):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DomainRequestPermissionWithdraw(PermissionsLoginMixin):
|
||||
"""Permission mixin that redirects to withdraw action on domain request
|
||||
if user has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to withdraw this domain request."""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# user needs to be the creator of the domain request
|
||||
# this query is empty if there isn't a domain request with this
|
||||
# id and this user as creator
|
||||
if not DomainRequest.objects.filter(creator=self.request.user, id=self.kwargs["pk"]).exists():
|
||||
return False
|
||||
|
||||
# Restricted users should not be able to withdraw domain requests
|
||||
if self.request.user.is_restricted():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DomainRequestWizardPermission(PermissionsLoginMixin):
|
||||
"""Permission mixin that redirects to start or edit domain request if
|
||||
user has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has permission to start or edit a domain request.
|
||||
|
||||
The user is in self.request.user
|
||||
"""
|
||||
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# The user has an ineligible flag
|
||||
if self.request.user.is_restricted():
|
||||
return False
|
||||
|
||||
# If the user is an org user and doesn't have add/edit perms, forbid this
|
||||
if self.request.user.is_org_user(self.request):
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
# user needs to be the creator of the domain request to edit it.
|
||||
id = self.kwargs.get("id") if hasattr(self, "kwargs") else None
|
||||
if not id:
|
||||
domain_request_wizard = self.request.session.get("wizard_domain_request")
|
||||
if domain_request_wizard:
|
||||
id = domain_request_wizard.get("domain_request_id")
|
||||
|
||||
# If no id is provided, we can assume that the user is starting a new request.
|
||||
# If one IS provided, check that they are the original creator of it.
|
||||
if id:
|
||||
if not DomainRequest.objects.filter(creator=self.request.user, id=id).exists():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DomainInvitationPermission(PermissionsLoginMixin):
|
||||
"""Permission mixin that redirects to domain invitation if user has
|
||||
access, otherwise 403"
|
||||
|
||||
A user has access to a domain invitation if they have a role on the
|
||||
associated domain.
|
||||
"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has a role on the domain of this invitation."""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if not DomainInvitation.objects.filter(
|
||||
id=self.kwargs["pk"], domain__permissions__user=self.request.user
|
||||
).exists():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class UserProfilePermission(PermissionsLoginMixin):
|
||||
"""Permission mixin that redirects to user profile if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access.
|
||||
|
||||
If the user is authenticated, they have access
|
||||
"""
|
||||
|
||||
# Check if the user is authenticated
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class PortfolioBasePermission(PermissionsLoginMixin):
|
||||
"""Permission mixin that redirects to portfolio pages if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to 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"]
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
return self.request.user.is_org_user(self.request)
|
||||
|
||||
|
||||
class PortfolioDomainsPermission(PortfolioBasePermission):
|
||||
"""Permission mixin that allows access to portfolio domain pages if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to 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_any_domains_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return super().has_permission()
|
||||
|
||||
|
||||
class PortfolioDomainRequestsPermission(PortfolioBasePermission):
|
||||
"""Permission mixin that allows access to portfolio domain request pages if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to domain requests 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_any_requests_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return super().has_permission()
|
||||
|
||||
|
||||
class PortfolioMembersPermission(PortfolioBasePermission):
|
||||
"""Permission mixin that allows access to portfolio members pages if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to members 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_view_members_portfolio_permission(
|
||||
portfolio
|
||||
) and not self.request.user.has_edit_members_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return super().has_permission()
|
||||
|
||||
|
||||
class PortfolioMemberPermission(PortfolioBasePermission):
|
||||
"""Permission mixin that allows access to portfolio member or invited member pages if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to members or invited members 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_view_members_portfolio_permission(
|
||||
portfolio
|
||||
) and not self.request.user.has_edit_members_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return super().has_permission()
|
||||
|
||||
|
||||
class PortfolioMemberEditPermission(PortfolioBasePermission):
|
||||
"""Permission mixin that allows access to portfolio member or invited member pages if user
|
||||
has access to edit, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to members or invited members 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()
|
||||
|
||||
|
||||
class PortfolioMemberDomainsPermission(PortfolioBasePermission):
|
||||
"""Permission mixin that allows access to portfolio member or invited member domains 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_view_members_portfolio_permission(
|
||||
portfolio
|
||||
) and not self.request.user.has_edit_members_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
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()
|
||||
|
|
|
@ -1,291 +0,0 @@
|
|||
"""View classes that enforce authorization."""
|
||||
|
||||
import abc # abstract base class
|
||||
|
||||
from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView
|
||||
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
|
||||
from registrar.models.user import User
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
|
||||
from .mixins import (
|
||||
DomainPermission,
|
||||
DomainRequestPermission,
|
||||
DomainRequestPermissionWithdraw,
|
||||
DomainInvitationPermission,
|
||||
DomainRequestWizardPermission,
|
||||
PortfolioDomainRequestsPermission,
|
||||
PortfolioDomainsPermission,
|
||||
PortfolioMemberDomainsPermission,
|
||||
PortfolioMemberDomainsEditPermission,
|
||||
PortfolioMemberEditPermission,
|
||||
UserDeleteDomainRolePermission,
|
||||
UserProfilePermission,
|
||||
PortfolioBasePermission,
|
||||
PortfolioMembersPermission,
|
||||
PortfolioMemberPermission,
|
||||
DomainRequestPortfolioViewonlyPermission,
|
||||
)
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
||||
"""Abstract base view for domains that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = Domain
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "domain"
|
||||
|
||||
# Adds context information for user permissions
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = self.request.user
|
||||
context["is_analyst_or_superuser"] = user.has_perm("registrar.analyst_access_permission") or user.has_perm(
|
||||
"registrar.full_access_permission"
|
||||
)
|
||||
context["is_domain_manager"] = UserDomainRole.objects.filter(user=user, domain=self.object).exists()
|
||||
context["is_portfolio_user"] = self.can_access_domain_via_portfolio(self.object.pk)
|
||||
context["is_editable"] = self.is_editable()
|
||||
# Stored in a variable for the linter
|
||||
action = "analyst_action"
|
||||
action_location = "analyst_action_location"
|
||||
# Flag to see if an analyst is attempting to make edits
|
||||
if action in self.request.session:
|
||||
context[action] = self.request.session[action]
|
||||
if action_location in self.request.session:
|
||||
context[action_location] = self.request.session[action_location]
|
||||
|
||||
return context
|
||||
|
||||
def is_editable(self):
|
||||
"""Returns whether domain is editable in the context of the view"""
|
||||
domain_editable = self.object.is_editable()
|
||||
if not domain_editable:
|
||||
return False
|
||||
|
||||
# if user is domain manager or analyst or admin, return True
|
||||
if (
|
||||
self.can_access_other_user_domains(self.object.id)
|
||||
or UserDomainRole.objects.filter(user=self.request.user, domain=self.object).exists()
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DomainRequestPermissionView(DomainRequestPermission, DetailView, abc.ABC):
|
||||
"""Abstract base view for domain requests that enforces permissions
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = DomainRequest
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "DomainRequest"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DomainRequestPortfolioViewonlyView(DomainRequestPortfolioViewonlyPermission, DetailView, abc.ABC):
|
||||
"""Abstract base view for domain requests that enforces permissions
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = DomainRequest
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "DomainRequest"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DomainRequestPermissionWithdrawView(DomainRequestPermissionWithdraw, DetailView, abc.ABC):
|
||||
"""Abstract base view for domain request withdraw function
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = DomainRequest
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "DomainRequest"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DomainRequestWizardPermissionView(DomainRequestWizardPermission, TemplateView, abc.ABC):
|
||||
"""Abstract base view for the domain request form that enforces permissions
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DomainInvitationPermissionCancelView(DomainInvitationPermission, UpdateView, abc.ABC):
|
||||
"""Abstract view for cancelling a DomainInvitation."""
|
||||
|
||||
model = DomainInvitation
|
||||
object: DomainInvitation
|
||||
|
||||
|
||||
class DomainRequestPermissionDeleteView(DomainRequestPermission, DeleteView, abc.ABC):
|
||||
"""Abstract view for deleting a DomainRequest."""
|
||||
|
||||
model = DomainRequest
|
||||
object: DomainRequest
|
||||
|
||||
|
||||
class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC):
|
||||
"""Abstract base view for deleting a UserDomainRole.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = UserDomainRole
|
||||
# workaround for type mismatch in DeleteView
|
||||
object: UserDomainRole
|
||||
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "userdomainrole"
|
||||
|
||||
|
||||
class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC):
|
||||
"""Abstract base view for user profile view that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = User
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "user"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PortfolioBasePermissionView(PortfolioBasePermission, DetailView, abc.ABC):
|
||||
"""Abstract base view for portfolio views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = Portfolio
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "portfolio"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PortfolioDomainsPermissionView(PortfolioDomainsPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio domains views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
|
||||
class NoPortfolioDomainsPermissionView(PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for a user without access to the
|
||||
portfolio domains views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
|
||||
class PortfolioDomainRequestsPermissionView(PortfolioDomainRequestsPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio domain request views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
|
||||
class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio members views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
|
||||
class PortfolioMemberPermissionView(PortfolioMemberPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio member views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
|
||||
class PortfolioMemberEditPermissionView(PortfolioMemberEditPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio member edit views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
|
||||
class PortfolioMemberDomainsPermissionView(PortfolioMemberDomainsPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio member domains views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`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