mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-20 17:56:11 +02:00
Merge pull request #2720 from cisagov/rjm/2351-org-requests-page
#2351: Org requests page - [RJM]
This commit is contained in:
commit
ccd1d0fb5a
35 changed files with 1103 additions and 229 deletions
|
@ -1168,7 +1168,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
|
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
|
||||||
const statusIndicator = document.querySelector('.domain__filter-indicator');
|
const statusIndicator = document.querySelector('.domain__filter-indicator');
|
||||||
const statusToggle = document.querySelector('.usa-button--filter');
|
const statusToggle = document.querySelector('.usa-button--filter');
|
||||||
const noPortfolioFlag = document.getElementById('no-portfolio-js-flag');
|
|
||||||
const portfolioElement = document.getElementById('portfolio-js-value');
|
const portfolioElement = document.getElementById('portfolio-js-value');
|
||||||
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
|
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
|
||||||
|
|
||||||
|
@ -1226,7 +1225,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
let markupForSuborganizationRow = '';
|
let markupForSuborganizationRow = '';
|
||||||
|
|
||||||
if (!noPortfolioFlag) {
|
if (portfolioValue) {
|
||||||
markupForSuborganizationRow = `
|
markupForSuborganizationRow = `
|
||||||
<td>
|
<td>
|
||||||
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
||||||
|
@ -1427,9 +1426,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// NOTE: We may need to evolve this as we add more filters.
|
// NOTE: We may need to evolve this as we add more filters.
|
||||||
document.addEventListener('focusin', function(event) {
|
document.addEventListener('focusin', function(event) {
|
||||||
const accordion = document.querySelector('.usa-accordion--select');
|
const accordion = document.querySelector('.usa-accordion--select');
|
||||||
const accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||||
|
|
||||||
if (accordionIsOpen && !accordion.contains(event.target)) {
|
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
||||||
closeFilters();
|
closeFilters();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1438,9 +1437,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// NOTE: We may need to evolve this as we add more filters.
|
// NOTE: We may need to evolve this as we add more filters.
|
||||||
document.addEventListener('click', function(event) {
|
document.addEventListener('click', function(event) {
|
||||||
const accordion = document.querySelector('.usa-accordion--select');
|
const accordion = document.querySelector('.usa-accordion--select');
|
||||||
const accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||||
|
|
||||||
if (accordionIsOpen && !accordion.contains(event.target)) {
|
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
||||||
closeFilters();
|
closeFilters();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1485,6 +1484,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
|
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
|
||||||
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
|
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
|
||||||
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
|
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
|
||||||
|
const portfolioElement = document.getElementById('portfolio-js-value');
|
||||||
|
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
|
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
|
||||||
|
@ -1533,7 +1534,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
* @param {*} scroll - control for the scrollToElement functionality
|
* @param {*} scroll - control for the scrollToElement functionality
|
||||||
* @param {*} searchTerm - the search term
|
* @param {*} searchTerm - the search term
|
||||||
*/
|
*/
|
||||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm) {
|
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
|
||||||
// fetch json of page of domain requests, given params
|
// fetch json of page of domain requests, given params
|
||||||
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
|
@ -1545,7 +1546,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
// fetch json of page of requests, given params
|
||||||
|
let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`
|
||||||
|
if (portfolio)
|
||||||
|
url += `&portfolio=${portfolio}`
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
|
@ -1601,10 +1607,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const actionLabel = request.action_label;
|
const actionLabel = request.action_label;
|
||||||
const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
|
const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
|
||||||
|
|
||||||
// Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed
|
// The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page)
|
||||||
|
// Even if the request is not deletable, we may need these empty strings for the td if the deletable column is displayed
|
||||||
let modalTrigger = '';
|
let modalTrigger = '';
|
||||||
|
|
||||||
// If the request is deletable, create modal body and insert it
|
let markupCreatorRow = '';
|
||||||
|
|
||||||
|
if (portfolioValue) {
|
||||||
|
markupCreatorRow = `
|
||||||
|
<td>
|
||||||
|
<span class="text-wrap break-word">${request.creator ? request.creator : ''}</span>
|
||||||
|
</td>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages
|
||||||
if (request.is_deletable) {
|
if (request.is_deletable) {
|
||||||
let modalHeading = '';
|
let modalHeading = '';
|
||||||
let modalDescription = '';
|
let modalDescription = '';
|
||||||
|
@ -1627,7 +1644,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
role="button"
|
role="button"
|
||||||
id="button-toggle-delete-domain-alert-${request.id}"
|
id="button-toggle-delete-domain-alert-${request.id}"
|
||||||
href="#toggle-delete-domain-alert-${request.id}"
|
href="#toggle-delete-domain-alert-${request.id}"
|
||||||
class="usa-button--unstyled text-no-underline late-loading-modal-trigger"
|
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger line-height-sans-5"
|
||||||
aria-controls="toggle-delete-domain-alert-${request.id}"
|
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||||
data-open-modal
|
data-open-modal
|
||||||
>
|
>
|
||||||
|
@ -1692,8 +1709,57 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
`
|
`
|
||||||
|
|
||||||
domainRequestsSectionWrapper.appendChild(modal);
|
domainRequestsSectionWrapper.appendChild(modal);
|
||||||
|
|
||||||
|
// Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly
|
||||||
|
if (portfolioValue) {
|
||||||
|
modalTrigger = `
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
id="button-toggle-delete-domain-alert-${request.id}"
|
||||||
|
href="#toggle-delete-domain-alert-${request.id}"
|
||||||
|
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 visible-mobile-flex line-height-sans-5"
|
||||||
|
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||||
|
data-open-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
|
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
|
||||||
|
<div class="usa-accordion__heading">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="more-actions-${request.id}"
|
||||||
|
>
|
||||||
|
<svg class="usa-icon top-2px" 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-${request.id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-0" hidden>
|
||||||
|
<h2>More options</h2>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
id="button-toggle-delete-domain-alert-${request.id}"
|
||||||
|
href="#toggle-delete-domain-alert-${request.id}"
|
||||||
|
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5"
|
||||||
|
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||||
|
data-open-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
|
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<th scope="row" role="rowheader" data-label="Domain name">
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
|
@ -1702,6 +1768,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
<td data-sort-value="${new Date(request.last_submitted_date).getTime()}" data-label="Date submitted">
|
<td data-sort-value="${new Date(request.last_submitted_date).getTime()}" data-label="Date submitted">
|
||||||
${submissionDate}
|
${submissionDate}
|
||||||
</td>
|
</td>
|
||||||
|
${markupCreatorRow}
|
||||||
<td data-label="Status">
|
<td data-label="Status">
|
||||||
${request.status}
|
${request.status}
|
||||||
</td>
|
</td>
|
||||||
|
@ -1817,6 +1884,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeMoreActionMenu(accordionThatIsOpen) {
|
||||||
|
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
|
||||||
|
accordionThatIsOpen.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('focusin', function(event) {
|
||||||
|
closeOpenAccordions(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
closeOpenAccordions(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeOpenAccordions(event) {
|
||||||
|
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
|
||||||
|
openAccordions.forEach((openAccordionButton) => {
|
||||||
|
// Find the corresponding accordion
|
||||||
|
const accordion = openAccordionButton.closest('.usa-accordion--more-actions');
|
||||||
|
if (accordion && !accordion.contains(event.target)) {
|
||||||
|
// Close the accordion if the click is outside
|
||||||
|
closeMoreActionMenu(openAccordionButton);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
loadDomainRequests(1);
|
loadDomainRequests(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
.usa-accordion--select {
|
.usa-accordion--select,
|
||||||
|
.usa-accordion--more-actions {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: auto;
|
width: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -14,7 +15,6 @@
|
||||||
// Note, width is determined by a custom width class on one of the children
|
// Note, width is determined by a custom width class on one of the children
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 33.88px;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px color('base-lighter');
|
border: solid 1px color('base-lighter');
|
||||||
|
@ -31,3 +31,17 @@
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-accordion--select .usa-accordion__content {
|
||||||
|
top: 33.88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-accordion--more-actions .usa-accordion__content {
|
||||||
|
top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child .usa-accordion--more-actions .usa-accordion__content {
|
||||||
|
top: auto;
|
||||||
|
bottom: -10px;
|
||||||
|
right: 30px;
|
||||||
|
}
|
||||||
|
|
|
@ -159,6 +159,23 @@ abbr[title] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden-mobile-flex {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
.visible-mobile-flex {
|
||||||
|
display: flex!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include at-media(tablet) {
|
||||||
|
.hidden-mobile-flex {
|
||||||
|
display: flex!important;
|
||||||
|
}
|
||||||
|
.visible-mobile-flex {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.flex-end {
|
.flex-end {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
@ -200,6 +217,11 @@ abbr[title] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-right-neg-4px {
|
// Boost this USWDS utility class for the accordions in the portfolio requests table
|
||||||
margin-right: -4px;
|
.left-auto {
|
||||||
|
left: auto!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-word {
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,14 +211,7 @@ a.usa-button--unstyled:visited {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dotgov-table a
|
||||||
.dotgov-table a,
|
|
||||||
.usa-link--icon {
|
|
||||||
&:visited {
|
|
||||||
color: color('primary');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a .usa-icon,
|
a .usa-icon,
|
||||||
.usa-button--with-icon .usa-icon {
|
.usa-button--with-icon .usa-icon {
|
||||||
height: 1.3em;
|
height: 1.3em;
|
||||||
|
@ -230,3 +223,9 @@ a .usa-icon,
|
||||||
height: 1.5em;
|
height: 1.5em;
|
||||||
width: 1.5em;
|
width: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.text-secondary,
|
||||||
|
button.text-secondary:hover,
|
||||||
|
.dotgov-table a.text-secondary {
|
||||||
|
color: $theme-color-error;
|
||||||
|
}
|
||||||
|
|
|
@ -89,16 +89,24 @@
|
||||||
.usa-nav__primary {
|
.usa-nav__primary {
|
||||||
.usa-nav-link,
|
.usa-nav-link,
|
||||||
.usa-nav-link:hover,
|
.usa-nav-link:hover,
|
||||||
.usa-nav-link:active {
|
.usa-nav-link:active,
|
||||||
|
button {
|
||||||
color: color('primary');
|
color: color('primary');
|
||||||
font-weight: font-weight('normal');
|
font-weight: font-weight('normal');
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.usa-current,
|
.usa-current,
|
||||||
.usa-current:hover,
|
.usa-current:hover,
|
||||||
.usa-current:active {
|
.usa-current:active,
|
||||||
|
button.usa-current {
|
||||||
font-weight: font-weight('bold');
|
font-weight: font-weight('bold');
|
||||||
}
|
}
|
||||||
|
button[aria-expanded="true"] {
|
||||||
|
color: color('white');
|
||||||
|
}
|
||||||
|
button:not(.usa-current):hover::after {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.usa-nav__secondary {
|
.usa-nav__secondary {
|
||||||
// I don't know why USWDS has this at 2 rem, which puts it out of alignment
|
// I don't know why USWDS has this at 2 rem, which puts it out of alignment
|
||||||
|
|
|
@ -79,6 +79,11 @@ urlpatterns = [
|
||||||
views.PortfolioDomainRequestsView.as_view(),
|
views.PortfolioDomainRequestsView.as_view(),
|
||||||
name="domain-requests",
|
name="domain-requests",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"no-organization-requests/",
|
||||||
|
views.PortfolioNoDomainRequestsView.as_view(),
|
||||||
|
name="no-portfolio-requests",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"organization/",
|
"organization/",
|
||||||
views.PortfolioOrganizationView.as_view(),
|
views.PortfolioOrganizationView.as_view(),
|
||||||
|
|
|
@ -60,35 +60,42 @@ def add_has_profile_feature_flag_to_context(request):
|
||||||
|
|
||||||
def portfolio_permissions(request):
|
def portfolio_permissions(request):
|
||||||
"""Make portfolio permissions for the request user available in global context"""
|
"""Make portfolio permissions for the request user available in global context"""
|
||||||
context = {
|
portfolio_context = {
|
||||||
"has_base_portfolio_permission": False,
|
"has_base_portfolio_permission": False,
|
||||||
"has_domains_portfolio_permission": False,
|
"has_any_domains_portfolio_permission": False,
|
||||||
"has_domain_requests_portfolio_permission": False,
|
"has_any_requests_portfolio_permission": False,
|
||||||
|
"has_edit_request_portfolio_permission": False,
|
||||||
|
"has_view_suborganization_portfolio_permission": False,
|
||||||
|
"has_edit_suborganization_portfolio_permission": False,
|
||||||
"has_view_members_portfolio_permission": False,
|
"has_view_members_portfolio_permission": False,
|
||||||
"has_edit_members_portfolio_permission": False,
|
"has_edit_members_portfolio_permission": False,
|
||||||
"has_view_suborganization": False,
|
|
||||||
"has_edit_suborganization": False,
|
|
||||||
"portfolio": None,
|
"portfolio": None,
|
||||||
"has_organization_feature_flag": False,
|
"has_organization_feature_flag": False,
|
||||||
|
"has_organization_requests_flag": False,
|
||||||
|
"has_organization_members_flag": False,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
|
# Linting: line too long
|
||||||
|
view_suborg = request.user.has_view_suborganization_portfolio_permission(portfolio)
|
||||||
|
edit_suborg = request.user.has_edit_suborganization_portfolio_permission(portfolio)
|
||||||
if portfolio:
|
if portfolio:
|
||||||
return {
|
return {
|
||||||
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(portfolio),
|
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(portfolio),
|
||||||
"has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(portfolio),
|
"has_edit_request_portfolio_permission": request.user.has_edit_request_portfolio_permission(portfolio),
|
||||||
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(
|
"has_view_suborganization_portfolio_permission": view_suborg,
|
||||||
portfolio
|
"has_edit_suborganization_portfolio_permission": edit_suborg,
|
||||||
),
|
"has_any_domains_portfolio_permission": request.user.has_any_domains_portfolio_permission(portfolio),
|
||||||
|
"has_any_requests_portfolio_permission": request.user.has_any_requests_portfolio_permission(portfolio),
|
||||||
"has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio),
|
"has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio),
|
||||||
"has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio),
|
"has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio),
|
||||||
"has_view_suborganization": request.user.has_view_suborganization(portfolio),
|
|
||||||
"has_edit_suborganization": request.user.has_edit_suborganization(portfolio),
|
|
||||||
"portfolio": portfolio,
|
"portfolio": portfolio,
|
||||||
"has_organization_feature_flag": True,
|
"has_organization_feature_flag": True,
|
||||||
|
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
|
||||||
|
"has_organization_members_flag": request.user.has_organization_members_flag(),
|
||||||
}
|
}
|
||||||
return context
|
return portfolio_context
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Handles cases where request.user might not exist
|
# Handles cases where request.user might not exist
|
||||||
return context
|
return portfolio_context
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-09-09 14:48
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0123_alter_portfolioinvitation_portfolio_additional_permissions_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="portfolioinvitation",
|
||||||
|
name="portfolio_additional_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_all_domains", "View all domains and domain reports"),
|
||||||
|
("view_managed_domains", "View managed domains"),
|
||||||
|
("view_members", "View members"),
|
||||||
|
("edit_members", "Create and edit members"),
|
||||||
|
("view_all_requests", "View all requests"),
|
||||||
|
("edit_requests", "Create and edit requests"),
|
||||||
|
("view_portfolio", "View organization"),
|
||||||
|
("edit_portfolio", "Edit organization"),
|
||||||
|
("view_suborganization", "View suborganization"),
|
||||||
|
("edit_suborganization", "Edit suborganization"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more additional permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userportfoliopermission",
|
||||||
|
name="additional_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_all_domains", "View all domains and domain reports"),
|
||||||
|
("view_managed_domains", "View managed domains"),
|
||||||
|
("view_members", "View members"),
|
||||||
|
("edit_members", "Create and edit members"),
|
||||||
|
("view_all_requests", "View all requests"),
|
||||||
|
("edit_requests", "Create and edit requests"),
|
||||||
|
("view_portfolio", "View organization"),
|
||||||
|
("edit_portfolio", "Edit organization"),
|
||||||
|
("view_suborganization", "View suborganization"),
|
||||||
|
("edit_suborganization", "Edit suborganization"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more additional permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -198,31 +198,25 @@ class User(AbstractUser):
|
||||||
def has_edit_org_portfolio_permission(self, portfolio):
|
def has_edit_org_portfolio_permission(self, portfolio):
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
|
||||||
|
|
||||||
def has_domains_portfolio_permission(self, portfolio):
|
def has_any_domains_portfolio_permission(self, portfolio):
|
||||||
return self._has_portfolio_permission(
|
return self._has_portfolio_permission(
|
||||||
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
|
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
|
||||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||||
|
|
||||||
def has_domain_requests_portfolio_permission(self, portfolio):
|
def has_organization_requests_flag(self):
|
||||||
# BEGIN
|
|
||||||
# Note code below is to add organization_request feature
|
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.user = self
|
request.user = self
|
||||||
has_organization_requests_flag = flag_is_active(request, "organization_requests")
|
return flag_is_active(request, "organization_requests")
|
||||||
if not has_organization_requests_flag:
|
|
||||||
return False
|
def has_organization_members_flag(self):
|
||||||
# END
|
request = HttpRequest()
|
||||||
return self._has_portfolio_permission(
|
request.user = self
|
||||||
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
return flag_is_active(request, "organization_members")
|
||||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
|
|
||||||
|
|
||||||
def has_view_members_portfolio_permission(self, portfolio):
|
def has_view_members_portfolio_permission(self, portfolio):
|
||||||
# BEGIN
|
# BEGIN
|
||||||
# Note code below is to add organization_request feature
|
# Note code below is to add organization_request feature
|
||||||
request = HttpRequest()
|
if not self.has_organization_members_flag():
|
||||||
request.user = self
|
|
||||||
has_organization_members_flag = flag_is_active(request, "organization_members")
|
|
||||||
if not has_organization_members_flag:
|
|
||||||
return False
|
return False
|
||||||
# END
|
# END
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
|
||||||
|
@ -230,23 +224,37 @@ class User(AbstractUser):
|
||||||
def has_edit_members_portfolio_permission(self, portfolio):
|
def has_edit_members_portfolio_permission(self, portfolio):
|
||||||
# BEGIN
|
# BEGIN
|
||||||
# Note code below is to add organization_request feature
|
# Note code below is to add organization_request feature
|
||||||
request = HttpRequest()
|
if not self.has_organization_members_flag():
|
||||||
request.user = self
|
|
||||||
has_organization_members_flag = flag_is_active(request, "organization_members")
|
|
||||||
if not has_organization_members_flag:
|
|
||||||
return False
|
return False
|
||||||
# END
|
# END
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
|
||||||
|
|
||||||
def has_view_all_domains_permission(self, portfolio):
|
def has_view_all_domains_portfolio_permission(self, portfolio):
|
||||||
"""Determines if the current user can view all available domains in a given portfolio"""
|
"""Determines if the current user can view all available domains in a given portfolio"""
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||||
|
|
||||||
|
def has_any_requests_portfolio_permission(self, portfolio):
|
||||||
|
# BEGIN
|
||||||
|
# Note code below is to add organization_request feature
|
||||||
|
if not self.has_organization_requests_flag():
|
||||||
|
return False
|
||||||
|
# END
|
||||||
|
return self._has_portfolio_permission(
|
||||||
|
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||||
|
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||||
|
|
||||||
|
def has_view_all_requests_portfolio_permission(self, portfolio):
|
||||||
|
"""Determines if the current user can view all available domain requests in a given portfolio"""
|
||||||
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||||
|
|
||||||
|
def has_edit_request_portfolio_permission(self, portfolio):
|
||||||
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||||
|
|
||||||
# Field specific permission checks
|
# Field specific permission checks
|
||||||
def has_view_suborganization(self, portfolio):
|
def has_view_suborganization_portfolio_permission(self, portfolio):
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
|
||||||
|
|
||||||
def has_edit_suborganization(self, portfolio):
|
def has_edit_suborganization_portfolio_permission(self, portfolio):
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
|
||||||
|
|
||||||
def get_first_portfolio(self):
|
def get_first_portfolio(self):
|
||||||
|
@ -255,36 +263,36 @@ class User(AbstractUser):
|
||||||
return permission.portfolio
|
return permission.portfolio
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def has_edit_requests(self, portfolio):
|
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
|
||||||
|
|
||||||
def portfolio_role_summary(self, portfolio):
|
def portfolio_role_summary(self, portfolio):
|
||||||
"""Returns a list of roles based on the user's permissions."""
|
"""Returns a list of roles based on the user's permissions."""
|
||||||
roles = []
|
roles = []
|
||||||
|
|
||||||
# Define the conditions and their corresponding roles
|
# Define the conditions and their corresponding roles
|
||||||
conditions_roles = [
|
conditions_roles = [
|
||||||
(self.has_edit_suborganization(portfolio), ["Admin"]),
|
(self.has_edit_suborganization_portfolio_permission(portfolio), ["Admin"]),
|
||||||
(
|
(
|
||||||
self.has_view_all_domains_permission(portfolio)
|
self.has_view_all_domains_portfolio_permission(portfolio)
|
||||||
and self.has_domain_requests_portfolio_permission(portfolio)
|
and self.has_any_requests_portfolio_permission(portfolio)
|
||||||
and self.has_edit_requests(portfolio),
|
and self.has_edit_request_portfolio_permission(portfolio),
|
||||||
["View-only admin", "Domain requestor"],
|
["View-only admin", "Domain requestor"],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
self.has_view_all_domains_permission(portfolio)
|
self.has_view_all_domains_portfolio_permission(portfolio)
|
||||||
and self.has_domain_requests_portfolio_permission(portfolio),
|
and self.has_any_requests_portfolio_permission(portfolio),
|
||||||
["View-only admin"],
|
["View-only admin"],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
self.has_base_portfolio_permission(portfolio)
|
self.has_base_portfolio_permission(portfolio)
|
||||||
and self.has_edit_requests(portfolio)
|
and self.has_edit_request_portfolio_permission(portfolio)
|
||||||
and self.has_domains_portfolio_permission(portfolio),
|
and self.has_any_domains_portfolio_permission(portfolio),
|
||||||
["Domain requestor", "Domain manager"],
|
["Domain requestor", "Domain manager"],
|
||||||
),
|
),
|
||||||
(self.has_base_portfolio_permission(portfolio) and self.has_edit_requests(portfolio), ["Domain requestor"]),
|
|
||||||
(
|
(
|
||||||
self.has_base_portfolio_permission(portfolio) and self.has_domains_portfolio_permission(portfolio),
|
self.has_base_portfolio_permission(portfolio) and self.has_edit_request_portfolio_permission(portfolio),
|
||||||
|
["Domain requestor"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.has_base_portfolio_permission(portfolio) and self.has_any_domains_portfolio_permission(portfolio),
|
||||||
["Domain manager"],
|
["Domain manager"],
|
||||||
),
|
),
|
||||||
(self.has_base_portfolio_permission(portfolio), ["Member"]),
|
(self.has_base_portfolio_permission(portfolio), ["Member"]),
|
||||||
|
@ -446,8 +454,6 @@ class User(AbstractUser):
|
||||||
self.check_domain_invitations_on_login()
|
self.check_domain_invitations_on_login()
|
||||||
self.check_portfolio_invitations_on_login()
|
self.check_portfolio_invitations_on_login()
|
||||||
|
|
||||||
# NOTE TO DAVE: I'd simply suggest that we move these functions outside of the user object,
|
|
||||||
# and move them to some sort of utility file. That way we aren't calling request inside here.
|
|
||||||
def is_org_user(self, request):
|
def is_org_user(self, request):
|
||||||
has_organization_feature_flag = flag_is_active(request, "organization_feature")
|
has_organization_feature_flag = flag_is_active(request, "organization_feature")
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
|
@ -456,7 +462,7 @@ class User(AbstractUser):
|
||||||
def get_user_domain_ids(self, request):
|
def get_user_domain_ids(self, request):
|
||||||
"""Returns either the domains ids associated with this user on UserDomainRole or Portfolio"""
|
"""Returns either the domains ids associated with this user on UserDomainRole or Portfolio"""
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
if self.is_org_user(request) and self.has_view_all_domains_permission(portfolio):
|
if self.is_org_user(request) and self.has_view_all_domains_portfolio_permission(portfolio):
|
||||||
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
||||||
else:
|
else:
|
||||||
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
|
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
|
||||||
|
|
|
@ -21,7 +21,6 @@ class UserPortfolioPermissionChoices(models.TextChoices):
|
||||||
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
||||||
|
|
||||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
||||||
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"
|
|
||||||
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
||||||
|
|
||||||
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from registrar.models.user import User
|
from registrar.models import User
|
||||||
from waffle.decorators import flag_is_active
|
from waffle.decorators import flag_is_active
|
||||||
|
|
||||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||||
|
@ -144,25 +144,30 @@ class CheckPortfolioMiddleware:
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# set the portfolio in the session if it is not set
|
# if multiple portfolios are allowed for this user
|
||||||
if "portfolio" not in request.session or request.session["portfolio"] is None:
|
if flag_is_active(request, "organization_feature"):
|
||||||
# if multiple portfolios are allowed for this user
|
self.set_portfolio_in_session(request)
|
||||||
if flag_is_active(request, "multiple_portfolios"):
|
elif request.session.get("portfolio"):
|
||||||
# NOTE: we will want to change later to have a workflow for selecting
|
# Edge case: User disables flag while already logged in
|
||||||
# portfolio and another for switching portfolio; for now, select first
|
request.session["portfolio"] = None
|
||||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
elif "portfolio" not in request.session:
|
||||||
elif flag_is_active(request, "organization_feature"):
|
# Set the portfolio in the session if its not already in it
|
||||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
request.session["portfolio"] = None
|
||||||
else:
|
|
||||||
request.session["portfolio"] = None
|
|
||||||
|
|
||||||
if request.session["portfolio"] is not None and current_path == self.home:
|
if request.user.is_org_user(request):
|
||||||
if request.user.is_org_user(request):
|
if current_path == self.home:
|
||||||
if request.user.has_domains_portfolio_permission(request.session["portfolio"]):
|
if request.user.has_any_domains_portfolio_permission(request.session["portfolio"]):
|
||||||
portfolio_redirect = reverse("domains")
|
portfolio_redirect = reverse("domains")
|
||||||
else:
|
else:
|
||||||
portfolio_redirect = reverse("no-portfolio-domains")
|
portfolio_redirect = reverse("no-portfolio-domains")
|
||||||
|
|
||||||
return HttpResponseRedirect(portfolio_redirect)
|
return HttpResponseRedirect(portfolio_redirect)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def set_portfolio_in_session(self, request):
|
||||||
|
# NOTE: we will want to change later to have a workflow for selecting
|
||||||
|
# portfolio and another for switching portfolio; for now, select first
|
||||||
|
if flag_is_active(request, "multiple_portfolios"):
|
||||||
|
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||||
|
else:
|
||||||
|
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||||
|
|
|
@ -72,9 +72,9 @@
|
||||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
|
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if portfolio and has_domains_portfolio_permission and has_view_suborganization %}
|
{% if portfolio and has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %}
|
||||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
{% url 'domain-suborganization' 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_suborganization %}
|
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% url 'domain-org-name-address' pk=domain.id as url %}
|
{% url 'domain-org-name-address' pk=domain.id as url %}
|
||||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
|
|
||||||
<div class="grid-row margin-top-1">
|
<div class="grid-row margin-top-1">
|
||||||
<div class="grid-col">
|
<div class="grid-col">
|
||||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record">
|
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record text-secondary line-height-sans-5">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg>Delete
|
</svg>Delete
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tablet:grid-col-2">
|
<div class="tablet:grid-col-2">
|
||||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon delete-record margin-bottom-075">
|
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon delete-record margin-bottom-075 text-secondary line-height-sans-5">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg>Delete
|
</svg>Delete
|
||||||
|
|
|
@ -16,6 +16,26 @@
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||||
</svg><span class="margin-left-05">Previous step</span>
|
</svg><span class="margin-left-05">Previous step</span>
|
||||||
</a>
|
</a>
|
||||||
|
{% comment %}
|
||||||
|
TODO: uncomment in #2596
|
||||||
|
{% else %}
|
||||||
|
{% if portfolio %}
|
||||||
|
{% url 'domain-requests' as url_2 %}
|
||||||
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||||
|
<ol class="usa-breadcrumb__list">
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
<a href="{{ url_2 }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||||
|
{% if requested_domain__name %}
|
||||||
|
<span>{{ requested_domain__name }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span>Start a new domain request</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{% endif %} {% endcomment %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block form_messages %}
|
{% block form_messages %}
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
|
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2">
|
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2 text-secondary line-height-sans-5">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg><span class="margin-left-05">Delete</span>
|
</svg><span class="margin-left-05">Delete</span>
|
||||||
|
|
|
@ -8,15 +8,33 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="main-content" class="grid-container">
|
<main id="main-content" class="grid-container">
|
||||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||||
<a href="{% url 'home' %}" class="breadcrumb__back">
|
{% comment %}
|
||||||
|
TODO: Uncomment in #2596
|
||||||
|
{% if portfolio %}
|
||||||
|
{% url 'domain-requests' as url %}
|
||||||
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||||
|
<ol class="usa-breadcrumb__list">
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||||
|
<span>{{ DomainRequest.requested_domain.name }}</span
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{% else %}{% endcomment %}
|
||||||
|
{% url 'home' as url %}
|
||||||
|
<a href="{{ url }}" class="breadcrumb__back">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||||
Back to manage your domains
|
Back to manage your domains
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
{% comment %} {% endif %}{% endcomment %}
|
||||||
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
||||||
<div
|
<div
|
||||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
{% comment %} Only show this menu option if the user has the perms to do so {% endcomment %}
|
{% comment %} Only show this menu option if the user has the perms to do so {% endcomment %}
|
||||||
{% if has_domains_portfolio_permission and has_view_suborganization %}
|
{% if has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %}
|
||||||
{% with url_name="domain-suborganization" %}
|
{% with url_name="domain-suborganization" %}
|
||||||
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
|
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
If you believe there is an error please contact <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
If you believe there is an error please contact <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if has_domains_portfolio_permission and has_edit_suborganization %}
|
{% if has_any_domains_portfolio_permission and has_edit_suborganization_portfolio_permission %}
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% input_with_errors form.sub_organization %}
|
{% input_with_errors form.sub_organization %}
|
||||||
|
|
|
@ -5,10 +5,13 @@
|
||||||
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
||||||
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
|
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
{% if not has_domain_requests_portfolio_permission %}
|
{% if not portfolio %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<!-- Embedding the portfolio value in a data attribute -->
|
||||||
|
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
||||||
|
@ -45,7 +48,10 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
|
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
|
||||||
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Date submitted</th>
|
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Submitted</th>
|
||||||
|
{% if portfolio %}
|
||||||
|
<th data-sortable="creator" scope="col" role="columnheader">Created by</th>
|
||||||
|
{% endif %}
|
||||||
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||||
<!-- AJAX will conditionally add a th for delete actions -->
|
<!-- AJAX will conditionally add a th for delete actions -->
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||||
{% if not portfolio %}
|
{% if not portfolio %}
|
||||||
<h2 id="domains-header" class="display-inline-block">Domains</h2>
|
<h2 id="domains-header" class="display-inline-block">Domains</h2>
|
||||||
<span class="display-none" id="no-portfolio-js-flag"></span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Embedding the portfolio value in a data attribute -->
|
<!-- Embedding the portfolio value in a data attribute -->
|
||||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||||
|
@ -157,7 +156,7 @@
|
||||||
<th data-sortable="name" scope="col" role="columnheader">Domain name</th>
|
<th data-sortable="name" scope="col" role="columnheader">Domain name</th>
|
||||||
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
||||||
<th data-sortable="state_display" scope="col" role="columnheader">Status</th>
|
<th data-sortable="state_display" scope="col" role="columnheader">Status</th>
|
||||||
{% if portfolio and has_view_suborganization %}
|
{% if portfolio and has_view_suborganization_portfolio_permission %}
|
||||||
<th data-sortable="domain_info__sub_organization" scope="col" role="columnheader">Suborganization</th>
|
<th data-sortable="domain_info__sub_organization" scope="col" role="columnheader">Suborganization</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th
|
<th
|
||||||
|
|
|
@ -37,9 +37,9 @@
|
||||||
</div>
|
</div>
|
||||||
<ul class="usa-nav__primary usa-accordion">
|
<ul class="usa-nav__primary usa-accordion">
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
{% if has_domains_portfolio_permission %}
|
{% if has_any_domains_portfolio_permission %}
|
||||||
{% url 'domains' as url %}
|
{% url 'domains' as url %}
|
||||||
{%else %}
|
{% else %}
|
||||||
{% url 'no-portfolio-domains' as url %}
|
{% url 'no-portfolio-domains' as url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
|
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
|
||||||
|
@ -52,21 +52,55 @@
|
||||||
</a>
|
</a>
|
||||||
</li> -->
|
</li> -->
|
||||||
|
|
||||||
{% if has_domain_requests_portfolio_permission %}
|
{% if has_organization_requests_flag %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
|
<!-- user has one of the view permissions plus the edit permission, show the dropdown -->
|
||||||
|
{% if has_edit_request_portfolio_permission %}
|
||||||
{% url 'domain-requests' as url %}
|
{% url 'domain-requests' as url %}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-accordion__button usa-nav__link{% if 'request'|in_path:request.path %} usa-current{% endif %}"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="basic-nav-section-two"
|
||||||
|
>
|
||||||
|
<span>Domain requests</span>
|
||||||
|
</button>
|
||||||
|
<ul id="basic-nav-section-two" class="usa-nav__submenu">
|
||||||
|
<li class="usa-nav__submenu-item">
|
||||||
|
<a href="{{ url }}"
|
||||||
|
><span>Domain requests</span></a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="usa-nav__submenu-item">
|
||||||
|
<a href="{% url 'domain-request:' %}"
|
||||||
|
><span>Start a new domain request</span></a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<!-- user has view but no edit permissions -->
|
||||||
|
{% elif has_any_requests_portfolio_permission %}
|
||||||
|
{% url 'domain-requests' as url %}
|
||||||
|
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
|
||||||
|
Domain requests
|
||||||
|
</a>
|
||||||
|
<!-- user does not have permissions -->
|
||||||
|
{% else %}
|
||||||
|
{% url 'no-portfolio-requests' as url %}
|
||||||
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
|
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
|
||||||
Domain requests
|
Domain requests
|
||||||
</a>
|
</a>
|
||||||
</li>
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if has_view_members_portfolio_permission %}
|
|
||||||
|
{% if has_organization_members_flag %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
<a href="#" class="usa-nav-link">
|
<a href="#" class="usa-nav-link">
|
||||||
Members
|
Members
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
{% url 'organization' as url %}
|
{% url 'organization' as url %}
|
||||||
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
|
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
|
||||||
|
|
30
src/registrar/templates/portfolio_no_requests.html
Normal file
30
src/registrar/templates/portfolio_no_requests.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends 'portfolio_base.html' %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %} Domain Requests | {% endblock %}
|
||||||
|
|
||||||
|
{% block portfolio_content %}
|
||||||
|
<h1 id="domains-header">Current domain requests</h1>
|
||||||
|
<section class="section-outlined">
|
||||||
|
<div class="section-outlined__header margin-bottom-3">
|
||||||
|
<h2 id="domains-header" class="display-inline-block">You don’t have access to domain requests.</h2>
|
||||||
|
{% if portfolio_administrators %}
|
||||||
|
<p>If you believe you should have access to a request, reach out to your organization’s administrators.</p>
|
||||||
|
<p>Your organizations administrators:</p>
|
||||||
|
<ul class="margin-top-0">
|
||||||
|
{% for administrator in portfolio_administrators %}
|
||||||
|
{% if administrator.email %}
|
||||||
|
<li>{{ administrator.email }}</li>
|
||||||
|
{% else %}
|
||||||
|
<li>{{ administrator }}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p><strong>No administrators were found on your organization.</strong></p>
|
||||||
|
<p>If you believe you should have access to a request, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
|
@ -11,18 +11,27 @@
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
<h1 id="domain-requests-header">Domain requests</h1>
|
<h1 id="domain-requests-header">Domain requests</h1>
|
||||||
|
<div class="grid-row grid-gap">
|
||||||
|
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||||
|
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||||
|
</div>
|
||||||
|
{% if has_edit_request_portfolio_permission %}
|
||||||
|
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||||
|
{% comment %}
|
||||||
|
IMPORTANT:
|
||||||
|
If this button is added on any other page, make sure to update the
|
||||||
|
relevant view to reset request.session["new_request"] = True
|
||||||
|
{% endcomment %}
|
||||||
|
<p class="float-right-tablet tablet:margin-y-0">
|
||||||
|
<a href="{% url 'domain-request:' %}" class="usa-button"
|
||||||
|
>
|
||||||
|
Start a new domain request
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
IMPORTANT:
|
|
||||||
If this button is added on any other page, make sure to update the
|
|
||||||
relevant view to reset request.session["new_request"] = True
|
|
||||||
{% endcomment %}
|
|
||||||
<p class="margin-top-4">
|
|
||||||
<a href="{% url 'domain-request:' %}" class="usa-button"
|
|
||||||
>
|
|
||||||
Start a new domain request
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
|
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1135,7 +1135,7 @@ class TestPortfolioInvitations(TestCase):
|
||||||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Hotel California")
|
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Hotel California")
|
||||||
self.portfolio_role_base = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
self.portfolio_role_base = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||||
self.portfolio_role_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
self.portfolio_role_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||||
self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS
|
self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||||
self.portfolio_permission_2 = UserPortfolioPermissionChoices.EDIT_REQUESTS
|
self.portfolio_permission_2 = UserPortfolioPermissionChoices.EDIT_REQUESTS
|
||||||
self.invitation, _ = PortfolioInvitation.objects.get_or_create(
|
self.invitation, _ = PortfolioInvitation.objects.get_or_create(
|
||||||
email=self.email,
|
email=self.email,
|
||||||
|
@ -1326,16 +1326,16 @@ class TestUser(TestCase):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
@patch.object(User, "has_edit_suborganization", return_value=True)
|
@patch.object(User, "has_edit_suborganization_portfolio_permission", return_value=True)
|
||||||
def test_portfolio_role_summary_admin(self, mock_edit_suborganization):
|
def test_portfolio_role_summary_admin(self, mock_edit_suborganization):
|
||||||
# Test if the user is recognized as an Admin
|
# Test if the user is recognized as an Admin
|
||||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"])
|
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"])
|
||||||
|
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User,
|
User,
|
||||||
has_view_all_domains_permission=lambda self, portfolio: True,
|
has_view_all_domains_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_domain_requests_portfolio_permission=lambda self, portfolio: True,
|
has_any_requests_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_edit_requests=lambda self, portfolio: True,
|
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self):
|
def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self):
|
||||||
# Test if the user has both 'View-only admin' and 'Domain requestor' roles
|
# Test if the user has both 'View-only admin' and 'Domain requestor' roles
|
||||||
|
@ -1343,8 +1343,8 @@ class TestUser(TestCase):
|
||||||
|
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User,
|
User,
|
||||||
has_view_all_domains_permission=lambda self, portfolio: True,
|
has_view_all_domains_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_domain_requests_portfolio_permission=lambda self, portfolio: True,
|
has_any_requests_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_view_only_admin(self):
|
def test_portfolio_role_summary_view_only_admin(self):
|
||||||
# Test if the user is recognized as a View-only admin
|
# Test if the user is recognized as a View-only admin
|
||||||
|
@ -1353,15 +1353,17 @@ class TestUser(TestCase):
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User,
|
User,
|
||||||
has_base_portfolio_permission=lambda self, portfolio: True,
|
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_edit_requests=lambda self, portfolio: True,
|
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_domains_portfolio_permission=lambda self, portfolio: True,
|
has_any_domains_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_member_domain_requestor_domain_manager(self):
|
def test_portfolio_role_summary_member_domain_requestor_domain_manager(self):
|
||||||
# Test if the user has 'Member', 'Domain requestor', and 'Domain manager' roles
|
# Test if the user has 'Member', 'Domain requestor', and 'Domain manager' roles
|
||||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor", "Domain manager"])
|
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor", "Domain manager"])
|
||||||
|
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User, has_base_portfolio_permission=lambda self, portfolio: True, has_edit_requests=lambda self, portfolio: True
|
User,
|
||||||
|
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||||
|
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_member_domain_requestor(self):
|
def test_portfolio_role_summary_member_domain_requestor(self):
|
||||||
# Test if the user has 'Member' and 'Domain requestor' roles
|
# Test if the user has 'Member' and 'Domain requestor' roles
|
||||||
|
@ -1370,7 +1372,7 @@ class TestUser(TestCase):
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User,
|
User,
|
||||||
has_base_portfolio_permission=lambda self, portfolio: True,
|
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_domains_portfolio_permission=lambda self, portfolio: True,
|
has_any_domains_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_member_domain_manager(self):
|
def test_portfolio_role_summary_member_domain_manager(self):
|
||||||
# Test if the user has 'Member' and 'Domain manager' roles
|
# Test if the user has 'Member' and 'Domain manager' roles
|
||||||
|
@ -1385,6 +1387,74 @@ class TestUser(TestCase):
|
||||||
# Test if the user has no roles
|
# Test if the user has no roles
|
||||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), [])
|
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), [])
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_base_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_base_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_edit_org_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_edit_org_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_any_domains_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.side_effect = [False, True] # First permission false, second permission true
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_any_domains_portfolio_permission(self.portfolio))
|
||||||
|
self.assertEqual(mock_has_permission.call_count, 2)
|
||||||
|
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||||
|
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_view_all_domains_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_view_all_domains_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_has_any_requests_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.side_effect = [False, True] # First permission false, second permission true
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_any_requests_portfolio_permission(self.portfolio))
|
||||||
|
self.assertEqual(mock_has_permission.call_count, 2)
|
||||||
|
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||||
|
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_view_all_requests_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_view_all_requests_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_edit_request_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_edit_request_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_view_suborganization_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_view_suborganization_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_edit_suborganization_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_edit_suborganization_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_check_transition_domains_without_domains_on_login(self):
|
def test_check_transition_domains_without_domains_on_login(self):
|
||||||
"""A user's on_each_login callback does not check transition domains.
|
"""A user's on_each_login callback does not check transition domains.
|
||||||
|
@ -1547,8 +1617,8 @@ class TestUser(TestCase):
|
||||||
|
|
||||||
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||||
|
|
||||||
self.assertFalse(user_can_view_all_domains)
|
self.assertFalse(user_can_view_all_domains)
|
||||||
self.assertFalse(user_can_view_all_requests)
|
self.assertFalse(user_can_view_all_requests)
|
||||||
|
@ -1562,8 +1632,8 @@ class TestUser(TestCase):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||||
|
|
||||||
self.assertTrue(user_can_view_all_domains)
|
self.assertTrue(user_can_view_all_domains)
|
||||||
self.assertFalse(user_can_view_all_requests)
|
self.assertFalse(user_can_view_all_requests)
|
||||||
|
@ -1572,16 +1642,16 @@ class TestUser(TestCase):
|
||||||
portfolio_permission.save()
|
portfolio_permission.save()
|
||||||
portfolio_permission.refresh_from_db()
|
portfolio_permission.refresh_from_db()
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||||
|
|
||||||
self.assertTrue(user_can_view_all_domains)
|
self.assertTrue(user_can_view_all_domains)
|
||||||
self.assertTrue(user_can_view_all_requests)
|
self.assertTrue(user_can_view_all_requests)
|
||||||
|
|
||||||
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||||
|
|
||||||
self.assertTrue(user_can_view_all_domains)
|
self.assertTrue(user_can_view_all_domains)
|
||||||
self.assertTrue(user_can_view_all_requests)
|
self.assertTrue(user_can_view_all_requests)
|
||||||
|
|
|
@ -12,10 +12,11 @@ from registrar.models import (
|
||||||
)
|
)
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from .common import create_test_user
|
from .common import MockSESClient, completed_domain_request, create_test_user
|
||||||
from waffle.testutils import override_flag
|
from waffle.testutils import override_flag
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
|
import boto3_mocking # type: ignore
|
||||||
|
from django.test import Client
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -24,6 +25,7 @@ logger = logging.getLogger(__name__)
|
||||||
class TestPortfolio(WebTest):
|
class TestPortfolio(WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
self.client = Client()
|
||||||
self.user = create_test_user()
|
self.user = create_test_user()
|
||||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
||||||
|
@ -76,7 +78,7 @@ class TestPortfolio(WebTest):
|
||||||
def test_middleware_does_not_redirect_if_no_permission(self):
|
def test_middleware_does_not_redirect_if_no_permission(self):
|
||||||
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
|
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
user=self.user, portfolio=self.portfolio, additional_permissions=[]
|
user=self.user, portfolio=self.portfolio, additional_permissions=[]
|
||||||
)
|
)
|
||||||
self.user.portfolio = self.portfolio
|
self.user.portfolio = self.portfolio
|
||||||
|
@ -504,7 +506,7 @@ class TestPortfolio(WebTest):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(reverse("home"), follow=True)
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
self.assertFalse(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
self.assertFalse(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "You aren")
|
self.assertContains(response, "You aren")
|
||||||
|
|
||||||
|
@ -519,7 +521,7 @@ class TestPortfolio(WebTest):
|
||||||
|
|
||||||
# Test the domains page - this user should have access
|
# Test the domains page - this user should have access
|
||||||
response = self.client.get(reverse("domains"))
|
response = self.client.get(reverse("domains"))
|
||||||
self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Domain name")
|
self.assertContains(response, "Domain name")
|
||||||
|
|
||||||
|
@ -530,7 +532,7 @@ class TestPortfolio(WebTest):
|
||||||
|
|
||||||
# Test the domains page - this user should have access
|
# Test the domains page - this user should have access
|
||||||
response = self.client.get(reverse("domains"))
|
response = self.client.get(reverse("domains"))
|
||||||
self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Domain name")
|
self.assertContains(response, "Domain name")
|
||||||
permission.delete()
|
permission.delete()
|
||||||
|
@ -547,7 +549,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
],
|
],
|
||||||
|
@ -573,7 +575,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
],
|
],
|
||||||
|
@ -599,7 +601,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
],
|
],
|
||||||
|
@ -630,3 +632,208 @@ class TestPortfolio(WebTest):
|
||||||
|
|
||||||
self.assertContains(home, "Hotel California")
|
self.assertContains(home, "Hotel California")
|
||||||
self.assertContains(home, "Members")
|
self.assertContains(home, "Members")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_portfolio_domain_requests_page_when_user_has_no_permissions(self):
|
||||||
|
"""Test the no requests page"""
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# create and submit a domain request
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
requests_page = self.client.get(reverse("no-portfolio-requests"), follow=True)
|
||||||
|
|
||||||
|
self.assertContains(requests_page, "You don’t have access to domain requests.")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_main_nav_when_user_has_no_permissions(self):
|
||||||
|
"""Test the nav contains a link to the no requests page"""
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# create and submit a domain request
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
# link to no requests
|
||||||
|
self.assertContains(portfolio_landing_page, "no-organization-requests/")
|
||||||
|
# dropdown
|
||||||
|
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
|
||||||
|
# link to requests
|
||||||
|
self.assertNotContains(portfolio_landing_page, 'href="/requests/')
|
||||||
|
# link to create
|
||||||
|
self.assertNotContains(portfolio_landing_page, 'href="/request/')
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_main_nav_when_user_has_all_permissions(self):
|
||||||
|
"""Test the nav contains a dropdown with a link to create and another link to view requests
|
||||||
|
Also test for the existence of the Create a new request btn on the requests page"""
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# create and submit a domain request
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
# link to no requests
|
||||||
|
self.assertNotContains(portfolio_landing_page, "no-organization-requests/")
|
||||||
|
# dropdown
|
||||||
|
self.assertContains(portfolio_landing_page, "basic-nav-section-two")
|
||||||
|
# link to requests
|
||||||
|
self.assertContains(portfolio_landing_page, 'href="/requests/')
|
||||||
|
# link to create
|
||||||
|
self.assertContains(portfolio_landing_page, 'href="/request/')
|
||||||
|
|
||||||
|
requests_page = self.client.get(reverse("domain-requests"))
|
||||||
|
|
||||||
|
# create new request btn
|
||||||
|
self.assertContains(requests_page, "Start a new domain request")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_main_nav_when_user_has_view_but_not_edit_permissions(self):
|
||||||
|
"""Test the nav contains a simple link to view requests
|
||||||
|
Also test for the existence of the Create a new request btn on the requests page"""
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# create and submit a domain request
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
# link to no requests
|
||||||
|
self.assertNotContains(portfolio_landing_page, "no-organization-requests/")
|
||||||
|
# dropdown
|
||||||
|
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
|
||||||
|
# link to requests
|
||||||
|
self.assertContains(portfolio_landing_page, 'href="/requests/')
|
||||||
|
# link to create
|
||||||
|
self.assertNotContains(portfolio_landing_page, 'href="/request/')
|
||||||
|
|
||||||
|
requests_page = self.client.get(reverse("domain-requests"))
|
||||||
|
|
||||||
|
# create new request btn
|
||||||
|
self.assertNotContains(requests_page, "Start a new domain request")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_organization_requests_additional_column(self):
|
||||||
|
"""The requests table has a column for created at"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
home = self.app.get(reverse("home")).follow()
|
||||||
|
|
||||||
|
self.assertContains(home, "Hotel California")
|
||||||
|
self.assertContains(home, "Domain requests")
|
||||||
|
|
||||||
|
domain_requests = self.app.get(reverse("domain-requests"))
|
||||||
|
self.assertEqual(domain_requests.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(domain_requests, "Created by")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_no_org_requests_no_additional_column(self):
|
||||||
|
"""The requests table does not have a column for created at"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
home = self.app.get(reverse("home"))
|
||||||
|
|
||||||
|
self.assertContains(home, "Domain requests")
|
||||||
|
self.assertNotContains(home, "Created by")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_portfolio_cache_updates_when_modified(self):
|
||||||
|
"""Test that the portfolio in session updates when the portfolio is modified"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
|
||||||
|
|
||||||
|
with override_flag("organization_feature", active=True):
|
||||||
|
# Initial request to set the portfolio in session
|
||||||
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
portfolio = self.client.session.get("portfolio")
|
||||||
|
self.assertEqual(portfolio.organization_name, "Hotel California")
|
||||||
|
self.assertContains(response, "Hotel California")
|
||||||
|
|
||||||
|
# Modify the portfolio
|
||||||
|
self.portfolio.organization_name = "Updated Hotel California"
|
||||||
|
self.portfolio.save()
|
||||||
|
|
||||||
|
# Make another request
|
||||||
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
# Check if the updated portfolio name is in the response
|
||||||
|
self.assertContains(response, "Updated Hotel California")
|
||||||
|
|
||||||
|
# Verify that the session contains the updated portfolio
|
||||||
|
portfolio = self.client.session.get("portfolio")
|
||||||
|
self.assertEqual(portfolio.organization_name, "Updated Hotel California")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_portfolio_cache_updates_when_flag_disabled_while_logged_in(self):
|
||||||
|
"""Test that the portfolio in session is set to None when the organization_feature flag is disabled"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
|
||||||
|
|
||||||
|
with override_flag("organization_feature", active=True):
|
||||||
|
# Initial request to set the portfolio in session
|
||||||
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
|
portfolio = self.client.session.get("portfolio")
|
||||||
|
self.assertEqual(portfolio.organization_name, "Hotel California")
|
||||||
|
self.assertContains(response, "Hotel California")
|
||||||
|
|
||||||
|
# Disable the organization_feature flag
|
||||||
|
with override_flag("organization_feature", active=False):
|
||||||
|
# Make another request
|
||||||
|
response = self.client.get(reverse("home"))
|
||||||
|
self.assertIsNone(self.client.session.get("portfolio"))
|
||||||
|
self.assertNotContains(response, "Hotel California")
|
||||||
|
|
|
@ -7,7 +7,7 @@ from api.tests.common import less_console_noise_decorator
|
||||||
from .common import MockSESClient, completed_domain_request # type: ignore
|
from .common import MockSESClient, completed_domain_request # type: ignore
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
from waffle.testutils import override_flag
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
DomainRequest,
|
DomainRequest,
|
||||||
DraftDomain,
|
DraftDomain,
|
||||||
|
@ -17,12 +17,14 @@ from registrar.models import (
|
||||||
User,
|
User,
|
||||||
Website,
|
Website,
|
||||||
FederalAgency,
|
FederalAgency,
|
||||||
|
Portfolio,
|
||||||
|
UserPortfolioPermission,
|
||||||
)
|
)
|
||||||
from registrar.views.domain_request import DomainRequestWizard, Step
|
from registrar.views.domain_request import DomainRequestWizard, Step
|
||||||
|
|
||||||
from .common import less_console_noise
|
from .common import less_console_noise
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -2925,6 +2927,39 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
response = self.client.get("/get-domain-requests-json/")
|
response = self.client.get("/get-domain-requests-json/")
|
||||||
self.assertContains(response, "Withdrawn")
|
self.assertContains(response, "Withdrawn")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_domain_request_withdraw_portfolio_redirects_correctly(self):
|
||||||
|
"""Tests that the withdraw button on portfolio redirects to the portfolio domain requests page"""
|
||||||
|
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
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, "Admin Tester")
|
||||||
|
self.assertContains(detail_page, "Status:")
|
||||||
|
# click the "Withdraw request" button
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
with less_console_noise():
|
||||||
|
withdraw_page = detail_page.click("Withdraw request")
|
||||||
|
self.assertContains(withdraw_page, "Withdraw request for")
|
||||||
|
home_page = withdraw_page.click("Withdraw request")
|
||||||
|
|
||||||
|
# Assert that it redirects to the portfolio requests page and the status has been updated to withdrawn
|
||||||
|
self.assertEqual(home_page.status_code, 302)
|
||||||
|
self.assertEqual(home_page.location, reverse("domain-requests"))
|
||||||
|
|
||||||
|
response = self.client.get("/get-domain-requests-json/")
|
||||||
|
self.assertContains(response, "Withdrawn")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_request_withdraw_no_permissions(self):
|
def test_domain_request_withdraw_no_permissions(self):
|
||||||
"""Can't withdraw domain requests as a restricted user."""
|
"""Can't withdraw domain requests as a restricted user."""
|
||||||
|
|
|
@ -2,9 +2,14 @@ from registrar.models import DomainRequest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from registrar.models.draft_domain import DraftDomain
|
from registrar.models.draft_domain import DraftDomain
|
||||||
|
from registrar.models.portfolio import Portfolio
|
||||||
|
from registrar.models.user import User
|
||||||
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
|
from waffle.testutils import override_flag
|
||||||
|
|
||||||
|
|
||||||
class GetRequestsJsonTest(TestWithUser, WebTest):
|
class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
|
@ -20,6 +25,19 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
|
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
|
||||||
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
|
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
|
||||||
|
|
||||||
|
# Create Portfolio
|
||||||
|
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Example org")
|
||||||
|
|
||||||
|
# create a second user to assign requests to
|
||||||
|
cls.user2 = User.objects.create(
|
||||||
|
username="test_user2",
|
||||||
|
first_name="Second",
|
||||||
|
last_name="last",
|
||||||
|
email="info2@example.com",
|
||||||
|
phone="8003111234",
|
||||||
|
title="title",
|
||||||
|
)
|
||||||
|
|
||||||
# Create domain requests for the user
|
# Create domain requests for the user
|
||||||
cls.domain_requests = [
|
cls.domain_requests = [
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
|
@ -28,6 +46,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
last_submitted_date="2024-01-01",
|
last_submitted_date="2024-01-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-01-01",
|
created_at="2024-01-01",
|
||||||
|
portfolio=cls.portfolio,
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=cls.user,
|
creator=cls.user,
|
||||||
|
@ -42,6 +61,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
last_submitted_date="2024-03-01",
|
last_submitted_date="2024-03-01",
|
||||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
created_at="2024-03-01",
|
created_at="2024-03-01",
|
||||||
|
portfolio=cls.portfolio,
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=cls.user,
|
creator=cls.user,
|
||||||
|
@ -113,6 +133,14 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED,
|
status=DomainRequest.DomainRequestStatus.APPROVED,
|
||||||
created_at="2024-12-01",
|
created_at="2024-12-01",
|
||||||
),
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=cls.user2,
|
||||||
|
requested_domain=None,
|
||||||
|
last_submitted_date="2024-12-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
created_at="2024-12-01",
|
||||||
|
portfolio=cls.portfolio,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -120,6 +148,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
DraftDomain.objects.all().delete()
|
DraftDomain.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
|
||||||
def test_get_domain_requests_json_authenticated(self):
|
def test_get_domain_requests_json_authenticated(self):
|
||||||
"""Test that domain requests are returned properly for an authenticated user."""
|
"""Test that domain requests are returned properly for an authenticated user."""
|
||||||
|
@ -262,6 +291,118 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
for expected_value, actual_value in zip(expected_domain_values, requested_domains):
|
for expected_value, actual_value in zip(expected_domain_values, requested_domains):
|
||||||
self.assertEqual(expected_value, actual_value)
|
self.assertEqual(expected_value, actual_value)
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_get_domain_requests_json_with_portfolio_view_all_requests(self):
|
||||||
|
"""Test that an authenticated user gets the list of 3 requests for portfolio. The 3 requests
|
||||||
|
are the requests that are associated with the portfolio."""
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"portfolio": self.portfolio.id})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
|
||||||
|
# Check the number of requests
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 3)
|
||||||
|
|
||||||
|
# Expected domain requests
|
||||||
|
expected_domain_requests = [self.domain_requests[0], self.domain_requests[2], self.domain_requests[13]]
|
||||||
|
|
||||||
|
# Extract fields from response
|
||||||
|
domain_request_ids = [domain_request["id"] for domain_request in data["domain_requests"]]
|
||||||
|
requested_domain = [domain_request["requested_domain"] for domain_request in data["domain_requests"]]
|
||||||
|
creator = [domain_request["creator"] for domain_request in data["domain_requests"]]
|
||||||
|
status = [domain_request["status"] for domain_request in data["domain_requests"]]
|
||||||
|
action_urls = [domain_request["action_url"] for domain_request in data["domain_requests"]]
|
||||||
|
action_labels = [domain_request["action_label"] for domain_request in data["domain_requests"]]
|
||||||
|
svg_icons = [domain_request["svg_icon"] for domain_request in data["domain_requests"]]
|
||||||
|
|
||||||
|
# Check fields for each domain_request
|
||||||
|
for i, expected_domain_request in enumerate(expected_domain_requests):
|
||||||
|
self.assertEqual(expected_domain_request.id, domain_request_ids[i])
|
||||||
|
if expected_domain_request.requested_domain:
|
||||||
|
self.assertEqual(expected_domain_request.requested_domain.name, requested_domain[i])
|
||||||
|
else:
|
||||||
|
self.assertIsNone(requested_domain[i])
|
||||||
|
self.assertEqual(expected_domain_request.creator.email, creator[i])
|
||||||
|
# Check action url, action label and svg icon
|
||||||
|
# Example domain requests will test each of below three scenarios
|
||||||
|
if creator[i] != self.user.email:
|
||||||
|
# Test case where action is View
|
||||||
|
self.assertEqual("View", action_labels[i])
|
||||||
|
self.assertEqual("#", action_urls[i])
|
||||||
|
self.assertEqual("visibility", svg_icons[i])
|
||||||
|
elif status[i] in [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED.label,
|
||||||
|
DomainRequest.DomainRequestStatus.ACTION_NEEDED.label,
|
||||||
|
DomainRequest.DomainRequestStatus.WITHDRAWN.label,
|
||||||
|
]:
|
||||||
|
# Test case where action is Edit
|
||||||
|
self.assertEqual("Edit", action_labels[i])
|
||||||
|
self.assertEqual(
|
||||||
|
reverse("edit-domain-request", kwargs={"id": expected_domain_request.id}), action_urls[i]
|
||||||
|
)
|
||||||
|
self.assertEqual("edit", svg_icons[i])
|
||||||
|
else:
|
||||||
|
# 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]
|
||||||
|
)
|
||||||
|
self.assertEqual("settings", svg_icons[i])
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_get_domain_requests_json_with_portfolio_edit_requests(self):
|
||||||
|
"""Test that an authenticated user gets the list of 2 requests for portfolio. The 2 requests
|
||||||
|
are the requests that are associated with the portfolio and owned by self.user."""
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"portfolio": self.portfolio.id})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
|
||||||
|
# Check the number of requests
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 2)
|
||||||
|
|
||||||
|
# Expected domain requests
|
||||||
|
expected_domain_requests = [self.domain_requests[0], self.domain_requests[2]]
|
||||||
|
|
||||||
|
# Extract fields from response, since other tests test all fields, only ids and requested
|
||||||
|
# domains tested in this test
|
||||||
|
domain_request_ids = [domain_request["id"] for domain_request in data["domain_requests"]]
|
||||||
|
requested_domain = [domain_request["requested_domain"] for domain_request in data["domain_requests"]]
|
||||||
|
|
||||||
|
# Check fields for each domain_request
|
||||||
|
for i, expected_domain_request in enumerate(expected_domain_requests):
|
||||||
|
self.assertEqual(expected_domain_request.id, domain_request_ids[i])
|
||||||
|
if expected_domain_request.requested_domain:
|
||||||
|
self.assertEqual(expected_domain_request.requested_domain.name, requested_domain[i])
|
||||||
|
else:
|
||||||
|
self.assertIsNone(requested_domain[i])
|
||||||
|
|
||||||
def test_pagination(self):
|
def test_pagination(self):
|
||||||
"""Test that pagination works properly. There are 11 total non-approved requests and
|
"""Test that pagination works properly. There are 11 total non-approved requests and
|
||||||
a page size of 10"""
|
a page size of 10"""
|
||||||
|
|
|
@ -175,7 +175,7 @@ class DomainView(DomainBaseView):
|
||||||
If particular views allow permissions, they will need to override
|
If particular views allow permissions, they will need to override
|
||||||
this function."""
|
this function."""
|
||||||
portfolio = self.request.session.get("portfolio")
|
portfolio = self.request.session.get("portfolio")
|
||||||
if self.request.user.has_domains_portfolio_permission(portfolio):
|
if self.request.user.has_any_domains_portfolio_permission(portfolio):
|
||||||
if Domain.objects.filter(id=pk).exists():
|
if Domain.objects.filter(id=pk).exists():
|
||||||
domain = Domain.objects.get(id=pk)
|
domain = Domain.objects.get(id=pk)
|
||||||
if domain.domain_info.portfolio == portfolio:
|
if domain.domain_info.portfolio == portfolio:
|
||||||
|
|
|
@ -152,7 +152,14 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
except DomainRequest.DoesNotExist:
|
except DomainRequest.DoesNotExist:
|
||||||
logger.debug("DomainRequest id %s did not have a DomainRequest" % id)
|
logger.debug("DomainRequest id %s did not have a DomainRequest" % id)
|
||||||
|
|
||||||
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
|
# If a user is creating a request, we assume that perms are handled upstream
|
||||||
|
if self.request.user.is_org_user(self.request):
|
||||||
|
self._domain_request = DomainRequest.objects.create(
|
||||||
|
creator=self.request.user,
|
||||||
|
portfolio=self.request.session.get("portfolio"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
|
||||||
|
|
||||||
self.storage["domain_request_id"] = self._domain_request.id
|
self.storage["domain_request_id"] = self._domain_request.id
|
||||||
return self._domain_request
|
return self._domain_request
|
||||||
|
@ -395,6 +402,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
"""Define context for access on all wizard pages."""
|
"""Define context for access on all wizard pages."""
|
||||||
|
|
||||||
|
requested_domain_name = None
|
||||||
|
if self.domain_request.requested_domain is not None:
|
||||||
|
requested_domain_name = self.domain_request.requested_domain.name
|
||||||
|
|
||||||
context_stuff = {}
|
context_stuff = {}
|
||||||
if DomainRequest._form_complete(self.domain_request, self.request):
|
if DomainRequest._form_complete(self.domain_request, self.request):
|
||||||
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
||||||
|
@ -411,6 +422,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
You’ll only be able to withdraw your request.",
|
You’ll only be able to withdraw your request.",
|
||||||
"review_form_is_complete": True,
|
"review_form_is_complete": True,
|
||||||
"user": self.request.user,
|
"user": self.request.user,
|
||||||
|
"requested_domain__name": requested_domain_name,
|
||||||
}
|
}
|
||||||
else: # form is not complete
|
else: # form is not complete
|
||||||
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
||||||
|
@ -426,6 +438,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
Return to the request and visit the steps that are marked as "incomplete."',
|
Return to the request and visit the steps that are marked as "incomplete."',
|
||||||
"review_form_is_complete": False,
|
"review_form_is_complete": False,
|
||||||
"user": self.request.user,
|
"user": self.request.user,
|
||||||
|
"requested_domain__name": requested_domain_name,
|
||||||
}
|
}
|
||||||
return context_stuff
|
return context_stuff
|
||||||
|
|
||||||
|
@ -505,7 +518,11 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
# if user opted to save progress and return,
|
# if user opted to save progress and return,
|
||||||
# return them to the home page
|
# return them to the home page
|
||||||
if button == "save_and_return":
|
if button == "save_and_return":
|
||||||
return HttpResponseRedirect(reverse("home"))
|
if request.user.is_org_user(request):
|
||||||
|
return HttpResponseRedirect(reverse("domain-requests"))
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(reverse("home"))
|
||||||
|
|
||||||
# otherwise, proceed as normal
|
# otherwise, proceed as normal
|
||||||
return self.goto_next_step()
|
return self.goto_next_step()
|
||||||
|
|
||||||
|
@ -774,7 +791,10 @@ class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
||||||
domain_request = DomainRequest.objects.get(id=self.kwargs["pk"])
|
domain_request = DomainRequest.objects.get(id=self.kwargs["pk"])
|
||||||
domain_request.withdraw()
|
domain_request.withdraw()
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
return HttpResponseRedirect(reverse("home"))
|
if self.request.user.is_org_user(self.request):
|
||||||
|
return HttpResponseRedirect(reverse("domain-requests"))
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(reverse("home"))
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
|
|
|
@ -10,16 +10,59 @@ from django.db.models import Q
|
||||||
@login_required
|
@login_required
|
||||||
def get_domain_requests_json(request):
|
def get_domain_requests_json(request):
|
||||||
"""Given the current request,
|
"""Given the current request,
|
||||||
get all domain requests that are associated with the request user and exclude the APPROVED ones"""
|
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_requests = DomainRequest.objects.filter(creator=request.user).exclude(
|
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_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(domain_request, request.user) for domain_request in page_obj.object_list
|
||||||
|
]
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"domain_requests": domain_requests,
|
||||||
|
"has_next": page_obj.has_next(),
|
||||||
|
"has_previous": page_obj.has_previous(),
|
||||||
|
"page": page_obj.number,
|
||||||
|
"num_pages": paginator.num_pages,
|
||||||
|
"total": paginator.count,
|
||||||
|
"unfiltered_total": unfiltered_total,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_domain_request_ids_from_request(request):
|
||||||
|
"""Get domain request ids from request.
|
||||||
|
|
||||||
|
If portfolio specified, return domain request ids associated with portfolio.
|
||||||
|
Otherwise, return domain request ids associated with request.user.
|
||||||
|
"""
|
||||||
|
portfolio = request.GET.get("portfolio")
|
||||||
|
filter_condition = Q(creator=request.user)
|
||||||
|
if portfolio:
|
||||||
|
if request.user.is_org_user(request) and request.user.has_view_all_requests_portfolio_permission(portfolio):
|
||||||
|
filter_condition = Q(portfolio=portfolio)
|
||||||
|
else:
|
||||||
|
filter_condition = Q(portfolio=portfolio, creator=request.user)
|
||||||
|
domain_requests = DomainRequest.objects.filter(filter_condition).exclude(
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED
|
status=DomainRequest.DomainRequestStatus.APPROVED
|
||||||
)
|
)
|
||||||
unfiltered_total = domain_requests.count()
|
return domain_requests.values_list("id", flat=True)
|
||||||
|
|
||||||
# Handle sorting
|
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
def apply_search(queryset, request):
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
|
||||||
search_term = request.GET.get("search_term")
|
search_term = request.GET.get("search_term")
|
||||||
|
|
||||||
if search_term:
|
if search_term:
|
||||||
|
@ -30,70 +73,60 @@ def get_domain_requests_json(request):
|
||||||
# If yes, we should return domain requests that do not have a
|
# If yes, we should return domain requests that do not have a
|
||||||
# requested_domain (those display as New domain request in the UI)
|
# requested_domain (those display as New domain request in the UI)
|
||||||
if search_term_lower in new_domain_request_text:
|
if search_term_lower in new_domain_request_text:
|
||||||
domain_requests = domain_requests.filter(
|
queryset = queryset.filter(
|
||||||
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
domain_requests = domain_requests.filter(Q(requested_domain__name__icontains=search_term))
|
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def apply_sorting(queryset, request):
|
||||||
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
|
||||||
if order == "desc":
|
if order == "desc":
|
||||||
sort_by = f"-{sort_by}"
|
sort_by = f"-{sort_by}"
|
||||||
domain_requests = domain_requests.order_by(sort_by)
|
return queryset.order_by(sort_by)
|
||||||
page_number = request.GET.get("page", 1)
|
|
||||||
paginator = Paginator(domain_requests, 10)
|
|
||||||
page_obj = paginator.get_page(page_number)
|
|
||||||
|
|
||||||
domain_requests_data = [
|
|
||||||
{
|
def serialize_domain_request(domain_request, user):
|
||||||
"requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None,
|
# Determine if the request is deletable
|
||||||
"last_submitted_date": domain_request.last_submitted_date,
|
is_deletable = domain_request.status in [
|
||||||
"status": domain_request.get_status_display(),
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
"created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
"id": domain_request.id,
|
|
||||||
"is_deletable": domain_request.status
|
|
||||||
in [DomainRequest.DomainRequestStatus.STARTED, DomainRequest.DomainRequestStatus.WITHDRAWN],
|
|
||||||
"action_url": (
|
|
||||||
reverse("edit-domain-request", kwargs={"id": domain_request.id})
|
|
||||||
if domain_request.status
|
|
||||||
in [
|
|
||||||
DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
|
||||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
]
|
|
||||||
else reverse("domain-request-status", kwargs={"pk": domain_request.id})
|
|
||||||
),
|
|
||||||
"action_label": (
|
|
||||||
"Edit"
|
|
||||||
if domain_request.status
|
|
||||||
in [
|
|
||||||
DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
|
||||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
]
|
|
||||||
else "Manage"
|
|
||||||
),
|
|
||||||
"svg_icon": (
|
|
||||||
"edit"
|
|
||||||
if domain_request.status
|
|
||||||
in [
|
|
||||||
DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
|
||||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
]
|
|
||||||
else "settings"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for domain_request in page_obj
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return JsonResponse(
|
# Determine action label based on user permissions and request status
|
||||||
{
|
editable_statuses = [
|
||||||
"domain_requests": domain_requests_data,
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
"has_next": page_obj.has_next(),
|
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||||
"has_previous": page_obj.has_previous(),
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
"page": page_obj.number,
|
]
|
||||||
"num_pages": paginator.num_pages,
|
|
||||||
"total": paginator.count,
|
if user.has_edit_request_portfolio_permission and domain_request.creator == user:
|
||||||
"unfiltered_total": unfiltered_total,
|
action_label = "Edit" if domain_request.status in editable_statuses else "Manage"
|
||||||
}
|
else:
|
||||||
)
|
action_label = "View"
|
||||||
|
|
||||||
|
# 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": "#",
|
||||||
|
}
|
||||||
|
|
||||||
|
svg_icon_map = {"Edit": "edit", "Manage": "settings", "View": "visibility"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None,
|
||||||
|
"last_submitted_date": domain_request.last_submitted_date,
|
||||||
|
"status": domain_request.get_status_display(),
|
||||||
|
"created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601
|
||||||
|
"creator": domain_request.creator.email,
|
||||||
|
"id": domain_request.id,
|
||||||
|
"is_deletable": is_deletable,
|
||||||
|
"action_url": action_url_map.get(action_label),
|
||||||
|
"action_label": action_label,
|
||||||
|
"svg_icon": svg_icon_map.get(action_label),
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from registrar.models import UserDomainRole, Domain, DomainInformation
|
from registrar.models import UserDomainRole, Domain, DomainInformation, User
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
@ -50,7 +50,8 @@ def get_domain_ids_from_request(request):
|
||||||
"""
|
"""
|
||||||
portfolio = request.GET.get("portfolio")
|
portfolio = request.GET.get("portfolio")
|
||||||
if portfolio:
|
if portfolio:
|
||||||
if request.user.is_org_user(request) and request.user.has_view_all_domains_permission(portfolio):
|
current_user: User = request.user
|
||||||
|
if current_user.is_org_user(request) and current_user.has_view_all_domains_portfolio_permission(portfolio):
|
||||||
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
||||||
return domain_infos.values_list("domain_id", flat=True)
|
return domain_infos.values_list("domain_id", flat=True)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -42,12 +42,41 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
||||||
|
|
||||||
|
|
||||||
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
||||||
"""Some users have access to the underlying portfolio, but not any domains.
|
"""Some users have access to the underlying portfolio, but not any domains.
|
||||||
This is a custom view which explains that to the user - and denotes who to contact.
|
This is a custom view which explains that to the user - and denotes who to contact.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = Portfolio
|
model = Portfolio
|
||||||
template_name = "no_portfolio_domains.html"
|
template_name = "portfolio_no_domains.html"
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return render(request, self.template_name, context=self.get_context_data())
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""Add additional context data to the template."""
|
||||||
|
# We can override the base class. This view only needs this item.
|
||||||
|
context = {}
|
||||||
|
portfolio = self.request.session.get("portfolio")
|
||||||
|
if portfolio:
|
||||||
|
admin_ids = UserPortfolioPermission.objects.filter(
|
||||||
|
portfolio=portfolio,
|
||||||
|
roles__overlap=[
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
],
|
||||||
|
).values_list("user__id", flat=True)
|
||||||
|
|
||||||
|
admin_users = User.objects.filter(id__in=admin_ids)
|
||||||
|
context["portfolio_administrators"] = admin_users
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioNoDomainRequestsView(NoPortfolioDomainsPermissionView, 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = Portfolio
|
||||||
|
template_name = "portfolio_no_requests.html"
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return render(request, self.template_name, context=self.get_context_data())
|
return render(request, self.template_name, context=self.get_context_data())
|
||||||
|
|
|
@ -433,7 +433,7 @@ class PortfolioDomainsPermission(PortfolioBasePermission):
|
||||||
up from the portfolio's primary key in self.kwargs["pk"]"""
|
up from the portfolio's primary key in self.kwargs["pk"]"""
|
||||||
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
portfolio = self.request.session.get("portfolio")
|
||||||
if not self.request.user.has_domains_portfolio_permission(portfolio):
|
if not self.request.user.has_any_domains_portfolio_permission(portfolio):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return super().has_permission()
|
return super().has_permission()
|
||||||
|
@ -450,7 +450,7 @@ class PortfolioDomainRequestsPermission(PortfolioBasePermission):
|
||||||
up from the portfolio's primary key in self.kwargs["pk"]"""
|
up from the portfolio's primary key in self.kwargs["pk"]"""
|
||||||
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
portfolio = self.request.session.get("portfolio")
|
||||||
if not self.request.user.has_domain_requests_portfolio_permission(portfolio):
|
if not self.request.user.has_any_requests_portfolio_permission(portfolio):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return super().has_permission()
|
return super().has_permission()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue