mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 10:29:23 +02:00
Merge branch 'main' into es/2162-delete-submitter-v2
This commit is contained in:
commit
5c32a1922b
47 changed files with 1694 additions and 273 deletions
|
@ -860,3 +860,55 @@ Example: `cf ssh getgov-za`
|
|||
|
||||
### Running locally
|
||||
```docker-compose exec app ./manage.py populate_domain_request_dates```
|
||||
|
||||
## Create federal portfolio
|
||||
This script takes the name of a `FederalAgency` (like 'AMTRAK') and does the following:
|
||||
1. Creates the portfolio record based off of data on the federal agency object itself.
|
||||
2. Creates suborganizations from existing DomainInformation records.
|
||||
3. Associates the SeniorOfficial record (if it exists).
|
||||
4. Adds this portfolio to DomainInformation / DomainRequests or both.
|
||||
|
||||
Errors:
|
||||
1. ValueError: Federal agency not found in database.
|
||||
2. Logged Warning: No senior official found for portfolio
|
||||
3. Logged Error: No suborganizations found for portfolio.
|
||||
4. Logged Warning: No new suborganizations to add.
|
||||
5. Logged Warning: No valid DomainRequest records to update.
|
||||
6. Logged Warning: No valid DomainInformation records to update.
|
||||
|
||||
### Running on sandboxes
|
||||
|
||||
#### Step 1: Login to CloudFoundry
|
||||
```cf login -a api.fr.cloud.gov --sso```
|
||||
|
||||
#### Step 2: SSH into your environment
|
||||
```cf ssh getgov-{space}```
|
||||
|
||||
Example: `cf ssh getgov-za`
|
||||
|
||||
#### Step 3: Create a shell instance
|
||||
```/tmp/lifecycle/shell```
|
||||
|
||||
#### Step 4: Upload your csv to the desired sandbox
|
||||
[Follow these steps](#use-scp-to-transfer-data-to-sandboxes) to upload the federal_cio csv to a sandbox of your choice.
|
||||
|
||||
#### Step 5: Running the script
|
||||
```./manage.py create_federal_portfolio "{federal_agency_name}" --both```
|
||||
|
||||
Example (only requests): `./manage.py create_federal_portfolio "AMTRAK" --parse_requests`
|
||||
|
||||
### Running locally
|
||||
|
||||
#### Step 1: Running the script
|
||||
```docker-compose exec app ./manage.py create_federal_portfolio "{federal_agency_name}" --both```
|
||||
|
||||
##### Parameters
|
||||
| | Parameter | Description |
|
||||
|:-:|:-------------------------- |:-------------------------------------------------------------------------------------------|
|
||||
| 1 | **federal_agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
||||
| 2 | **both** | If True, runs parse_requests and parse_domains. |
|
||||
| 3 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
||||
| 4 | **parse_domains** | If True, then the created portfolio is added to all related Domains. |
|
||||
|
||||
Note: Regarding parameters #2-#3, you cannot use `--both` while using these. You must specify either `--parse_requests` or `--parse_domains` seperately. While all of these parameters are optional in that you do not need to specify all of them,
|
||||
you must specify at least one to run this script.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf import settings # type: ignore
|
||||
|
||||
|
||||
class Cert:
|
||||
|
@ -12,7 +12,7 @@ class Cert:
|
|||
variable but Python's ssl library requires a file.
|
||||
"""
|
||||
|
||||
def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None:
|
||||
def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None: # type: ignore
|
||||
self.filename = self._write(data)
|
||||
|
||||
def __del__(self):
|
||||
|
@ -31,4 +31,4 @@ class Key(Cert):
|
|||
"""Location of private key as written to disk."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(data=settings.SECRET_REGISTRY_KEY)
|
||||
super().__init__(data=settings.SECRET_REGISTRY_KEY) # type: ignore
|
||||
|
|
|
@ -962,7 +962,9 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||
domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED)
|
||||
|
||||
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
||||
portfolio_ids = obj.get_portfolios().values_list("portfolio", flat=True)
|
||||
portfolios = models.Portfolio.objects.filter(id__in=portfolio_ids)
|
||||
extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
|
||||
|
|
|
@ -748,7 +748,10 @@ function initializeWidgetOnList(list, parentId) {
|
|||
|
||||
//------ Requested Domains
|
||||
const requestedDomainElement = document.getElementById('id_requested_domain');
|
||||
const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text;
|
||||
// We have to account for different superuser and analyst markups
|
||||
const requestedDomain = requestedDomainElement.options
|
||||
? requestedDomainElement.options[requestedDomainElement.selectedIndex].text
|
||||
: requestedDomainElement.text;
|
||||
|
||||
//------ Submitter
|
||||
// Function to extract text by ID and handle missing elements
|
||||
|
@ -762,7 +765,10 @@ function initializeWidgetOnList(list, parentId) {
|
|||
// Extract the submitter name, title, email, and phone number
|
||||
const submitterDiv = document.querySelector('.form-row.field-submitter');
|
||||
const submitterNameElement = document.getElementById('id_submitter');
|
||||
const submitterName = submitterNameElement.options[submitterNameElement.selectedIndex].text;
|
||||
// We have to account for different superuser and analyst markups
|
||||
const submitterName = submitterNameElement
|
||||
? submitterNameElement.options[submitterNameElement.selectedIndex].text
|
||||
: submitterDiv.querySelector('a').text;
|
||||
const submitterTitle = extractTextById('contact_info_title', submitterDiv);
|
||||
const submitterEmail = extractTextById('contact_info_email', submitterDiv);
|
||||
const submitterPhone = extractTextById('contact_info_phone', submitterDiv);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -1226,7 +1225,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>
|
||||
|
@ -1427,9 +1426,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// NOTE: We may need to evolve this as we add more filters.
|
||||
document.addEventListener('focusin', function(event) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
@ -1438,9 +1437,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// NOTE: We may need to evolve this as we add more filters.
|
||||
document.addEventListener('click', function(event) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
@ -1485,6 +1484,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
|
||||
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
|
||||
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.
|
||||
|
@ -1533,7 +1534,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, searchTerm = currentSearchTerm) {
|
||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
|
||||
// fetch json of page of domain requests, given params
|
||||
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
||||
if (!baseUrl) {
|
||||
|
@ -1545,7 +1546,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
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(data => {
|
||||
if (data.error) {
|
||||
|
@ -1601,10 +1607,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
|
||||
// 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 = '';
|
||||
|
||||
// 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 = '';
|
||||
|
@ -1627,7 +1644,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
|
||||
>
|
||||
|
@ -1692,8 +1709,57 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
`
|
||||
|
||||
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');
|
||||
row.innerHTML = `
|
||||
<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">
|
||||
${submissionDate}
|
||||
</td>
|
||||
${markupCreatorRow}
|
||||
<td data-label="Status">
|
||||
${request.status}
|
||||
</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
|
||||
loadDomainRequests(1);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,6 +23,9 @@ from cfenv import AppEnv # type: ignore
|
|||
from pathlib import Path
|
||||
from typing import Final
|
||||
from botocore.config import Config
|
||||
import json
|
||||
import logging
|
||||
from django.utils.log import ServerFormatter
|
||||
|
||||
# # # ###
|
||||
# Setup code goes here #
|
||||
|
@ -57,7 +60,7 @@ env_db_url = env.dj_db_url("DATABASE_URL")
|
|||
env_debug = env.bool("DJANGO_DEBUG", default=False)
|
||||
env_is_production = env.bool("IS_PRODUCTION", default=False)
|
||||
env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG")
|
||||
env_base_url = env.str("DJANGO_BASE_URL")
|
||||
env_base_url: str = env.str("DJANGO_BASE_URL")
|
||||
env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "")
|
||||
env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox")
|
||||
|
||||
|
@ -192,7 +195,7 @@ MIDDLEWARE = [
|
|||
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
||||
]
|
||||
|
||||
# application object used by Django’s built-in servers (e.g. `runserver`)
|
||||
# application object used by Django's built-in servers (e.g. `runserver`)
|
||||
WSGI_APPLICATION = "registrar.config.wsgi.application"
|
||||
|
||||
# endregion
|
||||
|
@ -415,7 +418,7 @@ LANGUAGE_COOKIE_SECURE = True
|
|||
# and to interpret datetimes entered in forms
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
# enable Django’s translation system
|
||||
# enable Django's translation system
|
||||
USE_I18N = True
|
||||
|
||||
# enable localized formatting of numbers and dates
|
||||
|
@ -450,6 +453,40 @@ PHONENUMBER_DEFAULT_REGION = "US"
|
|||
# logger.error("Can't do this important task. Something is very wrong.")
|
||||
# logger.critical("Going to crash now.")
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
"""Formats logs into JSON for better parsing"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(datefmt="%d/%b/%Y %H:%M:%S")
|
||||
|
||||
def format(self, record):
|
||||
log_record = {
|
||||
"timestamp": self.formatTime(record, self.datefmt),
|
||||
"level": record.levelname,
|
||||
"name": record.name,
|
||||
"lineno": record.lineno,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
return json.dumps(log_record)
|
||||
|
||||
|
||||
class JsonServerFormatter(ServerFormatter):
|
||||
"""Formats server logs into JSON for better parsing"""
|
||||
|
||||
def format(self, record):
|
||||
formatted_record = super().format(record)
|
||||
log_entry = {"server_time": record.server_time, "level": record.levelname, "message": formatted_record}
|
||||
return json.dumps(log_entry)
|
||||
|
||||
|
||||
# default to json formatted logs
|
||||
server_formatter, console_formatter = "json.server", "json"
|
||||
|
||||
# don't use json format locally, it makes logs hard to read in console
|
||||
if "localhost" in env_base_url:
|
||||
server_formatter, console_formatter = "django.server", "verbose"
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
# Don't import Django's existing loggers
|
||||
|
@ -469,6 +506,12 @@ LOGGING = {
|
|||
"format": "[{server_time}] {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"json.server": {
|
||||
"()": JsonServerFormatter,
|
||||
},
|
||||
"json": {
|
||||
"()": JsonFormatter,
|
||||
},
|
||||
},
|
||||
# define where log messages will be sent;
|
||||
# each logger can have one or more handlers
|
||||
|
@ -476,12 +519,12 @@ LOGGING = {
|
|||
"console": {
|
||||
"level": env_log_level,
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
"formatter": console_formatter,
|
||||
},
|
||||
"django.server": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "django.server",
|
||||
"formatter": server_formatter,
|
||||
},
|
||||
# No file logger is configured,
|
||||
# because containerized apps
|
||||
|
|
|
@ -78,6 +78,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(),
|
||||
|
|
|
@ -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 = {
|
||||
portfolio_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 portfolio_context
|
||||
|
||||
except AttributeError:
|
||||
# Handles cases where request.user might not exist
|
||||
return context
|
||||
return portfolio_context
|
||||
|
|
255
src/registrar/management/commands/create_federal_portfolio.py
Normal file
255
src/registrar/management/commands/create_federal_portfolio.py
Normal file
|
@ -0,0 +1,255 @@
|
|||
"""Loads files from /tmp into our sandboxes"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper
|
||||
from registrar.models import DomainInformation, DomainRequest, FederalAgency, Suborganization, Portfolio, User
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates a federal portfolio given a FederalAgency name"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add three arguments:
|
||||
1. agency_name => the value of FederalAgency.agency
|
||||
2. --parse_requests => if true, adds the given portfolio to each related DomainRequest
|
||||
3. --parse_domains => if true, adds the given portfolio to each related DomainInformation
|
||||
"""
|
||||
parser.add_argument(
|
||||
"agency_name",
|
||||
help="The name of the FederalAgency to add",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--parse_requests",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Adds portfolio to DomainRequests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--parse_domains",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Adds portfolio to DomainInformation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--both",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Adds portfolio to both requests and domains",
|
||||
)
|
||||
|
||||
def handle(self, agency_name, **options):
|
||||
parse_requests = options.get("parse_requests")
|
||||
parse_domains = options.get("parse_domains")
|
||||
both = options.get("both")
|
||||
|
||||
if not both:
|
||||
if not parse_requests and not parse_domains:
|
||||
raise CommandError("You must specify at least one of --parse_requests or --parse_domains.")
|
||||
else:
|
||||
if parse_requests or parse_domains:
|
||||
raise CommandError("You cannot pass --parse_requests or --parse_domains when passing --both.")
|
||||
|
||||
federal_agency = FederalAgency.objects.filter(agency__iexact=agency_name).first()
|
||||
if not federal_agency:
|
||||
raise ValueError(
|
||||
f"Cannot find the federal agency '{agency_name}' in our database. "
|
||||
"The value you enter for `agency_name` must be "
|
||||
"prepopulated in the FederalAgency table before proceeding."
|
||||
)
|
||||
|
||||
portfolio = self.create_or_modify_portfolio(federal_agency)
|
||||
self.create_suborganizations(portfolio, federal_agency)
|
||||
|
||||
if parse_requests or both:
|
||||
self.handle_portfolio_requests(portfolio, federal_agency)
|
||||
|
||||
if parse_domains or both:
|
||||
self.handle_portfolio_domains(portfolio, federal_agency)
|
||||
|
||||
def create_or_modify_portfolio(self, federal_agency):
|
||||
"""Creates or modifies a portfolio record based on a federal agency."""
|
||||
portfolio_args = {
|
||||
"federal_agency": federal_agency,
|
||||
"organization_name": federal_agency.agency,
|
||||
"organization_type": DomainRequest.OrganizationChoices.FEDERAL,
|
||||
"creator": User.get_default_user(),
|
||||
"notes": "Auto-generated record",
|
||||
}
|
||||
|
||||
if federal_agency.so_federal_agency.exists():
|
||||
portfolio_args["senior_official"] = federal_agency.so_federal_agency.first()
|
||||
|
||||
portfolio, created = Portfolio.objects.get_or_create(
|
||||
organization_name=portfolio_args.get("organization_name"), defaults=portfolio_args
|
||||
)
|
||||
|
||||
if created:
|
||||
message = f"Created portfolio '{portfolio}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||
|
||||
if portfolio_args.get("senior_official"):
|
||||
message = f"Added senior official '{portfolio_args['senior_official']}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||
else:
|
||||
message = (
|
||||
f"No senior official added to portfolio '{portfolio}'. "
|
||||
"None was returned for the reverse relation `FederalAgency.so_federal_agency.first()`"
|
||||
)
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||
else:
|
||||
proceed = TerminalHelper.prompt_for_execution(
|
||||
system_exit_on_terminate=False,
|
||||
prompt_message=f"""
|
||||
The given portfolio '{federal_agency.agency}' already exists in our DB.
|
||||
If you cancel, the rest of the script will still execute but this record will not update.
|
||||
""",
|
||||
prompt_title="Do you wish to modify this record?",
|
||||
)
|
||||
if proceed:
|
||||
|
||||
# Don't override the creator and notes fields
|
||||
if portfolio.creator:
|
||||
portfolio_args.pop("creator")
|
||||
|
||||
if portfolio.notes:
|
||||
portfolio_args.pop("notes")
|
||||
|
||||
# Update everything else
|
||||
for key, value in portfolio_args.items():
|
||||
setattr(portfolio, key, value)
|
||||
|
||||
portfolio.save()
|
||||
message = f"Modified portfolio '{portfolio}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
if portfolio_args.get("senior_official"):
|
||||
message = f"Added/modified senior official '{portfolio_args['senior_official']}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
return portfolio
|
||||
|
||||
def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||
"""Create Suborganizations tied to the given portfolio based on DomainInformation objects"""
|
||||
valid_agencies = DomainInformation.objects.filter(
|
||||
federal_agency=federal_agency, organization_name__isnull=False
|
||||
)
|
||||
org_names = set(valid_agencies.values_list("organization_name", flat=True))
|
||||
|
||||
if not org_names:
|
||||
message = (
|
||||
"Could not add any suborganizations."
|
||||
f"\nNo suborganizations were found for '{federal_agency}' when filtering on this name, "
|
||||
"and excluding null organization_name records."
|
||||
)
|
||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.FAIL, message)
|
||||
return
|
||||
|
||||
# Check if we need to update any existing suborgs first. This step is optional.
|
||||
existing_suborgs = Suborganization.objects.filter(name__in=org_names)
|
||||
if existing_suborgs.exists():
|
||||
self._update_existing_suborganizations(portfolio, existing_suborgs)
|
||||
|
||||
# Create new suborgs, as long as they don't exist in the db already
|
||||
new_suborgs = []
|
||||
for name in org_names - set(existing_suborgs.values_list("name", flat=True)):
|
||||
# Stored in variables due to linter wanting type information here.
|
||||
portfolio_name: str = portfolio.organization_name if portfolio.organization_name is not None else ""
|
||||
if name is not None and name.lower() == portfolio_name.lower():
|
||||
# You can use this to populate location information, when this occurs.
|
||||
# However, this isn't needed for now so we can skip it.
|
||||
message = (
|
||||
f"Skipping suborganization create on record '{name}'. "
|
||||
"The federal agency name is the same as the portfolio name."
|
||||
)
|
||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message)
|
||||
else:
|
||||
new_suborgs.append(Suborganization(name=name, portfolio=portfolio)) # type: ignore
|
||||
|
||||
if new_suborgs:
|
||||
Suborganization.objects.bulk_create(new_suborgs)
|
||||
TerminalHelper.colorful_logger(
|
||||
logger.info, TerminalColors.OKGREEN, f"Added {len(new_suborgs)} suborganizations"
|
||||
)
|
||||
else:
|
||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, "No suborganizations added")
|
||||
|
||||
def _update_existing_suborganizations(self, portfolio, orgs_to_update):
|
||||
"""
|
||||
Update existing suborganizations with new portfolio.
|
||||
Prompts for user confirmation before proceeding.
|
||||
"""
|
||||
proceed = TerminalHelper.prompt_for_execution(
|
||||
system_exit_on_terminate=False,
|
||||
prompt_message=f"""Some suborganizations already exist in our DB.
|
||||
If you cancel, the rest of the script will still execute but these records will not update.
|
||||
|
||||
==Proposed Changes==
|
||||
The following suborgs will be updated: {[org.name for org in orgs_to_update]}
|
||||
""",
|
||||
prompt_title="Do you wish to modify existing suborganizations?",
|
||||
)
|
||||
if proceed:
|
||||
for org in orgs_to_update:
|
||||
org.portfolio = portfolio
|
||||
|
||||
Suborganization.objects.bulk_update(orgs_to_update, ["portfolio"])
|
||||
message = f"Updated {len(orgs_to_update)} suborganizations."
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||
"""
|
||||
Associate portfolio with domain requests for a federal agency.
|
||||
Updates all relevant domain request records.
|
||||
"""
|
||||
invalid_states = [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
]
|
||||
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency).exclude(status__in=invalid_states)
|
||||
if not domain_requests.exists():
|
||||
message = f"""
|
||||
Portfolios not added to domain requests: no valid records found.
|
||||
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
||||
"""
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||
return None
|
||||
|
||||
# Get all suborg information and store it in a dict to avoid doing a db call
|
||||
suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name")
|
||||
for domain_request in domain_requests:
|
||||
domain_request.portfolio = portfolio
|
||||
if domain_request.organization_name in suborgs:
|
||||
domain_request.sub_organization = suborgs.get(domain_request.organization_name)
|
||||
|
||||
DomainRequest.objects.bulk_update(domain_requests, ["portfolio", "sub_organization"])
|
||||
message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests."
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||
|
||||
def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||
"""
|
||||
Associate portfolio with domains for a federal agency.
|
||||
Updates all relevant domain information records.
|
||||
"""
|
||||
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency)
|
||||
if not domain_infos.exists():
|
||||
message = f"""
|
||||
Portfolios not added to domains: no valid records found.
|
||||
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||
"""
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||
return None
|
||||
|
||||
# Get all suborg information and store it in a dict to avoid doing a db call
|
||||
suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name")
|
||||
for domain_info in domain_infos:
|
||||
domain_info.portfolio = portfolio
|
||||
if domain_info.organization_name in suborgs:
|
||||
domain_info.sub_organization = suborgs.get(domain_info.organization_name)
|
||||
|
||||
DomainInformation.objects.bulk_update(domain_infos, ["portfolio", "sub_organization"])
|
||||
message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
|
@ -423,7 +423,7 @@ class Command(BaseCommand):
|
|||
valid_fed_type = fed_type in fed_choices
|
||||
valid_fed_agency = fed_agency in agency_choices
|
||||
|
||||
default_creator, _ = User.objects.get_or_create(username="System")
|
||||
default_creator = User.get_default_user()
|
||||
|
||||
new_domain_info_data = {
|
||||
"domain": domain,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -131,6 +131,12 @@ class User(AbstractUser):
|
|||
else:
|
||||
return self.username
|
||||
|
||||
@classmethod
|
||||
def get_default_user(cls):
|
||||
"""Returns the default "system" user"""
|
||||
default_creator, _ = User.objects.get_or_create(username="System")
|
||||
return default_creator
|
||||
|
||||
def restrict_user(self):
|
||||
self.status = self.RESTRICTED
|
||||
self.save()
|
||||
|
@ -192,31 +198,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 +224,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 +263,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"]),
|
||||
|
@ -292,6 +306,9 @@ class User(AbstractUser):
|
|||
|
||||
return roles
|
||||
|
||||
def get_portfolios(self):
|
||||
return self.portfolio_permissions.all()
|
||||
|
||||
@classmethod
|
||||
def needs_identity_verification(cls, email, uuid):
|
||||
"""A method used by our oidc classes to test whether a user needs email/uuid verification
|
||||
|
@ -437,8 +454,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 +462,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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -107,7 +107,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% endif %}
|
||||
{% elif field.field.name == "requested_domain" %}
|
||||
{% with current_path=request.get_full_path %}
|
||||
<a class="margin-top-05 padding-top-05" href="{% url 'admin:registrar_draftdomain_change' original.requested_domain.id %}?{{ 'return_path='|add:current_path }}">{{ original.requested_domain }}</a>
|
||||
<a class="margin-top-05 padding-top-05" id="id_requested_domain" href="{% url 'admin:registrar_draftdomain_change' original.requested_domain.id %}?{{ 'return_path='|add:current_path }}">{{ original.requested_domain }}</a>
|
||||
{% endwith%}
|
||||
{% elif field.field.name == "current_websites" %}
|
||||
{% comment %}
|
||||
|
|
|
@ -17,6 +17,26 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block after_related_objects %}
|
||||
{% if portfolios %}
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Portfolio information</h2>
|
||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Portfolios</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for portfolio in portfolios %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_portfolio_change' portfolio.pk %}">
|
||||
{{ portfolio }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Associated requests and domains</h2>
|
||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}">
|
||||
|
@ -52,21 +52,55 @@
|
|||
</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 has one 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 -->
|
||||
|
|
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 %}
|
||||
<div id="main-content">
|
||||
<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 %}
|
||||
</div>
|
||||
|
|
|
@ -908,6 +908,7 @@ def completed_domain_request( # noqa
|
|||
federal_type=None,
|
||||
action_needed_reason=None,
|
||||
portfolio=None,
|
||||
organization_name=None,
|
||||
):
|
||||
"""A completed domain request."""
|
||||
if not user:
|
||||
|
@ -943,7 +944,7 @@ def completed_domain_request( # noqa
|
|||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
organization_name=organization_name if organization_name else "Testorg",
|
||||
address_line1="address 1",
|
||||
address_line2="address 2",
|
||||
state_territory="NY",
|
||||
|
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
|||
from django.utils import timezone
|
||||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django_webtest import WebTest # type: ignore
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
from django.urls import reverse
|
||||
from registrar.admin import (
|
||||
|
@ -41,13 +42,12 @@ from registrar.models import (
|
|||
TransitionDomain,
|
||||
Portfolio,
|
||||
Suborganization,
|
||||
UserPortfolioPermission,
|
||||
UserDomainRole,
|
||||
SeniorOfficial,
|
||||
PortfolioInvitation,
|
||||
VerifiedByStaff,
|
||||
)
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.senior_official import SeniorOfficial
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||
from .common import (
|
||||
MockDbForSharedTests,
|
||||
AuditedAdminMockData,
|
||||
|
@ -60,11 +60,12 @@ from .common import (
|
|||
multiple_unalphabetical_domain_objects,
|
||||
GenericTestHelper,
|
||||
)
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from unittest.mock import ANY, patch, Mock
|
||||
from django_webtest import WebTest # type: ignore
|
||||
|
||||
|
||||
|
||||
import logging
|
||||
|
@ -963,7 +964,7 @@ class TestListHeaderAdmin(TestCase):
|
|||
)
|
||||
|
||||
|
||||
class TestMyUserAdmin(MockDbForSharedTests):
|
||||
class TestMyUserAdmin(MockDbForSharedTests, WebTest):
|
||||
"""Tests for the MyUserAdmin class as super or staff user
|
||||
|
||||
Notes:
|
||||
|
@ -983,6 +984,7 @@ class TestMyUserAdmin(MockDbForSharedTests):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.app.set_user(self.superuser.username)
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -1217,6 +1219,20 @@ class TestMyUserAdmin(MockDbForSharedTests):
|
|||
self.assertNotContains(response, "Portfolio roles:")
|
||||
self.assertNotContains(response, "Portfolio additional permissions:")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_can_see_related_portfolios(self):
|
||||
"""Tests if a user can see the portfolios they are associated with on the user page"""
|
||||
portfolio, _ = Portfolio.objects.get_or_create(organization_name="test", creator=self.superuser)
|
||||
permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.superuser, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
response = self.app.get(reverse("admin:registrar_user_change", args=[self.superuser.pk]))
|
||||
expected_href = reverse("admin:registrar_portfolio_change", args=[portfolio.pk])
|
||||
self.assertContains(response, expected_href)
|
||||
self.assertContains(response, str(portfolio))
|
||||
permission.delete()
|
||||
portfolio.delete()
|
||||
|
||||
|
||||
class AuditedAdminTest(TestCase):
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import copy
|
||||
import boto3_mocking # type: ignore
|
||||
from datetime import date, datetime, time
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
|
@ -8,6 +9,7 @@ from django.utils import timezone
|
|||
from django.utils.module_loading import import_string
|
||||
import logging
|
||||
import pyzipper
|
||||
from django.core.management.base import CommandError
|
||||
from registrar.management.commands.clean_tables import Command as CleanTablesCommand
|
||||
from registrar.management.commands.export_tables import Command as ExportTablesCommand
|
||||
from registrar.models import (
|
||||
|
@ -23,14 +25,17 @@ from registrar.models import (
|
|||
VerifiedByStaff,
|
||||
PublicContact,
|
||||
FederalAgency,
|
||||
Portfolio,
|
||||
Suborganization,
|
||||
)
|
||||
import tablib
|
||||
from unittest.mock import patch, call, MagicMock, mock_open
|
||||
from epplibwrapper import commands, common
|
||||
|
||||
from .common import MockEppLib, less_console_noise, completed_domain_request
|
||||
from .common import MockEppLib, less_console_noise, completed_domain_request, MockSESClient
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -1408,3 +1413,137 @@ class TestPopulateFederalAgencyInitialsAndFceb(TestCase):
|
|||
missing_agency.refresh_from_db()
|
||||
self.assertIsNone(missing_agency.initials)
|
||||
self.assertIsNone(missing_agency.is_fceb)
|
||||
|
||||
|
||||
class TestCreateFederalPortfolio(TestCase):
|
||||
|
||||
@less_console_noise_decorator
|
||||
def setUp(self):
|
||||
self.mock_client = MockSESClient()
|
||||
self.user = User.objects.create(username="testuser")
|
||||
self.federal_agency = FederalAgency.objects.create(agency="Test Federal Agency")
|
||||
self.senior_official = SeniorOfficial.objects.create(
|
||||
first_name="first", last_name="last", email="testuser@igorville.gov", federal_agency=self.federal_agency
|
||||
)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
self.domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
federal_agency=self.federal_agency,
|
||||
user=self.user,
|
||||
)
|
||||
self.domain_request.approve()
|
||||
self.domain_info = DomainInformation.objects.filter(domain_request=self.domain_request).get()
|
||||
|
||||
self.domain_request_2 = completed_domain_request(
|
||||
name="sock@igorville.org",
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
federal_agency=self.federal_agency,
|
||||
user=self.user,
|
||||
organization_name="Test Federal Agency",
|
||||
)
|
||||
self.domain_request_2.approve()
|
||||
self.domain_info_2 = DomainInformation.objects.filter(domain_request=self.domain_request_2).get()
|
||||
|
||||
def tearDown(self):
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Suborganization.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
SeniorOfficial.objects.all().delete()
|
||||
FederalAgency.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def run_create_federal_portfolio(self, agency_name, parse_requests=False, parse_domains=False):
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
|
||||
return_value=True,
|
||||
):
|
||||
call_command(
|
||||
"create_federal_portfolio", agency_name, parse_requests=parse_requests, parse_domains=parse_domains
|
||||
)
|
||||
|
||||
def test_create_or_modify_portfolio(self):
|
||||
"""Test portfolio creation and modification with suborg and senior official."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||
|
||||
portfolio = Portfolio.objects.get(federal_agency=self.federal_agency)
|
||||
self.assertEqual(portfolio.organization_name, self.federal_agency.agency)
|
||||
self.assertEqual(portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||
self.assertEqual(portfolio.creator, User.get_default_user())
|
||||
self.assertEqual(portfolio.notes, "Auto-generated record")
|
||||
|
||||
# Test the suborgs
|
||||
suborganizations = Suborganization.objects.filter(portfolio__federal_agency=self.federal_agency)
|
||||
self.assertEqual(suborganizations.count(), 1)
|
||||
self.assertEqual(suborganizations.first().name, "Testorg")
|
||||
|
||||
# Test the senior official
|
||||
self.assertEqual(portfolio.senior_official, self.senior_official)
|
||||
|
||||
def test_handle_portfolio_requests(self):
|
||||
"""Verify portfolio association with domain requests."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||
|
||||
self.domain_request.refresh_from_db()
|
||||
self.assertIsNotNone(self.domain_request.portfolio)
|
||||
self.assertEqual(self.domain_request.portfolio.federal_agency, self.federal_agency)
|
||||
self.assertEqual(self.domain_request.sub_organization.name, "Testorg")
|
||||
|
||||
def test_handle_portfolio_domains(self):
|
||||
"""Check portfolio association with domain information."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_domains=True)
|
||||
|
||||
self.domain_info.refresh_from_db()
|
||||
self.assertIsNotNone(self.domain_info.portfolio)
|
||||
self.assertEqual(self.domain_info.portfolio.federal_agency, self.federal_agency)
|
||||
self.assertEqual(self.domain_info.sub_organization.name, "Testorg")
|
||||
|
||||
def test_handle_parse_both(self):
|
||||
"""Ensure correct parsing of both requests and domains."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True, parse_domains=True)
|
||||
|
||||
self.domain_request.refresh_from_db()
|
||||
self.domain_info.refresh_from_db()
|
||||
self.assertIsNotNone(self.domain_request.portfolio)
|
||||
self.assertIsNotNone(self.domain_info.portfolio)
|
||||
self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio)
|
||||
|
||||
def test_command_error_no_parse_options(self):
|
||||
"""Verify error when no parse options are provided."""
|
||||
with self.assertRaisesRegex(
|
||||
CommandError, "You must specify at least one of --parse_requests or --parse_domains."
|
||||
):
|
||||
self.run_create_federal_portfolio("Test Federal Agency")
|
||||
|
||||
def test_command_error_agency_not_found(self):
|
||||
"""Check error handling for non-existent agency."""
|
||||
expected_message = (
|
||||
"Cannot find the federal agency 'Non-existent Agency' in our database. "
|
||||
"The value you enter for `agency_name` must be prepopulated in the FederalAgency table before proceeding."
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, expected_message):
|
||||
self.run_create_federal_portfolio("Non-existent Agency", parse_requests=True)
|
||||
|
||||
def test_update_existing_portfolio(self):
|
||||
"""Test updating an existing portfolio."""
|
||||
# Create an existing portfolio
|
||||
existing_portfolio = Portfolio.objects.create(
|
||||
federal_agency=self.federal_agency,
|
||||
organization_name="Test Federal Agency",
|
||||
organization_type=DomainRequest.OrganizationChoices.CITY,
|
||||
creator=self.user,
|
||||
notes="Old notes",
|
||||
)
|
||||
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||
|
||||
existing_portfolio.refresh_from_db()
|
||||
self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency)
|
||||
self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||
|
||||
# Notes and creator should be untouched
|
||||
self.assertEqual(existing_portfolio.notes, "Old notes")
|
||||
self.assertEqual(existing_portfolio.creator, self.user)
|
||||
|
|
|
@ -1133,7 +1133,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,
|
||||
|
@ -1324,16 +1324,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
|
||||
|
@ -1341,8 +1341,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
|
||||
|
@ -1351,15 +1351,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
|
||||
|
@ -1368,7 +1370,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
|
||||
|
@ -1383,6 +1385,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.
|
||||
|
@ -1545,8 +1615,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)
|
||||
|
@ -1560,8 +1630,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)
|
||||
|
@ -1570,16 +1640,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)
|
||||
|
|
|
@ -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,208 @@ 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 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")
|
||||
|
|
|
@ -18,12 +18,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__)
|
||||
|
@ -2771,6 +2773,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."""
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -173,7 +173,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:
|
||||
|
@ -803,6 +803,23 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
)
|
||||
return None
|
||||
|
||||
# Check to see if an invite has already been sent
|
||||
try:
|
||||
invite = DomainInvitation.objects.get(email=email, domain=self.object)
|
||||
# check if the invite has already been accepted
|
||||
if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
|
||||
add_success = False
|
||||
messages.warning(
|
||||
self.request,
|
||||
f"{email} is already a manager for this domain.",
|
||||
)
|
||||
else:
|
||||
add_success = False
|
||||
# else if it has been sent but not accepted
|
||||
messages.warning(self.request, f"{email} has already been invited to this domain")
|
||||
except Exception:
|
||||
logger.error("An error occured")
|
||||
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/domain_invitation.txt",
|
||||
|
@ -828,24 +845,13 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
|
||||
def _make_invitation(self, email_address: str, requestor: User):
|
||||
"""Make a Domain invitation for this email and redirect with a message."""
|
||||
# Check to see if an invite has already been sent (NOTE: we do not want to create an invite just yet.)
|
||||
try:
|
||||
invite = DomainInvitation.objects.get(email=email_address, domain=self.object)
|
||||
# that invitation already existed
|
||||
if invite is not None:
|
||||
messages.warning(
|
||||
self.request,
|
||||
f"{email_address} has already been invited to this domain.",
|
||||
)
|
||||
except DomainInvitation.DoesNotExist:
|
||||
# Try to send the invitation. If it succeeds, add it to the DomainInvitation table.
|
||||
try:
|
||||
self._send_domain_invitation_email(email=email_address, requestor=requestor)
|
||||
except EmailSendingError:
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
else:
|
||||
# (NOTE: only create a domainInvitation if the e-mail sends correctly)
|
||||
DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||
self._send_domain_invitation_email(email=email_address, requestor=requestor)
|
||||
except EmailSendingError:
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
else:
|
||||
# (NOTE: only create a domainInvitation if the e-mail sends correctly)
|
||||
DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -885,11 +891,9 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
except IntegrityError:
|
||||
# User already has the desired role! Do nothing??
|
||||
pass
|
||||
|
||||
messages.success(self.request, f"Added user {requested_email}.")
|
||||
|
||||
messages.warning(self.request, f"{requested_email} is already a manager for this domain")
|
||||
else:
|
||||
messages.success(self.request, f"Added user {requested_email}.")
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
|
|
|
@ -148,7 +148,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
|
||||
|
@ -390,6 +397,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>"
|
||||
|
@ -406,6 +417,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
You’ll 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>'
|
||||
|
@ -421,6 +433,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
|
||||
|
||||
|
@ -497,7 +510,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()
|
||||
|
||||
|
@ -757,7 +774,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):
|
||||
|
|
|
@ -10,16 +10,59 @@ from django.db.models import Q
|
|||
@login_required
|
||||
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"""
|
||||
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
|
||||
)
|
||||
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 +73,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),
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from django.http import JsonResponse
|
||||
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.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
@ -50,7 +50,8 @@ def get_domain_ids_from_request(request):
|
|||
"""
|
||||
portfolio = request.GET.get("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)
|
||||
return domain_infos.values_list("domain_id", flat=True)
|
||||
else:
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue