Merge branch 'rjm/2351-org-requests-page' into dk/2593-domain-request-search-bar

This commit is contained in:
zandercymatics 2024-09-10 10:00:49 -06:00
commit 4bf5ea5378
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
34 changed files with 1053 additions and 221 deletions

View file

@ -1168,7 +1168,6 @@ document.addEventListener('DOMContentLoaded', function() {
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
const statusIndicator = document.querySelector('.domain__filter-indicator');
const statusToggle = document.querySelector('.usa-button--filter');
const noPortfolioFlag = document.getElementById('no-portfolio-js-flag');
const portfolioElement = document.getElementById('portfolio-js-value');
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
@ -1236,7 +1235,7 @@ document.addEventListener('DOMContentLoaded', function() {
let markupForSuborganizationRow = '';
if (!noPortfolioFlag) {
if (portfolioValue) {
markupForSuborganizationRow = `
<td>
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
@ -1497,6 +1496,8 @@ document.addEventListener('DOMContentLoaded', function() {
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
const portfolioElement = document.getElementById('portfolio-js-value');
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
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.
@ -1546,7 +1547,7 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {*} scroll - control for the scrollToElement functionality
* @param {*} searchTerm - the search term
*/
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, status = currentStatus, portfolio = portfolioValue, portfolio = portfolioValue) {
// fetch json of page of domain requests, given params
let baseUrl = document.getElementById("get_domain_requests_json_url");
if (!baseUrl) {
@ -1627,10 +1628,21 @@ document.addEventListener('DOMContentLoaded', function() {
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>`;
// Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed
// Delete markup will 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 = '';
// 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) {
let modalHeading = '';
let modalDescription = '';
@ -1653,7 +1665,7 @@ document.addEventListener('DOMContentLoaded', function() {
role="button"
id="button-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}"
data-open-modal
>
@ -1718,8 +1730,57 @@ document.addEventListener('DOMContentLoaded', function() {
`
domainRequestsSectionWrapper.appendChild(modal);
// Request is deletable, modal and modalTrigger are built. Now test is portfolio requests page and enhace the modalTrigger markup
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');
row.innerHTML = `
<th scope="row" role="rowheader" data-label="Domain name">
@ -1728,6 +1789,7 @@ document.addEventListener('DOMContentLoaded', function() {
<td data-sort-value="${new Date(request.last_submitted_date).getTime()}" data-label="Date submitted">
${submissionDate}
</td>
${markupCreatorRow}
<td data-label="Status">
${request.status}
</td>
@ -1843,6 +1905,36 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
function closeMoreActionMenu(accordionIsOpen) {
if (accordionIsOpen.getAttribute("aria-expanded") === "true") {
accordionIsOpen.click();
}
}
document.addEventListener('focusin', function(event) {
const accordions = document.querySelectorAll('.usa-accordion--more-actions');
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
openAccordions.forEach((openAccordionButton) => {
const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); // Find the corresponding accordion
if (accordion && !accordion.contains(event.target)) {
closeMoreActionMenu(openAccordionButton); // Close the accordion if the focus is outside
}
});
});
document.addEventListener('click', function(event) {
const accordions = document.querySelectorAll('.usa-accordion--more-actions');
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
openAccordions.forEach((openAccordionButton) => {
const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); // Find the corresponding accordion
if (accordion && !accordion.contains(event.target)) {
closeMoreActionMenu(openAccordionButton); // Close the accordion if the click is outside
}
});
});
// Initial load
loadDomainRequests(1);
}

View file

@ -1,6 +1,7 @@
@use "uswds-core" as *;
.usa-accordion--select {
.usa-accordion--select,
.usa-accordion--more-actions {
display: inline-block;
width: auto;
position: relative;
@ -14,7 +15,6 @@
// Note, width is determined by a custom width class on one of the children
position: absolute;
z-index: 1;
top: 33.88px;
left: 0;
border-radius: 4px;
border: solid 1px color('base-lighter');
@ -31,3 +31,17 @@
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;
}

View file

@ -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 {
align-items: flex-end;
}
@ -200,6 +217,11 @@ abbr[title] {
}
}
.margin-right-neg-4px {
margin-right: -4px;
// Boost this USWDS utility class for the accordions in the portfolio requests table
.left-auto {
left: auto!important;
}
.break-word {
word-break: break-word;
}

View file

@ -211,14 +211,7 @@ a.usa-button--unstyled:visited {
align-items: center;
}
.dotgov-table a,
.usa-link--icon {
&:visited {
color: color('primary');
}
}
.dotgov-table a
a .usa-icon,
.usa-button--with-icon .usa-icon {
height: 1.3em;
@ -230,3 +223,9 @@ a .usa-icon,
height: 1.5em;
width: 1.5em;
}
button.text-secondary,
button.text-secondary:hover,
.dotgov-table a.text-secondary {
color: $theme-color-error;
}

View file

@ -89,16 +89,24 @@
.usa-nav__primary {
.usa-nav-link,
.usa-nav-link:hover,
.usa-nav-link:active {
.usa-nav-link:active,
button {
color: color('primary');
font-weight: font-weight('normal');
font-size: 16px;
}
.usa-current,
.usa-current:hover,
.usa-current:active {
.usa-current:active,
button.usa-current {
font-weight: font-weight('bold');
}
button[aria-expanded="true"] {
color: color('white');
}
button:not(.usa-current):hover::after {
display: none!important;
}
}
.usa-nav__secondary {
// I don't know why USWDS has this at 2 rem, which puts it out of alignment

View file

@ -79,6 +79,11 @@ urlpatterns = [
views.PortfolioDomainRequestsView.as_view(),
name="domain-requests",
),
path(
"no-organization-requests/",
views.PortfolioNoDomainRequestsView.as_view(),
name="no-portfolio-requests",
),
path(
"organization/",
views.PortfolioOrganizationView.as_view(),

View file

@ -60,35 +60,42 @@ def add_has_profile_feature_flag_to_context(request):
def portfolio_permissions(request):
"""Make portfolio permissions for the request user available in global context"""
context = {
default_context = {
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
"has_any_domains_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_edit_members_portfolio_permission": False,
"has_view_suborganization": False,
"has_edit_suborganization": False,
"portfolio": None,
"has_organization_feature_flag": False,
"has_organization_requests_flag": False,
"has_organization_members_flag": False,
}
try:
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:
return {
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(portfolio),
"has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(portfolio),
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(
portfolio
),
"has_edit_request_portfolio_permission": request.user.has_edit_request_portfolio_permission(portfolio),
"has_view_suborganization_portfolio_permission": view_suborg,
"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_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,
"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 default_context
except AttributeError:
# Handles cases where request.user might not exist
return context
return default_context

View file

@ -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,
),
),
]

View file

@ -192,31 +192,25 @@ class User(AbstractUser):
def has_edit_org_portfolio_permission(self, 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(
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
def has_domain_requests_portfolio_permission(self, portfolio):
# BEGIN
# Note code below is to add organization_request feature
def has_organization_requests_flag(self):
request = HttpRequest()
request.user = self
has_organization_requests_flag = flag_is_active(request, "organization_requests")
if not has_organization_requests_flag:
return False
# END
return self._has_portfolio_permission(
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
return flag_is_active(request, "organization_requests")
def has_organization_members_flag(self):
request = HttpRequest()
request.user = self
return flag_is_active(request, "organization_members")
def has_view_members_portfolio_permission(self, portfolio):
# BEGIN
# Note code below is to add organization_request feature
request = HttpRequest()
request.user = self
has_organization_members_flag = flag_is_active(request, "organization_members")
if not has_organization_members_flag:
if not self.has_organization_members_flag():
return False
# END
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
@ -224,23 +218,37 @@ class User(AbstractUser):
def has_edit_members_portfolio_permission(self, portfolio):
# BEGIN
# Note code below is to add organization_request feature
request = HttpRequest()
request.user = self
has_organization_members_flag = flag_is_active(request, "organization_members")
if not has_organization_members_flag:
if not self.has_organization_members_flag():
return False
# END
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"""
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
def has_view_suborganization(self, portfolio):
def has_view_suborganization_portfolio_permission(self, portfolio):
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)
def get_first_portfolio(self):
@ -249,36 +257,36 @@ class User(AbstractUser):
return permission.portfolio
return None
def has_edit_requests(self, portfolio):
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
def portfolio_role_summary(self, portfolio):
"""Returns a list of roles based on the user's permissions."""
roles = []
# Define the conditions and their corresponding roles
conditions_roles = [
(self.has_edit_suborganization(portfolio), ["Admin"]),
(self.has_edit_suborganization_portfolio_permission(portfolio), ["Admin"]),
(
self.has_view_all_domains_permission(portfolio)
and self.has_domain_requests_portfolio_permission(portfolio)
and self.has_edit_requests(portfolio),
self.has_view_all_domains_portfolio_permission(portfolio)
and self.has_any_requests_portfolio_permission(portfolio)
and self.has_edit_request_portfolio_permission(portfolio),
["View-only admin", "Domain requestor"],
),
(
self.has_view_all_domains_permission(portfolio)
and self.has_domain_requests_portfolio_permission(portfolio),
self.has_view_all_domains_portfolio_permission(portfolio)
and self.has_any_requests_portfolio_permission(portfolio),
["View-only admin"],
),
(
self.has_base_portfolio_permission(portfolio)
and self.has_edit_requests(portfolio)
and self.has_domains_portfolio_permission(portfolio),
and self.has_edit_request_portfolio_permission(portfolio)
and self.has_any_domains_portfolio_permission(portfolio),
["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"],
),
(self.has_base_portfolio_permission(portfolio), ["Member"]),
@ -437,8 +445,6 @@ class User(AbstractUser):
self.check_domain_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):
has_organization_feature_flag = flag_is_active(request, "organization_feature")
portfolio = request.session.get("portfolio")
@ -447,7 +453,7 @@ class User(AbstractUser):
def get_user_domain_ids(self, request):
"""Returns either the domains ids associated with this user on UserDomainRole or 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)
else:
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)

View file

@ -21,7 +21,6 @@ class UserPortfolioPermissionChoices(models.TextChoices):
EDIT_MEMBERS = "edit_members", "Create and edit members"
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"
VIEW_PORTFOLIO = "view_portfolio", "View organization"

View file

@ -6,7 +6,7 @@ import logging
from urllib.parse import parse_qs
from django.urls import reverse
from django.http import HttpResponseRedirect
from registrar.models.user import User
from registrar.models import User
from waffle.decorators import flag_is_active
from registrar.models.utility.generic_helper import replace_url_queryparams
@ -144,25 +144,30 @@ class CheckPortfolioMiddleware:
if not request.user.is_authenticated:
return None
# set the portfolio in the session if it is not set
if "portfolio" not in request.session or request.session["portfolio"] is None:
# if multiple portfolios are allowed for this user
if flag_is_active(request, "multiple_portfolios"):
# NOTE: we will want to change later to have a workflow for selecting
# portfolio and another for switching portfolio; for now, select first
request.session["portfolio"] = request.user.get_first_portfolio()
elif flag_is_active(request, "organization_feature"):
request.session["portfolio"] = request.user.get_first_portfolio()
else:
request.session["portfolio"] = None
# if multiple portfolios are allowed for this user
if flag_is_active(request, "organization_feature"):
self.set_portfolio_in_session(request)
elif request.session.get("portfolio"):
# Edge case: User disables flag while already logged in
request.session["portfolio"] = None
elif "portfolio" not in request.session:
# Set the portfolio in the session if its not already in it
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.has_domains_portfolio_permission(request.session["portfolio"]):
if request.user.is_org_user(request):
if current_path == self.home:
if request.user.has_any_domains_portfolio_permission(request.session["portfolio"]):
portfolio_redirect = reverse("domains")
else:
portfolio_redirect = reverse("no-portfolio-domains")
return HttpResponseRedirect(portfolio_redirect)
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()

View file

@ -72,9 +72,9 @@
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
{% 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 %}
{% 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 %}
{% 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 %}

View file

@ -63,7 +63,7 @@
<div class="grid-row margin-top-1">
<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">
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
</svg>Delete

View file

@ -52,7 +52,7 @@
{% endwith %}
</div>
<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">
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
</svg>Delete

View file

@ -16,6 +16,26 @@
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
</svg><span class="margin-left-05">Previous step</span>
</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 %}
{% block form_messages %}

View file

@ -40,7 +40,7 @@
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
</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">
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
</svg><span class="margin-left-05">Delete</span>

View file

@ -8,15 +8,33 @@
{% block content %}
<main id="main-content" class="grid-container">
<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">
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
</svg>
<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>
</a>
{% comment %} {% endif %}{% endcomment %}
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
<div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"

View file

@ -61,7 +61,7 @@
{% if portfolio %}
{% 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" %}
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
{% endwith %}

View file

@ -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>.
</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">
{% csrf_token %}
{% input_with_errors form.sub_organization %}

View file

@ -5,10 +5,13 @@
<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">
<div class="grid-row">
{% if not has_domain_requests_portfolio_permission %}
{% if not portfolio %}
<div class="mobile:grid-col-12 desktop:grid-col-6">
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
</div>
{% else %}
<!-- Embedding the portfolio value in a data attribute -->
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
{% endif %}
<div class="mobile:grid-col-12 desktop:grid-col-6">
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
@ -45,7 +48,10 @@
<thead>
<tr>
<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 scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
<!-- AJAX will conditionally add a th for delete actions -->

View file

@ -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 %}">
{% if not portfolio %}
<h2 id="domains-header" class="display-inline-block">Domains</h2>
<span class="display-none" id="no-portfolio-js-flag"></span>
{% else %}
<!-- Embedding the portfolio value in a data attribute -->
<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="expiration_date" scope="col" role="columnheader">Expires</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>
{% endif %}
<th

View file

@ -37,9 +37,9 @@
</div>
<ul class="usa-nav__primary usa-accordion">
<li class="usa-nav__primary-item">
{% if has_domains_portfolio_permission %}
{% if has_any_domains_portfolio_permission %}
{% url 'domains' as url %}
{%else %}
{% else %}
{% url 'no-portfolio-domains' as url %}
{% endif %}
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
@ -51,22 +51,56 @@
Domain groups
</a>
</li> -->
{% if has_domain_requests_portfolio_permission %}
<li class="usa-nav__primary-item">
{% if has_organization_requests_flag %}
<li class="usa-nav__primary-item">
<!-- user hasone of the view permissions plus the edit permission, show the dropdown -->
{% if has_edit_request_portfolio_permission %}
{% 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 %}">
Domain requests
</a>
</li>
{% endif %}
</li>
{% endif %}
{% if has_view_members_portfolio_permission %}
{% if has_organization_members_flag %}
<li class="usa-nav__primary-item">
<a href="#" class="usa-nav-link">
Members
</a>
</li>
{% endif %}
<li class="usa-nav__primary-item">
{% url 'organization' as url %}
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->

View 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 dont have access to domain requests.</h2>
{% if portfolio_administrators %}
<p>If you believe you should have access to a request, reach out to your organizations 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 %}

View file

@ -11,18 +11,27 @@
{% block portfolio_content %}
<div id="main-content">
<h1 id="domain-requests-header">Domain requests</h1>
{% 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>
<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>
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
</div>

View file

@ -1135,7 +1135,7 @@ class TestPortfolioInvitations(TestCase):
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Hotel California")
self.portfolio_role_base = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
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.invitation, _ = PortfolioInvitation.objects.get_or_create(
email=self.email,
@ -1326,16 +1326,16 @@ class TestUser(TestCase):
User.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):
# Test if the user is recognized as an Admin
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"])
@patch.multiple(
User,
has_view_all_domains_permission=lambda self, portfolio: True,
has_domain_requests_portfolio_permission=lambda self, portfolio: True,
has_edit_requests=lambda self, portfolio: True,
has_view_all_domains_portfolio_permission=lambda self, portfolio: True,
has_any_requests_portfolio_permission=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):
# Test if the user has both 'View-only admin' and 'Domain requestor' roles
@ -1343,8 +1343,8 @@ class TestUser(TestCase):
@patch.multiple(
User,
has_view_all_domains_permission=lambda self, portfolio: True,
has_domain_requests_portfolio_permission=lambda self, portfolio: True,
has_view_all_domains_portfolio_permission=lambda self, portfolio: True,
has_any_requests_portfolio_permission=lambda self, portfolio: True,
)
def test_portfolio_role_summary_view_only_admin(self):
# Test if the user is recognized as a View-only admin
@ -1353,15 +1353,17 @@ class TestUser(TestCase):
@patch.multiple(
User,
has_base_portfolio_permission=lambda self, portfolio: True,
has_edit_requests=lambda self, portfolio: True,
has_domains_portfolio_permission=lambda self, portfolio: True,
has_edit_request_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):
# 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"])
@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):
# Test if the user has 'Member' and 'Domain requestor' roles
@ -1370,7 +1372,7 @@ class TestUser(TestCase):
@patch.multiple(
User,
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):
# Test if the user has 'Member' and 'Domain manager' roles
@ -1385,6 +1387,74 @@ class TestUser(TestCase):
# Test if the user has no roles
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
def test_check_transition_domains_without_domains_on_login(self):
"""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")
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
user_can_view_all_domains = self.user.has_any_domains_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_requests)
@ -1562,8 +1632,8 @@ class TestUser(TestCase):
],
)
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
self.assertTrue(user_can_view_all_domains)
self.assertFalse(user_can_view_all_requests)
@ -1572,16 +1642,16 @@ class TestUser(TestCase):
portfolio_permission.save()
portfolio_permission.refresh_from_db()
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
user_can_view_all_domains = self.user.has_any_domains_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_requests)
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_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
user_can_view_all_domains = self.user.has_any_domains_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_requests)

View file

@ -12,10 +12,11 @@ from registrar.models import (
)
from registrar.models.user_portfolio_permission import UserPortfolioPermission
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 django.contrib.sessions.middleware import SessionMiddleware
import boto3_mocking # type: ignore
from django.test import Client
import logging
logger = logging.getLogger(__name__)
@ -24,6 +25,7 @@ logger = logging.getLogger(__name__)
class TestPortfolio(WebTest):
def setUp(self):
super().setUp()
self.client = Client()
self.user = create_test_user()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
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):
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
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=[]
)
self.user.portfolio = self.portfolio
@ -504,7 +506,7 @@ class TestPortfolio(WebTest):
self.client.force_login(self.user)
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.assertContains(response, "You aren")
@ -519,7 +521,7 @@ class TestPortfolio(WebTest):
# Test the domains page - this user should have access
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.assertContains(response, "Domain name")
@ -530,7 +532,7 @@ class TestPortfolio(WebTest):
# Test the domains page - this user should have access
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.assertContains(response, "Domain name")
permission.delete()
@ -547,7 +549,7 @@ class TestPortfolio(WebTest):
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
@ -573,7 +575,7 @@ class TestPortfolio(WebTest):
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
@ -599,7 +601,7 @@ class TestPortfolio(WebTest):
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
@ -630,3 +632,170 @@ class TestPortfolio(WebTest):
self.assertContains(home, "Hotel California")
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 dont 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
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")

View file

@ -7,7 +7,7 @@ from api.tests.common import less_console_noise_decorator
from .common import MockSESClient, completed_domain_request # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
from waffle.testutils import override_flag
from registrar.models import (
DomainRequest,
DraftDomain,
@ -17,12 +17,14 @@ from registrar.models import (
User,
Website,
FederalAgency,
Portfolio,
UserPortfolioPermission,
)
from registrar.views.domain_request import DomainRequestWizard, Step
from .common import less_console_noise
from .test_views import TestWithUser
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
import logging
logger = logging.getLogger(__name__)
@ -2925,6 +2927,39 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
response = self.client.get("/get-domain-requests-json/")
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
def test_domain_request_withdraw_no_permissions(self):
"""Can't withdraw domain requests as a restricted user."""

View file

@ -2,9 +2,14 @@ from registrar.models import DomainRequest
from django.urls import reverse
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 django_webtest import WebTest # type: ignore
from django.utils.dateparse import parse_datetime
from waffle.testutils import override_flag
class GetRequestsJsonTest(TestWithUser, WebTest):
@ -20,6 +25,19 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.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
cls.domain_requests = [
DomainRequest.objects.create(
@ -28,6 +46,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
last_submitted_date="2024-01-01",
status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-01-01",
portfolio=cls.portfolio,
),
DomainRequest.objects.create(
creator=cls.user,
@ -42,6 +61,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
last_submitted_date="2024-03-01",
status=DomainRequest.DomainRequestStatus.REJECTED,
created_at="2024-03-01",
portfolio=cls.portfolio,
),
DomainRequest.objects.create(
creator=cls.user,
@ -113,6 +133,14 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
status=DomainRequest.DomainRequestStatus.APPROVED,
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
@ -120,6 +148,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
super().tearDownClass()
DomainRequest.objects.all().delete()
DraftDomain.objects.all().delete()
Portfolio.objects.all().delete()
def test_get_domain_requests_json_authenticated(self):
"""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):
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):
"""Test that pagination works properly. There are 11 total non-approved requests and
a page size of 10"""

View file

@ -175,7 +175,7 @@ class DomainView(DomainBaseView):
If particular views allow permissions, they will need to override
this function."""
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():
domain = Domain.objects.get(id=pk)
if domain.domain_info.portfolio == portfolio:

View file

@ -152,7 +152,14 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
except DomainRequest.DoesNotExist:
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
return self._domain_request
@ -395,6 +402,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
def get_context_data(self):
"""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 = {}
if DomainRequest._form_complete(self.domain_request, self.request):
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
@ -411,6 +422,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
Youll only be able to withdraw your request.",
"review_form_is_complete": True,
"user": self.request.user,
"requested_domain__name": requested_domain_name,
}
else: # form is not complete
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."',
"review_form_is_complete": False,
"user": self.request.user,
"requested_domain__name": requested_domain_name,
}
return context_stuff
@ -505,7 +518,11 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
# if user opted to save progress and return,
# return them to the home page
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
return self.goto_next_step()
@ -774,7 +791,10 @@ class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
domain_request = DomainRequest.objects.get(id=self.kwargs["pk"])
domain_request.withdraw()
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):

View file

@ -12,14 +12,55 @@ def get_domain_requests_json(request):
"""Given the current request,
get all domain requests that are associated with the request user and exclude the APPROVED ones"""
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
)
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'
order = request.GET.get("order", "asc") # Default to 'asc'
def apply_search(queryset, request):
search_term = request.GET.get("search_term")
if search_term:
@ -30,70 +71,60 @@ def get_domain_requests_json(request):
# If yes, we should return domain requests that do not have a
# requested_domain (those display as New domain request in the UI)
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)
)
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":
sort_by = f"-{sort_by}"
domain_requests = domain_requests.order_by(sort_by)
page_number = request.GET.get("page", 1)
paginator = Paginator(domain_requests, 10)
page_obj = paginator.get_page(page_number)
return queryset.order_by(sort_by)
domain_requests_data = [
{
"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
"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
def serialize_domain_request(domain_request, user):
# Determine if the request is deletable
is_deletable = domain_request.status in [
DomainRequest.DomainRequestStatus.STARTED,
DomainRequest.DomainRequestStatus.WITHDRAWN,
]
return JsonResponse(
{
"domain_requests": domain_requests_data,
"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,
}
)
# Determine action label based on user permissions and request status
editable_statuses = [
DomainRequest.DomainRequestStatus.STARTED,
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
DomainRequest.DomainRequestStatus.WITHDRAWN,
]
if user.has_edit_request_portfolio_permission and domain_request.creator == user:
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),
}

View file

@ -42,12 +42,41 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, 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.
"""
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):
return render(request, self.template_name, context=self.get_context_data())

View file

@ -433,7 +433,7 @@ class PortfolioDomainsPermission(PortfolioBasePermission):
up from the portfolio's primary key in self.kwargs["pk"]"""
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 super().has_permission()
@ -450,7 +450,7 @@ class PortfolioDomainRequestsPermission(PortfolioBasePermission):
up from the portfolio's primary key in self.kwargs["pk"]"""
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 super().has_permission()