mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-26 20:48:40 +02:00
Merge remote-tracking branch 'origin/main' into ms/2823-update-delete-domain-process
This commit is contained in:
commit
43484ec6f8
23 changed files with 879 additions and 213 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -3,7 +3,8 @@
|
||||||
docs/research/data/**
|
docs/research/data/**
|
||||||
**/assets/*
|
**/assets/*
|
||||||
!**/assets/src/
|
!**/assets/src/
|
||||||
!**/assets/sass/
|
!**/assets/src/js/
|
||||||
|
!**/assets/src/sass/
|
||||||
!**/assets/img/registrar/
|
!**/assets/img/registrar/
|
||||||
public/
|
public/
|
||||||
credentials*
|
credentials*
|
||||||
|
|
|
@ -893,22 +893,28 @@ Example: `cf ssh getgov-za`
|
||||||
[Follow these steps](#use-scp-to-transfer-data-to-sandboxes) to upload the federal_cio csv to a sandbox of your choice.
|
[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
|
#### Step 5: Running the script
|
||||||
```./manage.py create_federal_portfolio "{federal_agency_name}" --both```
|
To create a specific portfolio:
|
||||||
|
```./manage.py create_federal_portfolio --agency_name "{federal_agency_name}" --both```
|
||||||
Example (only requests): `./manage.py create_federal_portfolio "AMTRAK" --parse_requests`
|
Example (only requests): `./manage.py create_federal_portfolio "AMTRAK" --parse_requests`
|
||||||
|
|
||||||
|
To create a portfolios for all federal agencies in a branch:
|
||||||
|
```./manage.py create_federal_portfolio --branch "{executive|legislative|judicial}" --both```
|
||||||
|
Example (only requests): `./manage.py create_federal_portfolio --branch "executive" --parse_requests`
|
||||||
|
|
||||||
### Running locally
|
### Running locally
|
||||||
|
|
||||||
#### Step 1: Running the script
|
#### Step 1: Running the script
|
||||||
```docker-compose exec app ./manage.py create_federal_portfolio "{federal_agency_name}" --both```
|
```docker-compose exec app ./manage.py create_federal_portfolio --agency_name "{federal_agency_name}" --both```
|
||||||
|
|
||||||
##### Parameters
|
##### Parameters
|
||||||
| | Parameter | Description |
|
| | Parameter | Description |
|
||||||
|:-:|:-------------------------- |:-------------------------------------------------------------------------------------------|
|
|:-:|:-------------------------- |:-------------------------------------------------------------------------------------------|
|
||||||
| 1 | **federal_agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
| 1 | **agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
||||||
| 2 | **both** | If True, runs parse_requests and parse_domains. |
|
| 2 | **branch** | Creates a portfolio for each federal agency in a branch: executive, legislative, judicial |
|
||||||
| 3 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
| 3 | **both** | If True, runs parse_requests and parse_domains. |
|
||||||
| 4 | **parse_domains** | If True, then the created portfolio is added to all related Domains. |
|
| 4 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
||||||
|
| 5 | **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,
|
- Parameters #1-#2: Either `--agency_name` or `--branch` must be specified. Not both.
|
||||||
|
- 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.
|
you must specify at least one to run this script.
|
||||||
|
|
|
@ -85,6 +85,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
entrypoint: /app/node_entrypoint.sh
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
command: ./run_node_watch.sh
|
command: ./run_node_watch.sh
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
FROM docker.io/cimg/node:current-browsers
|
FROM docker.io/cimg/node:current-browsers
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
# Install app dependencies
|
# Install app dependencies
|
||||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||||
# where available (npm@5+)
|
# where available (npm@5+)
|
||||||
COPY --chown=circleci:circleci package*.json ./
|
COPY --chown=circleci:circleci package*.json ./
|
||||||
|
|
||||||
RUN npm install
|
|
24
src/node_entrypoint.sh
Executable file
24
src/node_entrypoint.sh
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get UID and GID of the /app directory owner
|
||||||
|
HOST_UID=$(stat -c '%u' /app)
|
||||||
|
HOST_GID=$(stat -c '%g' /app)
|
||||||
|
|
||||||
|
# Check if the circleci user exists
|
||||||
|
if id "circleci" &>/dev/null; then
|
||||||
|
echo "circleci user exists. Updating UID and GID to match host UID:GID ($HOST_UID:$HOST_GID)"
|
||||||
|
|
||||||
|
# Update circleci user's UID and GID
|
||||||
|
groupmod -g "$HOST_GID" circleci
|
||||||
|
usermod -u "$HOST_UID" circleci
|
||||||
|
|
||||||
|
echo "Updating ownership of /app recursively to circleci:circleci"
|
||||||
|
chown -R circleci:circleci /app
|
||||||
|
|
||||||
|
# Switch to circleci user and execute the command
|
||||||
|
echo "Switching to circleci user and running command: $@"
|
||||||
|
su -s /bin/bash -c "$*" circleci
|
||||||
|
else
|
||||||
|
echo "circleci user does not exist. Running command as the current user."
|
||||||
|
exec "$@"
|
||||||
|
fi
|
|
@ -157,15 +157,15 @@ export function initAddNewMemberPageListeners() {
|
||||||
|
|
||||||
// Populate permission details based on access level
|
// Populate permission details based on access level
|
||||||
if (selectedAccess && selectedAccess.value === 'admin') {
|
if (selectedAccess && selectedAccess.value === 'admin') {
|
||||||
populatePermissionDetails('new-member-admin-permissions')
|
populatePermissionDetails('new-member-admin-permissions');
|
||||||
} else {
|
} else {
|
||||||
populatePermissionDetails('new-member-basic-permissions')
|
populatePermissionDetails('new-member-basic-permissions');
|
||||||
}
|
}
|
||||||
|
|
||||||
//------- Show the modal
|
//------- Show the modal
|
||||||
let modalTrigger = document.querySelector("#invite_member_trigger");
|
let modalTrigger = document.querySelector("#invite_member_trigger");
|
||||||
if (modalTrigger) {
|
if (modalTrigger) {
|
||||||
modalTrigger.click()
|
modalTrigger.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
|
import { hideElement, showElement } from './helpers.js';
|
||||||
|
|
||||||
function setupUrbanizationToggle(stateTerritoryField) {
|
function setupUrbanizationToggle(stateTerritoryField) {
|
||||||
var urbanizationField = document.getElementById('urbanization-field');
|
let urbanizationField = document.getElementById('urbanization-field');
|
||||||
|
if (!urbanizationField) {
|
||||||
|
console.error("Cannot find expect field: #urbanization-field");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleUrbanizationField() {
|
function toggleUrbanizationField() {
|
||||||
// Checking specifically for Puerto Rico only
|
// Checking specifically for Puerto Rico only
|
||||||
if (stateTerritoryField.value === 'PR') {
|
if (stateTerritoryField.value === 'PR') {
|
||||||
urbanizationField.style.display = 'block';
|
showElement(urbanizationField);
|
||||||
} else {
|
} else {
|
||||||
urbanizationField.style.display = 'none';
|
hideElement(urbanizationField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class UserPortfolioPermissionFixture:
|
class UserPortfolioPermissionFixture:
|
||||||
"""Create user portfolio permissions for each user.
|
"""Create user portfolio permissions for each user.
|
||||||
Each user will be admin on 2 portfolios.
|
Each user will be admin on only one portfolio.
|
||||||
|
|
||||||
Depends on fixture_portfolios"""
|
Depends on fixture_portfolios"""
|
||||||
|
|
||||||
|
|
|
@ -13,16 +13,29 @@ logger = logging.getLogger(__name__)
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Creates a federal portfolio given a FederalAgency name"
|
help = "Creates a federal portfolio given a FederalAgency name"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Defines fields to track what portfolios were updated, skipped, or just outright failed."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.updated_portfolios = set()
|
||||||
|
self.skipped_portfolios = set()
|
||||||
|
self.failed_portfolios = set()
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
"""Add three arguments:
|
"""Add three arguments:
|
||||||
1. agency_name => the value of FederalAgency.agency
|
1. agency_name => the value of FederalAgency.agency
|
||||||
2. --parse_requests => if true, adds the given portfolio to each related DomainRequest
|
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
|
3. --parse_domains => if true, adds the given portfolio to each related DomainInformation
|
||||||
"""
|
"""
|
||||||
parser.add_argument(
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
"agency_name",
|
group.add_argument(
|
||||||
|
"--agency_name",
|
||||||
help="The name of the FederalAgency to add",
|
help="The name of the FederalAgency to add",
|
||||||
)
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--branch",
|
||||||
|
choices=["executive", "legislative", "judicial"],
|
||||||
|
help="The federal branch to process. Creates a portfolio for each FederalAgency in this branch.",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--parse_requests",
|
"--parse_requests",
|
||||||
action=argparse.BooleanOptionalAction,
|
action=argparse.BooleanOptionalAction,
|
||||||
|
@ -39,7 +52,9 @@ class Command(BaseCommand):
|
||||||
help="Adds portfolio to both requests and domains",
|
help="Adds portfolio to both requests and domains",
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, agency_name, **options):
|
def handle(self, **options):
|
||||||
|
agency_name = options.get("agency_name")
|
||||||
|
branch = options.get("branch")
|
||||||
parse_requests = options.get("parse_requests")
|
parse_requests = options.get("parse_requests")
|
||||||
parse_domains = options.get("parse_domains")
|
parse_domains = options.get("parse_domains")
|
||||||
both = options.get("both")
|
both = options.get("both")
|
||||||
|
@ -51,84 +66,94 @@ class Command(BaseCommand):
|
||||||
if parse_requests or parse_domains:
|
if parse_requests or parse_domains:
|
||||||
raise CommandError("You cannot pass --parse_requests or --parse_domains when passing --both.")
|
raise CommandError("You cannot pass --parse_requests or --parse_domains when passing --both.")
|
||||||
|
|
||||||
federal_agency = FederalAgency.objects.filter(agency__iexact=agency_name).first()
|
federal_agency_filter = {"agency__iexact": agency_name} if agency_name else {"federal_type": branch}
|
||||||
if not federal_agency:
|
agencies = FederalAgency.objects.filter(**federal_agency_filter)
|
||||||
raise ValueError(
|
if not agencies or agencies.count() < 1:
|
||||||
f"Cannot find the federal agency '{agency_name}' in our database. "
|
if agency_name:
|
||||||
"The value you enter for `agency_name` must be "
|
raise CommandError(
|
||||||
"prepopulated in the FederalAgency table before proceeding."
|
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."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise CommandError(f"Cannot find '{branch}' federal agencies in our database.")
|
||||||
|
|
||||||
portfolio = self.create_or_modify_portfolio(federal_agency)
|
for federal_agency in agencies:
|
||||||
self.create_suborganizations(portfolio, federal_agency)
|
message = f"Processing federal agency '{federal_agency.agency}'..."
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||||
|
try:
|
||||||
|
# C901 'Command.handle' is too complex (12)
|
||||||
|
self.handle_populate_portfolio(federal_agency, parse_domains, parse_requests, both)
|
||||||
|
except Exception as exec:
|
||||||
|
self.failed_portfolios.add(federal_agency)
|
||||||
|
logger.error(exec)
|
||||||
|
message = f"Failed to create portfolio '{federal_agency.agency}'"
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.FAIL, message)
|
||||||
|
|
||||||
|
TerminalHelper.log_script_run_summary(
|
||||||
|
self.updated_portfolios,
|
||||||
|
self.failed_portfolios,
|
||||||
|
self.skipped_portfolios,
|
||||||
|
debug=False,
|
||||||
|
skipped_header="----- SOME PORTFOLIOS WERE SKIPPED -----",
|
||||||
|
display_as_str=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_populate_portfolio(self, federal_agency, parse_domains, parse_requests, both):
|
||||||
|
"""Attempts to create a portfolio. If successful, this function will
|
||||||
|
also create new suborganizations"""
|
||||||
|
portfolio, created = self.create_portfolio(federal_agency)
|
||||||
|
if created:
|
||||||
|
self.create_suborganizations(portfolio, federal_agency)
|
||||||
|
if parse_domains or both:
|
||||||
|
self.handle_portfolio_domains(portfolio, federal_agency)
|
||||||
|
|
||||||
if parse_requests or both:
|
if parse_requests or both:
|
||||||
self.handle_portfolio_requests(portfolio, federal_agency)
|
self.handle_portfolio_requests(portfolio, federal_agency)
|
||||||
|
|
||||||
if parse_domains or both:
|
def create_portfolio(self, federal_agency):
|
||||||
self.handle_portfolio_domains(portfolio, federal_agency)
|
"""Creates a portfolio if it doesn't presently exist.
|
||||||
|
Returns portfolio, created."""
|
||||||
|
# Get the org name / senior official
|
||||||
|
org_name = federal_agency.agency
|
||||||
|
so = federal_agency.so_federal_agency.first() if federal_agency.so_federal_agency.exists() else None
|
||||||
|
|
||||||
def create_or_modify_portfolio(self, federal_agency):
|
# First just try to get an existing portfolio
|
||||||
"""Creates or modifies a portfolio record based on a federal agency."""
|
portfolio = Portfolio.objects.filter(organization_name=org_name).first()
|
||||||
portfolio_args = {
|
if portfolio:
|
||||||
"federal_agency": federal_agency,
|
self.skipped_portfolios.add(portfolio)
|
||||||
"organization_name": federal_agency.agency,
|
TerminalHelper.colorful_logger(
|
||||||
"organization_type": DomainRequest.OrganizationChoices.FEDERAL,
|
logger.info,
|
||||||
"creator": User.get_default_user(),
|
TerminalColors.YELLOW,
|
||||||
"notes": "Auto-generated record",
|
f"Portfolio with organization name '{org_name}' already exists. Skipping create.",
|
||||||
}
|
)
|
||||||
|
return portfolio, False
|
||||||
|
|
||||||
if federal_agency.so_federal_agency.exists():
|
# Create new portfolio if it doesn't exist
|
||||||
portfolio_args["senior_official"] = federal_agency.so_federal_agency.first()
|
portfolio = Portfolio.objects.create(
|
||||||
|
organization_name=org_name,
|
||||||
portfolio, created = Portfolio.objects.get_or_create(
|
federal_agency=federal_agency,
|
||||||
organization_name=portfolio_args.get("organization_name"), defaults=portfolio_args
|
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
creator=User.get_default_user(),
|
||||||
|
notes="Auto-generated record",
|
||||||
|
senior_official=so,
|
||||||
)
|
)
|
||||||
|
|
||||||
if created:
|
self.updated_portfolios.add(portfolio)
|
||||||
message = f"Created portfolio '{portfolio}'"
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, f"Created portfolio '{portfolio}'")
|
||||||
|
|
||||||
|
# Log if the senior official was added or not.
|
||||||
|
if portfolio.senior_official:
|
||||||
|
message = f"Added senior official '{portfolio.senior_official}'"
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
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:
|
else:
|
||||||
proceed = TerminalHelper.prompt_for_execution(
|
message = (
|
||||||
system_exit_on_terminate=False,
|
f"No senior official added to portfolio '{org_name}'. "
|
||||||
prompt_message=f"""
|
"None was returned for the reverse relation `FederalAgency.so_federal_agency.first()`"
|
||||||
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:
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||||
|
|
||||||
# Don't override the creator and notes fields
|
return portfolio, True
|
||||||
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):
|
def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||||
"""Create Suborganizations tied to the given portfolio based on DomainInformation objects"""
|
"""Create Suborganizations tied to the given portfolio based on DomainInformation objects"""
|
||||||
|
@ -146,10 +171,11 @@ class Command(BaseCommand):
|
||||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.FAIL, message)
|
TerminalHelper.colorful_logger(logger.warning, TerminalColors.FAIL, message)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if we need to update any existing suborgs first. This step is optional.
|
# Check for existing suborgs on the current portfolio
|
||||||
existing_suborgs = Suborganization.objects.filter(name__in=org_names)
|
existing_suborgs = Suborganization.objects.filter(name__in=org_names)
|
||||||
if existing_suborgs.exists():
|
if existing_suborgs.exists():
|
||||||
self._update_existing_suborganizations(portfolio, existing_suborgs)
|
message = f"Some suborganizations already exist for portfolio '{portfolio}'."
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, message)
|
||||||
|
|
||||||
# Create new suborgs, as long as they don't exist in the db already
|
# Create new suborgs, as long as they don't exist in the db already
|
||||||
new_suborgs = []
|
new_suborgs = []
|
||||||
|
@ -175,29 +201,6 @@ class Command(BaseCommand):
|
||||||
else:
|
else:
|
||||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, "No suborganizations added")
|
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):
|
def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||||
"""
|
"""
|
||||||
Associate portfolio with domain requests for a federal agency.
|
Associate portfolio with domain requests for a federal agency.
|
||||||
|
@ -208,12 +211,17 @@ class Command(BaseCommand):
|
||||||
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
]
|
]
|
||||||
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency).exclude(status__in=invalid_states)
|
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency, portfolio__isnull=True).exclude(
|
||||||
|
status__in=invalid_states
|
||||||
|
)
|
||||||
if not domain_requests.exists():
|
if not domain_requests.exists():
|
||||||
message = f"""
|
message = f"""
|
||||||
Portfolios not added to domain requests: no valid records found.
|
Portfolio '{portfolio}' 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.
|
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||||
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
||||||
|
Filter info: DomainRequest.objects.filter(federal_agency=federal_agency, portfolio__isnull=True).exclude(
|
||||||
|
status__in=invalid_states
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||||
return None
|
return None
|
||||||
|
@ -224,6 +232,7 @@ class Command(BaseCommand):
|
||||||
domain_request.portfolio = portfolio
|
domain_request.portfolio = portfolio
|
||||||
if domain_request.organization_name in suborgs:
|
if domain_request.organization_name in suborgs:
|
||||||
domain_request.sub_organization = suborgs.get(domain_request.organization_name)
|
domain_request.sub_organization = suborgs.get(domain_request.organization_name)
|
||||||
|
self.updated_portfolios.add(portfolio)
|
||||||
|
|
||||||
DomainRequest.objects.bulk_update(domain_requests, ["portfolio", "sub_organization"])
|
DomainRequest.objects.bulk_update(domain_requests, ["portfolio", "sub_organization"])
|
||||||
message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests."
|
message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests."
|
||||||
|
@ -234,11 +243,12 @@ class Command(BaseCommand):
|
||||||
Associate portfolio with domains for a federal agency.
|
Associate portfolio with domains for a federal agency.
|
||||||
Updates all relevant domain information records.
|
Updates all relevant domain information records.
|
||||||
"""
|
"""
|
||||||
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency)
|
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency, portfolio__isnull=True)
|
||||||
if not domain_infos.exists():
|
if not domain_infos.exists():
|
||||||
message = f"""
|
message = f"""
|
||||||
Portfolios not added to domains: no valid records found.
|
Portfolio '{portfolio}' not added to domains: no valid records found.
|
||||||
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
The filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||||
|
Filter info: DomainInformation.objects.filter(federal_agency=federal_agency, portfolio__isnull=True)
|
||||||
"""
|
"""
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||||
return None
|
return None
|
||||||
|
@ -251,5 +261,5 @@ class Command(BaseCommand):
|
||||||
domain_info.sub_organization = suborgs.get(domain_info.organization_name)
|
domain_info.sub_organization = suborgs.get(domain_info.organization_name)
|
||||||
|
|
||||||
DomainInformation.objects.bulk_update(domain_infos, ["portfolio", "sub_organization"])
|
DomainInformation.objects.bulk_update(domain_infos, ["portfolio", "sub_organization"])
|
||||||
message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains"
|
message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains."
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||||
|
|
|
@ -192,7 +192,7 @@ class PopulateScriptTemplate(ABC):
|
||||||
class TerminalHelper:
|
class TerminalHelper:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def log_script_run_summary(
|
def log_script_run_summary(
|
||||||
to_update, failed_to_update, skipped, debug: bool, log_header=None, display_as_str=False
|
to_update, failed_to_update, skipped, debug: bool, log_header=None, skipped_header=None, display_as_str=False
|
||||||
):
|
):
|
||||||
"""Prints success, failed, and skipped counts, as well as
|
"""Prints success, failed, and skipped counts, as well as
|
||||||
all affected objects."""
|
all affected objects."""
|
||||||
|
@ -203,8 +203,21 @@ class TerminalHelper:
|
||||||
if log_header is None:
|
if log_header is None:
|
||||||
log_header = "============= FINISHED ==============="
|
log_header = "============= FINISHED ==============="
|
||||||
|
|
||||||
|
if skipped_header is None:
|
||||||
|
skipped_header = "----- SOME DATA WAS INVALID (NEEDS MANUAL PATCHING) -----"
|
||||||
|
|
||||||
|
# Give the user the option to see failed / skipped records if any exist.
|
||||||
|
display_detailed_logs = False
|
||||||
|
if not debug and update_failed_count > 0 or update_skipped_count > 0:
|
||||||
|
display_detailed_logs = TerminalHelper.prompt_for_execution(
|
||||||
|
system_exit_on_terminate=False,
|
||||||
|
prompt_message=f"You will see {update_failed_count} failed and {update_skipped_count} skipped records.",
|
||||||
|
verify_message="** Some records were skipped, or some failed to update. **",
|
||||||
|
prompt_title="Do you wish to see the full list of failed, skipped and updated records?",
|
||||||
|
)
|
||||||
|
|
||||||
# Prepare debug messages
|
# Prepare debug messages
|
||||||
if debug:
|
if debug or display_detailed_logs:
|
||||||
updated_display = [str(u) for u in to_update] if display_as_str else to_update
|
updated_display = [str(u) for u in to_update] if display_as_str else to_update
|
||||||
skipped_display = [str(s) for s in skipped] if display_as_str else skipped
|
skipped_display = [str(s) for s in skipped] if display_as_str else skipped
|
||||||
failed_display = [str(f) for f in failed_to_update] if display_as_str else failed_to_update
|
failed_display = [str(f) for f in failed_to_update] if display_as_str else failed_to_update
|
||||||
|
@ -217,7 +230,7 @@ class TerminalHelper:
|
||||||
# Print out a list of everything that was changed, if we have any changes to log.
|
# Print out a list of everything that was changed, if we have any changes to log.
|
||||||
# Otherwise, don't print anything.
|
# Otherwise, don't print anything.
|
||||||
TerminalHelper.print_conditional(
|
TerminalHelper.print_conditional(
|
||||||
debug,
|
True,
|
||||||
f"{debug_messages.get('success') if update_success_count > 0 else ''}"
|
f"{debug_messages.get('success') if update_success_count > 0 else ''}"
|
||||||
f"{debug_messages.get('skipped') if update_skipped_count > 0 else ''}"
|
f"{debug_messages.get('skipped') if update_skipped_count > 0 else ''}"
|
||||||
f"{debug_messages.get('failed') if update_failed_count > 0 else ''}",
|
f"{debug_messages.get('failed') if update_failed_count > 0 else ''}",
|
||||||
|
@ -236,7 +249,7 @@ class TerminalHelper:
|
||||||
f"""{TerminalColors.YELLOW}
|
f"""{TerminalColors.YELLOW}
|
||||||
{log_header}
|
{log_header}
|
||||||
Updated {update_success_count} entries
|
Updated {update_success_count} entries
|
||||||
----- SOME DATA WAS INVALID (NEEDS MANUAL PATCHING) -----
|
{skipped_header}
|
||||||
Skipped updating {update_skipped_count} entries
|
Skipped updating {update_skipped_count} entries
|
||||||
{TerminalColors.ENDC}
|
{TerminalColors.ENDC}
|
||||||
"""
|
"""
|
||||||
|
@ -368,7 +381,9 @@ class TerminalHelper:
|
||||||
logger.info(print_statement)
|
logger.info(print_statement)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def prompt_for_execution(system_exit_on_terminate: bool, prompt_message: str, prompt_title: str) -> bool:
|
def prompt_for_execution(
|
||||||
|
system_exit_on_terminate: bool, prompt_message: str, prompt_title: str, verify_message=None
|
||||||
|
) -> bool:
|
||||||
"""Create to reduce code complexity.
|
"""Create to reduce code complexity.
|
||||||
Prompts the user to inspect the given string
|
Prompts the user to inspect the given string
|
||||||
and asks if they wish to proceed.
|
and asks if they wish to proceed.
|
||||||
|
@ -380,6 +395,9 @@ class TerminalHelper:
|
||||||
if system_exit_on_terminate:
|
if system_exit_on_terminate:
|
||||||
action_description_for_selecting_no = "exit"
|
action_description_for_selecting_no = "exit"
|
||||||
|
|
||||||
|
if verify_message is None:
|
||||||
|
verify_message = "*** IMPORTANT: VERIFY THE FOLLOWING LOOKS CORRECT ***"
|
||||||
|
|
||||||
# Allow the user to inspect the command string
|
# Allow the user to inspect the command string
|
||||||
# and ask if they wish to proceed
|
# and ask if they wish to proceed
|
||||||
proceed_execution = TerminalHelper.query_yes_no_exit(
|
proceed_execution = TerminalHelper.query_yes_no_exit(
|
||||||
|
@ -387,7 +405,7 @@ class TerminalHelper:
|
||||||
=====================================================
|
=====================================================
|
||||||
{prompt_title}
|
{prompt_title}
|
||||||
=====================================================
|
=====================================================
|
||||||
*** IMPORTANT: VERIFY THE FOLLOWING LOOKS CORRECT ***
|
{verify_message}
|
||||||
|
|
||||||
{prompt_message}
|
{prompt_message}
|
||||||
{TerminalColors.FAIL}
|
{TerminalColors.FAIL}
|
||||||
|
|
|
@ -10,18 +10,21 @@ from .host import Host
|
||||||
from .domain_invitation import DomainInvitation
|
from .domain_invitation import DomainInvitation
|
||||||
from .user_domain_role import UserDomainRole
|
from .user_domain_role import UserDomainRole
|
||||||
from .public_contact import PublicContact
|
from .public_contact import PublicContact
|
||||||
|
|
||||||
|
# IMPORTANT: UserPortfolioPermission must be before PortfolioInvitation.
|
||||||
|
# PortfolioInvitation imports from UserPortfolioPermission, so you will get a circular import otherwise.
|
||||||
|
from .user_portfolio_permission import UserPortfolioPermission
|
||||||
|
from .portfolio_invitation import PortfolioInvitation
|
||||||
from .user import User
|
from .user import User
|
||||||
from .user_group import UserGroup
|
from .user_group import UserGroup
|
||||||
from .website import Website
|
from .website import Website
|
||||||
from .transition_domain import TransitionDomain
|
from .transition_domain import TransitionDomain
|
||||||
from .verified_by_staff import VerifiedByStaff
|
from .verified_by_staff import VerifiedByStaff
|
||||||
from .waffle_flag import WaffleFlag
|
from .waffle_flag import WaffleFlag
|
||||||
from .portfolio_invitation import PortfolioInvitation
|
|
||||||
from .portfolio import Portfolio
|
from .portfolio import Portfolio
|
||||||
from .domain_group import DomainGroup
|
from .domain_group import DomainGroup
|
||||||
from .suborganization import Suborganization
|
from .suborganization import Suborganization
|
||||||
from .senior_official import SeniorOfficial
|
from .senior_official import SeniorOfficial
|
||||||
from .user_portfolio_permission import UserPortfolioPermission
|
|
||||||
from .allowed_email import AllowedEmail
|
from .allowed_email import AllowedEmail
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
"""People are invited by email to administer domains."""
|
"""People are invited by email to administer domains."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_fsm import FSMField, transition
|
from django_fsm import FSMField, transition
|
||||||
from registrar.models.domain_invitation import DomainInvitation
|
from django.contrib.auth import get_user_model
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models import DomainInvitation, UserPortfolioPermission
|
||||||
from .utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices # type: ignore
|
from .utility.portfolio_helper import (
|
||||||
|
UserPortfolioPermissionChoices,
|
||||||
|
UserPortfolioRoleChoices,
|
||||||
|
validate_portfolio_invitation,
|
||||||
|
) # type: ignore
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,3 +110,8 @@ class PortfolioInvitation(TimeStampedModel):
|
||||||
if self.additional_permissions and len(self.additional_permissions) > 0:
|
if self.additional_permissions and len(self.additional_permissions) > 0:
|
||||||
user_portfolio_permission.additional_permissions = self.additional_permissions
|
user_portfolio_permission.additional_permissions = self.additional_permissions
|
||||||
user_portfolio_permission.save()
|
user_portfolio_permission.save()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
||||||
|
super().clean()
|
||||||
|
validate_portfolio_invitation(self)
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from registrar.models import DomainInformation, UserDomainRole
|
from registrar.models import DomainInformation, UserDomainRole, PortfolioInvitation, UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
|
|
||||||
from .domain_invitation import DomainInvitation
|
from .domain_invitation import DomainInvitation
|
||||||
from .portfolio_invitation import PortfolioInvitation
|
|
||||||
from .transition_domain import TransitionDomain
|
from .transition_domain import TransitionDomain
|
||||||
from .verified_by_staff import VerifiedByStaff
|
from .verified_by_staff import VerifiedByStaff
|
||||||
from .domain import Domain
|
from .domain import Domain
|
||||||
|
@ -501,8 +499,6 @@ class User(AbstractUser):
|
||||||
def is_only_admin_of_portfolio(self, portfolio):
|
def is_only_admin_of_portfolio(self, portfolio):
|
||||||
"""Check if the user is the only admin of the given portfolio."""
|
"""Check if the user is the only admin of the given portfolio."""
|
||||||
|
|
||||||
UserPortfolioPermission = apps.get_model("registrar", "UserPortfolioPermission")
|
|
||||||
|
|
||||||
admin_permission = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
admin_permission = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||||
|
|
||||||
admins = UserPortfolioPermission.objects.filter(portfolio=portfolio, roles__contains=[admin_permission])
|
admins = UserPortfolioPermission.objects.filter(portfolio=portfolio, roles__contains=[admin_permission])
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import ValidationError
|
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from registrar.utility.waffle import flag_is_active_for_user
|
|
||||||
from registrar.models.utility.portfolio_helper import (
|
from registrar.models.utility.portfolio_helper import (
|
||||||
UserPortfolioPermissionChoices,
|
UserPortfolioPermissionChoices,
|
||||||
UserPortfolioRoleChoices,
|
UserPortfolioRoleChoices,
|
||||||
DomainRequestPermissionDisplay,
|
DomainRequestPermissionDisplay,
|
||||||
MemberPermissionDisplay,
|
MemberPermissionDisplay,
|
||||||
|
validate_user_portfolio_permission,
|
||||||
)
|
)
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
@ -22,18 +21,29 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
||||||
# Domain: field specific permissions
|
# Domain: field specific permissions
|
||||||
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||||
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
||||||
],
|
],
|
||||||
|
# NOTE: Check FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS before adding roles here.
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Determines which roles are forbidden for certain role types to possess.
|
||||||
|
# Used to throw a ValidationError on clean() for UserPortfolioPermission and PortfolioInvitation.
|
||||||
|
FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS = {
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
"registrar.User",
|
"registrar.User",
|
||||||
null=False,
|
null=False,
|
||||||
|
@ -142,30 +152,30 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
else:
|
else:
|
||||||
return MemberPermissionDisplay.NONE
|
return MemberPermissionDisplay.NONE
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_forbidden_permissions(cls, roles, additional_permissions):
|
||||||
|
"""Some permissions are forbidden for certain roles, like member.
|
||||||
|
This checks for conflicts between the current permission list and forbidden perms."""
|
||||||
|
|
||||||
|
# Get the portfolio permissions that the user currently possesses
|
||||||
|
portfolio_permissions = set(cls.get_portfolio_permissions(roles, additional_permissions))
|
||||||
|
|
||||||
|
# Get intersection of forbidden permissions across all roles.
|
||||||
|
# This is because if you have roles ["admin", "member"], then they can have the
|
||||||
|
# so called "forbidden" ones. But just member on their own cannot.
|
||||||
|
# The solution to this is to only grab what is only COMMONLY "forbidden".
|
||||||
|
# This will scale if we add more roles in the future.
|
||||||
|
# This is thes same as applying the `&` operator across all sets for each role.
|
||||||
|
common_forbidden_perms = set.intersection(
|
||||||
|
*[set(cls.FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS.get(role, [])) for role in roles]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the users current permissions overlap with any forbidden permissions
|
||||||
|
# by getting the intersection between current user permissions, and forbidden ones.
|
||||||
|
# This is the same as portfolio_permissions & common_forbidden_perms.
|
||||||
|
return portfolio_permissions.intersection(common_forbidden_perms)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
||||||
super().clean()
|
super().clean()
|
||||||
|
validate_user_portfolio_permission(self)
|
||||||
# Check if portfolio is set without accessing the related object.
|
|
||||||
has_portfolio = bool(self.portfolio_id)
|
|
||||||
if not has_portfolio and self._get_portfolio_permissions():
|
|
||||||
raise ValidationError("When portfolio roles or additional permissions are assigned, portfolio is required.")
|
|
||||||
|
|
||||||
if has_portfolio and not self._get_portfolio_permissions():
|
|
||||||
raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.")
|
|
||||||
|
|
||||||
# Check if a user is set without accessing the related object.
|
|
||||||
has_user = bool(self.user_id)
|
|
||||||
if has_user:
|
|
||||||
existing_permission_pks = UserPortfolioPermission.objects.filter(user=self.user).values_list(
|
|
||||||
"pk", flat=True
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
not flag_is_active_for_user(self.user, "multiple_portfolios")
|
|
||||||
and existing_permission_pks.exists()
|
|
||||||
and self.pk not in existing_permission_pks
|
|
||||||
):
|
|
||||||
raise ValidationError(
|
|
||||||
"This user is already assigned to a portfolio. "
|
|
||||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
from registrar.utility import StrEnum
|
from registrar.utility import StrEnum
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.apps import apps
|
||||||
|
from django.forms import ValidationError
|
||||||
|
from registrar.utility.waffle import flag_is_active_for_user
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
class UserPortfolioRoleChoices(models.TextChoices):
|
class UserPortfolioRoleChoices(models.TextChoices):
|
||||||
|
@ -69,3 +73,131 @@ class MemberPermissionDisplay(StrEnum):
|
||||||
MANAGER = "Manager"
|
MANAGER = "Manager"
|
||||||
VIEWER = "Viewer"
|
VIEWER = "Viewer"
|
||||||
NONE = "None"
|
NONE = "None"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_user_portfolio_permission(user_portfolio_permission):
|
||||||
|
"""
|
||||||
|
Validates a UserPortfolioPermission instance. Located in portfolio_helper to avoid circular imports
|
||||||
|
between PortfolioInvitation and UserPortfolioPermission models.
|
||||||
|
|
||||||
|
Used in UserPortfolioPermission.clean() for model validation.
|
||||||
|
|
||||||
|
Validates:
|
||||||
|
1. A portfolio must be assigned if roles or additional permissions are specified, and vice versa.
|
||||||
|
2. Assigned roles do not include any forbidden permissions.
|
||||||
|
3. If the 'multiple_portfolios' flag is inactive for the user,
|
||||||
|
they must not have existing portfolio permissions or invitations.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If any of the validation rules are violated.
|
||||||
|
"""
|
||||||
|
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||||
|
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||||
|
|
||||||
|
has_portfolio = bool(user_portfolio_permission.portfolio_id)
|
||||||
|
portfolio_permissions = set(user_portfolio_permission._get_portfolio_permissions())
|
||||||
|
|
||||||
|
# == Validate required fields == #
|
||||||
|
if not has_portfolio and portfolio_permissions:
|
||||||
|
raise ValidationError("When portfolio roles or additional permissions are assigned, portfolio is required.")
|
||||||
|
|
||||||
|
if has_portfolio and not portfolio_permissions:
|
||||||
|
raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.")
|
||||||
|
|
||||||
|
# == Validate role permissions. Compares existing permissions to forbidden ones. == #
|
||||||
|
roles = user_portfolio_permission.roles if user_portfolio_permission.roles is not None else []
|
||||||
|
bad_perms = user_portfolio_permission.get_forbidden_permissions(
|
||||||
|
roles, user_portfolio_permission.additional_permissions
|
||||||
|
)
|
||||||
|
if bad_perms:
|
||||||
|
readable_perms = [
|
||||||
|
UserPortfolioPermissionChoices.get_user_portfolio_permission_label(perm) for perm in bad_perms
|
||||||
|
]
|
||||||
|
readable_roles = [UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in roles]
|
||||||
|
raise ValidationError(
|
||||||
|
f"These permissions cannot be assigned to {', '.join(readable_roles)}: <{', '.join(readable_perms)}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# == Validate the multiple_porfolios flag. == #
|
||||||
|
if not flag_is_active_for_user(user_portfolio_permission.user, "multiple_portfolios"):
|
||||||
|
existing_permissions = UserPortfolioPermission.objects.exclude(id=user_portfolio_permission.id).filter(
|
||||||
|
user=user_portfolio_permission.user
|
||||||
|
)
|
||||||
|
if existing_permissions.exists():
|
||||||
|
raise ValidationError(
|
||||||
|
"This user is already assigned to a portfolio. "
|
||||||
|
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_invitations = PortfolioInvitation.objects.filter(email=user_portfolio_permission.user.email)
|
||||||
|
if existing_invitations.exists():
|
||||||
|
raise ValidationError(
|
||||||
|
"This user is already assigned to a portfolio invitation. "
|
||||||
|
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_portfolio_invitation(portfolio_invitation):
|
||||||
|
"""
|
||||||
|
Validates a PortfolioInvitation instance. Located in portfolio_helper to avoid circular imports
|
||||||
|
between PortfolioInvitation and UserPortfolioPermission models.
|
||||||
|
|
||||||
|
Used in PortfolioInvitation.clean() for model validation.
|
||||||
|
|
||||||
|
Validates:
|
||||||
|
1. A portfolio must be assigned if roles or additional permissions are specified, and vice versa.
|
||||||
|
2. Assigned roles do not include any forbidden permissions.
|
||||||
|
3. If the 'multiple_portfolios' flag is inactive for the user,
|
||||||
|
they must not have existing portfolio permissions or invitations.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If any of the validation rules are violated.
|
||||||
|
"""
|
||||||
|
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||||
|
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
has_portfolio = bool(portfolio_invitation.portfolio_id)
|
||||||
|
portfolio_permissions = set(portfolio_invitation.get_portfolio_permissions())
|
||||||
|
|
||||||
|
# == Validate required fields == #
|
||||||
|
if not has_portfolio and portfolio_permissions:
|
||||||
|
raise ValidationError("When portfolio roles or additional permissions are assigned, portfolio is required.")
|
||||||
|
|
||||||
|
if has_portfolio and not portfolio_permissions:
|
||||||
|
raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.")
|
||||||
|
|
||||||
|
# == Validate role permissions. Compares existing permissions to forbidden ones. == #
|
||||||
|
roles = portfolio_invitation.roles if portfolio_invitation.roles is not None else []
|
||||||
|
bad_perms = UserPortfolioPermission.get_forbidden_permissions(roles, portfolio_invitation.additional_permissions)
|
||||||
|
if bad_perms:
|
||||||
|
readable_perms = [
|
||||||
|
UserPortfolioPermissionChoices.get_user_portfolio_permission_label(perm) for perm in bad_perms
|
||||||
|
]
|
||||||
|
readable_roles = [UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in roles]
|
||||||
|
raise ValidationError(
|
||||||
|
f"These permissions cannot be assigned to {', '.join(readable_roles)}: <{', '.join(readable_perms)}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# == Validate the multiple_porfolios flag. == #
|
||||||
|
user = User.objects.filter(email=portfolio_invitation.email).first()
|
||||||
|
# If user returns None, then we check for global assignment of multiple_portfolios.
|
||||||
|
# Otherwise we just check on the user.
|
||||||
|
if not flag_is_active_for_user(user, "multiple_portfolios"):
|
||||||
|
existing_permissions = UserPortfolioPermission.objects.filter(user=user)
|
||||||
|
|
||||||
|
existing_invitations = PortfolioInvitation.objects.exclude(id=portfolio_invitation.id).filter(
|
||||||
|
email=portfolio_invitation.email
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_permissions.exists():
|
||||||
|
raise ValidationError(
|
||||||
|
"This user is already assigned to a portfolio. "
|
||||||
|
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_invitations.exists():
|
||||||
|
raise ValidationError(
|
||||||
|
"This user is already assigned to a portfolio invitation. "
|
||||||
|
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||||
|
)
|
||||||
|
|
|
@ -37,12 +37,9 @@
|
||||||
{% input_with_errors forms.0.zipcode %}
|
{% input_with_errors forms.0.zipcode %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<div id="urbanization-field" style="display: none;">
|
<div id="urbanization-field" class="display-none">
|
||||||
{% input_with_errors forms.0.urbanization %}
|
{% input_with_errors forms.0.urbanization %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<script src="{% static 'js/getgov.min.js' %}" defer></script>
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from waffle.testutils import override_flag
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
@ -25,6 +26,7 @@ from registrar.admin import (
|
||||||
TransitionDomainAdmin,
|
TransitionDomainAdmin,
|
||||||
UserGroupAdmin,
|
UserGroupAdmin,
|
||||||
PortfolioAdmin,
|
PortfolioAdmin,
|
||||||
|
UserPortfolioPermissionAdmin,
|
||||||
)
|
)
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
|
@ -63,8 +65,10 @@ from .common import (
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
from unittest.mock import ANY, patch, Mock
|
from unittest.mock import ANY, patch, Mock
|
||||||
|
from django.forms import ValidationError
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -187,6 +191,93 @@ class TestDomainInvitationAdmin(TestCase):
|
||||||
self.assertContains(response, retrieved_html, count=1)
|
self.assertContains(response, retrieved_html, count=1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserPortfolioPermissionAdmin(TestCase):
|
||||||
|
"""Tests for the PortfolioInivtationAdmin class"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Create a client object"""
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.admin = ListHeaderAdmin(model=UserPortfolioPermissionAdmin, admin_site=AdminSite())
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Delete all DomainInvitation objects"""
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
PortfolioInvitation.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_clean_user_portfolio_permission(self):
|
||||||
|
"""Tests validation of user portfolio permission"""
|
||||||
|
|
||||||
|
# Test validation fails when portfolio missing but permissions are present
|
||||||
|
permission = UserPortfolioPermission(user=self.superuser, roles=["organization_admin"], portfolio=None)
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
permission.clean()
|
||||||
|
self.assertEqual(
|
||||||
|
str(err.exception),
|
||||||
|
"When portfolio roles or additional permissions are assigned, portfolio is required.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test validation fails when portfolio present but no permissions are present
|
||||||
|
permission = UserPortfolioPermission(user=self.superuser, roles=None, portfolio=self.portfolio)
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
permission.clean()
|
||||||
|
self.assertEqual(
|
||||||
|
str(err.exception),
|
||||||
|
"When portfolio is assigned, portfolio roles or additional permissions are required.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test validation fails with forbidden permissions for single role
|
||||||
|
forbidden_member_roles = UserPortfolioPermission.FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS.get(
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||||
|
)
|
||||||
|
permission = UserPortfolioPermission(
|
||||||
|
user=self.superuser,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=forbidden_member_roles,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
permission.clean()
|
||||||
|
self.assertEqual(
|
||||||
|
str(err.exception),
|
||||||
|
"These permissions cannot be assigned to Member: "
|
||||||
|
"<Create and edit members, View all domains and domain reports, View members>",
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_forbidden_permissions_with_multiple_roles(self):
|
||||||
|
"""Tests that forbidden permissions are properly handled when a user has multiple roles"""
|
||||||
|
# Get forbidden permissions for member role
|
||||||
|
member_forbidden = UserPortfolioPermission.FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS.get(
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with both admin and member roles
|
||||||
|
roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN, UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||||
|
|
||||||
|
# These permissions would be forbidden for member alone, but should be allowed
|
||||||
|
# when combined with admin role
|
||||||
|
permissions = UserPortfolioPermission.get_forbidden_permissions(
|
||||||
|
roles=roles, additional_permissions=member_forbidden
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return empty set since no permissions are commonly forbidden between admin and member
|
||||||
|
self.assertEqual(permissions, set())
|
||||||
|
|
||||||
|
# Verify the same permissions are forbidden when only member role is present
|
||||||
|
member_only_permissions = UserPortfolioPermission.get_forbidden_permissions(
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], additional_permissions=member_forbidden
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return the forbidden permissions for member role
|
||||||
|
self.assertEqual(member_only_permissions, set(member_forbidden))
|
||||||
|
|
||||||
|
|
||||||
class TestPortfolioInvitationAdmin(TestCase):
|
class TestPortfolioInvitationAdmin(TestCase):
|
||||||
"""Tests for the PortfolioInvitationAdmin class as super user
|
"""Tests for the PortfolioInvitationAdmin class as super user
|
||||||
|
|
||||||
|
@ -204,9 +295,11 @@ class TestPortfolioInvitationAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Create a client object"""
|
"""Create a client object"""
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Delete all DomainInvitation objects"""
|
"""Delete all DomainInvitation objects"""
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
PortfolioInvitation.objects.all().delete()
|
PortfolioInvitation.objects.all().delete()
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
|
@ -214,6 +307,112 @@ class TestPortfolioInvitationAdmin(TestCase):
|
||||||
def tearDownClass(self):
|
def tearDownClass(self):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("multiple_portfolios", active=False)
|
||||||
|
def test_clean_multiple_portfolios_inactive(self):
|
||||||
|
"""Tests that users cannot have multiple portfolios or invitations when flag is inactive"""
|
||||||
|
# Create the first portfolio permission
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=self.superuser, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test a second portfolio permission object (should fail)
|
||||||
|
second_portfolio = Portfolio.objects.create(organization_name="Second Portfolio", creator=self.superuser)
|
||||||
|
second_permission = UserPortfolioPermission(
|
||||||
|
user=self.superuser, portfolio=second_portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
second_permission.clean()
|
||||||
|
self.assertIn("users cannot be assigned to multiple portfolios", str(err.exception))
|
||||||
|
|
||||||
|
# Test that adding a new portfolio invitation also fails
|
||||||
|
third_portfolio = Portfolio.objects.create(organization_name="Third Portfolio", creator=self.superuser)
|
||||||
|
invitation = PortfolioInvitation(
|
||||||
|
email=self.superuser.email, portfolio=third_portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
invitation.clean()
|
||||||
|
self.assertIn("users cannot be assigned to multiple portfolios", str(err.exception))
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("multiple_portfolios", active=True)
|
||||||
|
def test_clean_multiple_portfolios_active(self):
|
||||||
|
"""Tests that users can have multiple portfolios and invitations when flag is active"""
|
||||||
|
# Create first portfolio permission
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=self.superuser, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Second portfolio permission should succeed
|
||||||
|
second_portfolio = Portfolio.objects.create(organization_name="Second Portfolio", creator=self.superuser)
|
||||||
|
second_permission = UserPortfolioPermission(
|
||||||
|
user=self.superuser, portfolio=second_portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
second_permission.clean()
|
||||||
|
second_permission.save()
|
||||||
|
|
||||||
|
# Verify both permissions exist
|
||||||
|
user_permissions = UserPortfolioPermission.objects.filter(user=self.superuser)
|
||||||
|
self.assertEqual(user_permissions.count(), 2)
|
||||||
|
|
||||||
|
# Portfolio invitation should also succeed
|
||||||
|
third_portfolio = Portfolio.objects.create(organization_name="Third Portfolio", creator=self.superuser)
|
||||||
|
invitation = PortfolioInvitation(
|
||||||
|
email=self.superuser.email, portfolio=third_portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
invitation.clean()
|
||||||
|
invitation.save()
|
||||||
|
|
||||||
|
# Verify invitation exists
|
||||||
|
self.assertTrue(
|
||||||
|
PortfolioInvitation.objects.filter(
|
||||||
|
email=self.superuser.email,
|
||||||
|
portfolio=third_portfolio,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_clean_portfolio_invitation(self):
|
||||||
|
"""Tests validation of portfolio invitation permissions"""
|
||||||
|
|
||||||
|
# Test validation fails when portfolio missing but permissions present
|
||||||
|
invitation = PortfolioInvitation(email="test@example.com", roles=["organization_admin"], portfolio=None)
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
invitation.clean()
|
||||||
|
self.assertEqual(
|
||||||
|
str(err.exception),
|
||||||
|
"When portfolio roles or additional permissions are assigned, portfolio is required.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test validation fails when portfolio present but no permissions
|
||||||
|
invitation = PortfolioInvitation(email="test@example.com", roles=None, portfolio=self.portfolio)
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
invitation.clean()
|
||||||
|
self.assertEqual(
|
||||||
|
str(err.exception),
|
||||||
|
"When portfolio is assigned, portfolio roles or additional permissions are required.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test validation fails with forbidden permissions
|
||||||
|
forbidden_member_roles = UserPortfolioPermission.FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS.get(
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||||
|
)
|
||||||
|
invitation = PortfolioInvitation(
|
||||||
|
email="test@example.com",
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=forbidden_member_roles,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
invitation.clean()
|
||||||
|
self.assertEqual(
|
||||||
|
str(err.exception),
|
||||||
|
"These permissions cannot be assigned to Member: "
|
||||||
|
"<View all domains and domain reports, Create and edit members, View members>",
|
||||||
|
)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -2254,6 +2453,33 @@ class TestTransferUser(WebTest):
|
||||||
|
|
||||||
self.assertEquals(user_portfolio_permission.user, self.user1)
|
self.assertEquals(user_portfolio_permission.user, self.user1)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_transfers_user_portfolio_roles_no_error_when_duplicates(self):
|
||||||
|
"""Assert that duplicate portfolio user roles do not throw errorsd"""
|
||||||
|
portfolio1 = Portfolio.objects.create(organization_name="Hotel California", creator=self.user2)
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user1, portfolio=portfolio1, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user2, portfolio=portfolio1, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(messages, "error"):
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit()
|
||||||
|
|
||||||
|
# Verify portfolio permissions remain valid for the original user
|
||||||
|
self.assertTrue(
|
||||||
|
UserPortfolioPermission.objects.filter(
|
||||||
|
user=self.user1, portfolio=portfolio1, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.error.assert_not_called()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_transfer_user_transfers_domain_request_creator_and_investigator(self):
|
def test_transfer_user_transfers_domain_request_creator_and_investigator(self):
|
||||||
"""Assert that domain request fields get transferred"""
|
"""Assert that domain request fields get transferred"""
|
||||||
|
@ -2308,6 +2534,35 @@ class TestTransferUser(WebTest):
|
||||||
self.assertEquals(user_domain_role1.user, self.user1)
|
self.assertEquals(user_domain_role1.user, self.user1)
|
||||||
self.assertEquals(user_domain_role2.user, self.user1)
|
self.assertEquals(user_domain_role2.user, self.user1)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_transfers_domain_role_no_error_when_duplicate(self):
|
||||||
|
"""Assert that duplicate user domain roles do not throw errors"""
|
||||||
|
domain_1, _ = Domain.objects.get_or_create(name="chrome.gov", state=Domain.State.READY)
|
||||||
|
domain_2, _ = Domain.objects.get_or_create(name="v8.gov", state=Domain.State.READY)
|
||||||
|
UserDomainRole.objects.get_or_create(user=self.user1, domain=domain_1, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
UserDomainRole.objects.get_or_create(user=self.user2, domain=domain_1, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
UserDomainRole.objects.get_or_create(user=self.user2, domain=domain_2, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
|
with patch.object(messages, "error"):
|
||||||
|
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
UserDomainRole.objects.filter(
|
||||||
|
user=self.user1, domain=domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
UserDomainRole.objects.filter(
|
||||||
|
user=self.user1, domain=domain_2, role=UserDomainRole.Roles.MANAGER
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.error.assert_not_called()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_transfer_user_transfers_verified_by_staff_requestor(self):
|
def test_transfer_user_transfers_verified_by_staff_requestor(self):
|
||||||
"""Assert that verified by staff creator gets transferred"""
|
"""Assert that verified by staff creator gets transferred"""
|
||||||
|
|
|
@ -1421,10 +1421,41 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.mock_client = MockSESClient()
|
self.mock_client = MockSESClient()
|
||||||
self.user = User.objects.create(username="testuser")
|
self.user = User.objects.create(username="testuser")
|
||||||
|
|
||||||
|
# Create an agency wih no federal type (can only be created via specifiying it manually)
|
||||||
self.federal_agency = FederalAgency.objects.create(agency="Test Federal Agency")
|
self.federal_agency = FederalAgency.objects.create(agency="Test Federal Agency")
|
||||||
|
|
||||||
|
# And create some with federal_type ones with creative names
|
||||||
|
self.executive_agency_1 = FederalAgency.objects.create(
|
||||||
|
agency="Executive Agency 1", federal_type=BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
self.executive_agency_2 = FederalAgency.objects.create(
|
||||||
|
agency="Executive Agency 2", federal_type=BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
self.executive_agency_3 = FederalAgency.objects.create(
|
||||||
|
agency="Executive Agency 3", federal_type=BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
self.legislative_agency_1 = FederalAgency.objects.create(
|
||||||
|
agency="Legislative Agency 1", federal_type=BranchChoices.LEGISLATIVE
|
||||||
|
)
|
||||||
|
self.legislative_agency_2 = FederalAgency.objects.create(
|
||||||
|
agency="Legislative Agency 2", federal_type=BranchChoices.LEGISLATIVE
|
||||||
|
)
|
||||||
|
self.judicial_agency_1 = FederalAgency.objects.create(
|
||||||
|
agency="Judicial Agency 1", federal_type=BranchChoices.JUDICIAL
|
||||||
|
)
|
||||||
|
self.judicial_agency_2 = FederalAgency.objects.create(
|
||||||
|
agency="Judicial Agency 2", federal_type=BranchChoices.JUDICIAL
|
||||||
|
)
|
||||||
self.senior_official = SeniorOfficial.objects.create(
|
self.senior_official = SeniorOfficial.objects.create(
|
||||||
first_name="first", last_name="last", email="testuser@igorville.gov", federal_agency=self.federal_agency
|
first_name="first", last_name="last", email="testuser@igorville.gov", federal_agency=self.federal_agency
|
||||||
)
|
)
|
||||||
|
self.executive_so_1 = SeniorOfficial.objects.create(
|
||||||
|
first_name="first", last_name="last", email="apple@igorville.gov", federal_agency=self.executive_agency_1
|
||||||
|
)
|
||||||
|
self.executive_so_2 = SeniorOfficial.objects.create(
|
||||||
|
first_name="first", last_name="last", email="mango@igorville.gov", federal_agency=self.executive_agency_2
|
||||||
|
)
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
self.domain_request = completed_domain_request(
|
self.domain_request = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
@ -1436,7 +1467,7 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
self.domain_info = DomainInformation.objects.filter(domain_request=self.domain_request).get()
|
self.domain_info = DomainInformation.objects.filter(domain_request=self.domain_request).get()
|
||||||
|
|
||||||
self.domain_request_2 = completed_domain_request(
|
self.domain_request_2 = completed_domain_request(
|
||||||
name="sock@igorville.org",
|
name="icecreamforigorville.gov",
|
||||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||||
federal_agency=self.federal_agency,
|
federal_agency=self.federal_agency,
|
||||||
|
@ -1446,6 +1477,28 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
self.domain_request_2.approve()
|
self.domain_request_2.approve()
|
||||||
self.domain_info_2 = DomainInformation.objects.filter(domain_request=self.domain_request_2).get()
|
self.domain_info_2 = DomainInformation.objects.filter(domain_request=self.domain_request_2).get()
|
||||||
|
|
||||||
|
self.domain_request_3 = completed_domain_request(
|
||||||
|
name="exec_1.gov",
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
federal_agency=self.executive_agency_1,
|
||||||
|
user=self.user,
|
||||||
|
organization_name="Executive Agency 1",
|
||||||
|
)
|
||||||
|
self.domain_request_3.approve()
|
||||||
|
self.domain_info_3 = self.domain_request_3.DomainRequest_info
|
||||||
|
|
||||||
|
self.domain_request_4 = completed_domain_request(
|
||||||
|
name="exec_2.gov",
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
federal_agency=self.executive_agency_2,
|
||||||
|
user=self.user,
|
||||||
|
organization_name="Executive Agency 2",
|
||||||
|
)
|
||||||
|
self.domain_request_4.approve()
|
||||||
|
self.domain_info_4 = self.domain_request_4.DomainRequest_info
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
|
@ -1456,18 +1509,16 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def run_create_federal_portfolio(self, agency_name, parse_requests=False, parse_domains=False):
|
def run_create_federal_portfolio(self, **kwargs):
|
||||||
with patch(
|
with patch(
|
||||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
|
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
):
|
):
|
||||||
call_command(
|
call_command("create_federal_portfolio", **kwargs)
|
||||||
"create_federal_portfolio", agency_name, parse_requests=parse_requests, parse_domains=parse_domains
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_or_modify_portfolio(self):
|
def test_create_single_portfolio(self):
|
||||||
"""Test portfolio creation and modification with suborg and senior official."""
|
"""Test portfolio creation with suborg and senior official."""
|
||||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True)
|
||||||
|
|
||||||
portfolio = Portfolio.objects.get(federal_agency=self.federal_agency)
|
portfolio = Portfolio.objects.get(federal_agency=self.federal_agency)
|
||||||
self.assertEqual(portfolio.organization_name, self.federal_agency.agency)
|
self.assertEqual(portfolio.organization_name, self.federal_agency.agency)
|
||||||
|
@ -1483,9 +1534,125 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
# Test the senior official
|
# Test the senior official
|
||||||
self.assertEqual(portfolio.senior_official, self.senior_official)
|
self.assertEqual(portfolio.senior_official, self.senior_official)
|
||||||
|
|
||||||
|
def test_create_multiple_portfolios_for_branch_judicial(self):
|
||||||
|
"""Tests creating all portfolios under a given branch"""
|
||||||
|
federal_choice = DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
expected_portfolio_names = {
|
||||||
|
self.judicial_agency_1.agency,
|
||||||
|
self.judicial_agency_2.agency,
|
||||||
|
}
|
||||||
|
self.run_create_federal_portfolio(branch="judicial", parse_requests=True, parse_domains=True)
|
||||||
|
|
||||||
|
# Ensure that all the portfolios we expect to get created were created
|
||||||
|
portfolios = Portfolio.objects.all()
|
||||||
|
self.assertEqual(portfolios.count(), 2)
|
||||||
|
|
||||||
|
# Test that all created portfolios have the correct values
|
||||||
|
org_names, org_types, creators, notes = [], [], [], []
|
||||||
|
for portfolio in portfolios:
|
||||||
|
org_names.append(portfolio.organization_name)
|
||||||
|
org_types.append(portfolio.organization_type)
|
||||||
|
creators.append(portfolio.creator)
|
||||||
|
notes.append(portfolio.notes)
|
||||||
|
|
||||||
|
# Test organization_name, organization_type, creator, and notes (in that order)
|
||||||
|
self.assertTrue(all([org_name in expected_portfolio_names for org_name in org_names]))
|
||||||
|
self.assertTrue(all([org_type == federal_choice for org_type in org_types]))
|
||||||
|
self.assertTrue(all([creator == User.get_default_user() for creator in creators]))
|
||||||
|
self.assertTrue(all([note == "Auto-generated record" for note in notes]))
|
||||||
|
|
||||||
|
def test_create_multiple_portfolios_for_branch_legislative(self):
|
||||||
|
"""Tests creating all portfolios under a given branch"""
|
||||||
|
federal_choice = DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
expected_portfolio_names = {
|
||||||
|
self.legislative_agency_1.agency,
|
||||||
|
self.legislative_agency_2.agency,
|
||||||
|
}
|
||||||
|
self.run_create_federal_portfolio(branch="legislative", parse_requests=True, parse_domains=True)
|
||||||
|
|
||||||
|
# Ensure that all the portfolios we expect to get created were created
|
||||||
|
portfolios = Portfolio.objects.all()
|
||||||
|
self.assertEqual(portfolios.count(), 2)
|
||||||
|
|
||||||
|
# Test that all created portfolios have the correct values
|
||||||
|
org_names, org_types, creators, notes = [], [], [], []
|
||||||
|
for portfolio in portfolios:
|
||||||
|
org_names.append(portfolio.organization_name)
|
||||||
|
org_types.append(portfolio.organization_type)
|
||||||
|
creators.append(portfolio.creator)
|
||||||
|
notes.append(portfolio.notes)
|
||||||
|
|
||||||
|
# Test organization_name, organization_type, creator, and notes (in that order)
|
||||||
|
self.assertTrue(all([org_name in expected_portfolio_names for org_name in org_names]))
|
||||||
|
self.assertTrue(all([org_type == federal_choice for org_type in org_types]))
|
||||||
|
self.assertTrue(all([creator == User.get_default_user() for creator in creators]))
|
||||||
|
self.assertTrue(all([note == "Auto-generated record" for note in notes]))
|
||||||
|
|
||||||
|
def test_create_multiple_portfolios_for_branch_executive(self):
|
||||||
|
"""Tests creating all portfolios under a given branch"""
|
||||||
|
federal_choice = DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
|
||||||
|
# == Test creating executive portfolios == #
|
||||||
|
expected_portfolio_names = {
|
||||||
|
self.executive_agency_1.agency,
|
||||||
|
self.executive_agency_2.agency,
|
||||||
|
self.executive_agency_3.agency,
|
||||||
|
}
|
||||||
|
self.run_create_federal_portfolio(branch="executive", parse_requests=True, parse_domains=True)
|
||||||
|
|
||||||
|
# Ensure that all the portfolios we expect to get created were created
|
||||||
|
portfolios = Portfolio.objects.all()
|
||||||
|
self.assertEqual(portfolios.count(), 3)
|
||||||
|
|
||||||
|
# Test that all created portfolios have the correct values
|
||||||
|
org_names, org_types, creators, notes, senior_officials = [], [], [], [], []
|
||||||
|
for portfolio in portfolios:
|
||||||
|
org_names.append(portfolio.organization_name)
|
||||||
|
org_types.append(portfolio.organization_type)
|
||||||
|
creators.append(portfolio.creator)
|
||||||
|
notes.append(portfolio.notes)
|
||||||
|
senior_officials.append(portfolio.senior_official)
|
||||||
|
|
||||||
|
# Test organization_name, organization_type, creator, and notes (in that order)
|
||||||
|
self.assertTrue(all([org_name in expected_portfolio_names for org_name in org_names]))
|
||||||
|
self.assertTrue(all([org_type == federal_choice for org_type in org_types]))
|
||||||
|
self.assertTrue(all([creator == User.get_default_user() for creator in creators]))
|
||||||
|
self.assertTrue(all([note == "Auto-generated record" for note in notes]))
|
||||||
|
|
||||||
|
# Test senior officials were assigned correctly
|
||||||
|
expected_senior_officials = {
|
||||||
|
self.executive_so_1,
|
||||||
|
self.executive_so_2,
|
||||||
|
# We expect one record to skip
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
self.assertTrue(all([senior_official in expected_senior_officials for senior_official in senior_officials]))
|
||||||
|
|
||||||
|
# Test that domain requests / domains were assigned correctly
|
||||||
|
self.domain_request_3.refresh_from_db()
|
||||||
|
self.domain_request_4.refresh_from_db()
|
||||||
|
self.domain_info_3.refresh_from_db()
|
||||||
|
self.domain_info_4.refresh_from_db()
|
||||||
|
expected_requests = DomainRequest.objects.filter(
|
||||||
|
portfolio__id__in=[
|
||||||
|
# Implicity tests for existence
|
||||||
|
self.domain_request_3.portfolio.id,
|
||||||
|
self.domain_request_4.portfolio.id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
expected_domain_infos = DomainInformation.objects.filter(
|
||||||
|
portfolio__id__in=[
|
||||||
|
# Implicity tests for existence
|
||||||
|
self.domain_info_3.portfolio.id,
|
||||||
|
self.domain_info_4.portfolio.id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(expected_requests.count(), 2)
|
||||||
|
self.assertEqual(expected_domain_infos.count(), 2)
|
||||||
|
|
||||||
def test_handle_portfolio_requests(self):
|
def test_handle_portfolio_requests(self):
|
||||||
"""Verify portfolio association with domain requests."""
|
"""Verify portfolio association with domain requests."""
|
||||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True)
|
||||||
|
|
||||||
self.domain_request.refresh_from_db()
|
self.domain_request.refresh_from_db()
|
||||||
self.assertIsNotNone(self.domain_request.portfolio)
|
self.assertIsNotNone(self.domain_request.portfolio)
|
||||||
|
@ -1494,7 +1661,7 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
|
|
||||||
def test_handle_portfolio_domains(self):
|
def test_handle_portfolio_domains(self):
|
||||||
"""Check portfolio association with domain information."""
|
"""Check portfolio association with domain information."""
|
||||||
self.run_create_federal_portfolio("Test Federal Agency", parse_domains=True)
|
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_domains=True)
|
||||||
|
|
||||||
self.domain_info.refresh_from_db()
|
self.domain_info.refresh_from_db()
|
||||||
self.assertIsNotNone(self.domain_info.portfolio)
|
self.assertIsNotNone(self.domain_info.portfolio)
|
||||||
|
@ -1503,7 +1670,7 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
|
|
||||||
def test_handle_parse_both(self):
|
def test_handle_parse_both(self):
|
||||||
"""Ensure correct parsing of both requests and domains."""
|
"""Ensure correct parsing of both requests and domains."""
|
||||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True, parse_domains=True)
|
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True, parse_domains=True)
|
||||||
|
|
||||||
self.domain_request.refresh_from_db()
|
self.domain_request.refresh_from_db()
|
||||||
self.domain_info.refresh_from_db()
|
self.domain_info.refresh_from_db()
|
||||||
|
@ -1511,12 +1678,26 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
self.assertIsNotNone(self.domain_info.portfolio)
|
self.assertIsNotNone(self.domain_info.portfolio)
|
||||||
self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio)
|
self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio)
|
||||||
|
|
||||||
def test_command_error_no_parse_options(self):
|
def test_command_error_parse_options(self):
|
||||||
"""Verify error when no parse options are provided."""
|
"""Verify error when bad parse options are provided."""
|
||||||
|
# The command should enforce either --branch or --agency_name
|
||||||
|
with self.assertRaisesRegex(CommandError, "Error: one of the arguments --agency_name --branch is required"):
|
||||||
|
self.run_create_federal_portfolio()
|
||||||
|
|
||||||
|
# We should forbid both at the same time
|
||||||
|
with self.assertRaisesRegex(CommandError, "Error: argument --branch: not allowed with argument --agency_name"):
|
||||||
|
self.run_create_federal_portfolio(agency_name="test", branch="executive")
|
||||||
|
|
||||||
|
# We expect a error to be thrown when we dont pass parse requests or domains
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
CommandError, "You must specify at least one of --parse_requests or --parse_domains."
|
CommandError, "You must specify at least one of --parse_requests or --parse_domains."
|
||||||
):
|
):
|
||||||
self.run_create_federal_portfolio("Test Federal Agency")
|
self.run_create_federal_portfolio(branch="executive")
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
CommandError, "You must specify at least one of --parse_requests or --parse_domains."
|
||||||
|
):
|
||||||
|
self.run_create_federal_portfolio(agency_name="test")
|
||||||
|
|
||||||
def test_command_error_agency_not_found(self):
|
def test_command_error_agency_not_found(self):
|
||||||
"""Check error handling for non-existent agency."""
|
"""Check error handling for non-existent agency."""
|
||||||
|
@ -1524,11 +1705,11 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
"Cannot find the federal agency 'Non-existent Agency' in our database. "
|
"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."
|
"The value you enter for `agency_name` must be prepopulated in the FederalAgency table before proceeding."
|
||||||
)
|
)
|
||||||
with self.assertRaisesRegex(ValueError, expected_message):
|
with self.assertRaisesRegex(CommandError, expected_message):
|
||||||
self.run_create_federal_portfolio("Non-existent Agency", parse_requests=True)
|
self.run_create_federal_portfolio(agency_name="Non-existent Agency", parse_requests=True)
|
||||||
|
|
||||||
def test_update_existing_portfolio(self):
|
def test_does_not_update_existing_portfolio(self):
|
||||||
"""Test updating an existing portfolio."""
|
"""Tests that an existing portfolio is not updated"""
|
||||||
# Create an existing portfolio
|
# Create an existing portfolio
|
||||||
existing_portfolio = Portfolio.objects.create(
|
existing_portfolio = Portfolio.objects.create(
|
||||||
federal_agency=self.federal_agency,
|
federal_agency=self.federal_agency,
|
||||||
|
@ -1538,12 +1719,15 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
notes="Old notes",
|
notes="Old notes",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True)
|
||||||
|
|
||||||
existing_portfolio.refresh_from_db()
|
existing_portfolio.refresh_from_db()
|
||||||
self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency)
|
# SANITY CHECK: if the portfolio updates, it will change to FEDERAL.
|
||||||
self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
# if this case fails, it means we are overriding data (and not simply just other weirdness)
|
||||||
|
self.assertNotEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||||
|
|
||||||
# Notes and creator should be untouched
|
# Notes and creator should be untouched
|
||||||
|
self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.CITY)
|
||||||
|
self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency)
|
||||||
self.assertEqual(existing_portfolio.notes, "Old notes")
|
self.assertEqual(existing_portfolio.notes, "Old notes")
|
||||||
self.assertEqual(existing_portfolio.creator, self.user)
|
self.assertEqual(existing_portfolio.creator, self.user)
|
||||||
|
|
|
@ -885,13 +885,13 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,Viewer,True,1,cdomain1.gov\n"
|
"big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,Viewer,True,1,cdomain1.gov\n"
|
||||||
"tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,None,False,0,\n"
|
"tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,None,False,0,\n"
|
||||||
"icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,Manager,False,0,\n"
|
"icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,Manager,False,0,\n"
|
||||||
"cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,None,False,0,\n"
|
"cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer,Viewer,False,0,\n"
|
||||||
"nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Manager,False,0,\n"
|
"nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Manager,False,0,\n"
|
||||||
"nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Viewer,False,0,\n"
|
"nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Viewer,False,0,\n"
|
||||||
"nonexistentmember_3@igorville.gov,False,help@get.gov,Unretrieved,Invited,Viewer,None,False,0,\n"
|
"nonexistentmember_3@igorville.gov,False,help@get.gov,Unretrieved,Invited,Viewer,None,False,0,\n"
|
||||||
"nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,"
|
"nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,"
|
||||||
"Invited,Viewer Requester,Manager,False,0,\n"
|
"Invited,Viewer Requester,Manager,False,0,\n"
|
||||||
"nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer Requester,None,False,0,\n"
|
"nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer,Viewer,False,0,\n"
|
||||||
)
|
)
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
|
|
|
@ -677,18 +677,15 @@ class TestPortfolio(WebTest):
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
def test_cannot_view_members_table(self):
|
def test_cannot_view_members_table(self):
|
||||||
"""Test that user without proper permission is denied access to members view"""
|
"""Test that user without proper permission is denied access to members view."""
|
||||||
|
|
||||||
# Users can only view the members table if they have
|
# Users can only view the members table if they have
|
||||||
# Portfolio Permission "view_members" selected.
|
# Portfolio Permission "view_members" selected.
|
||||||
# NOTE: Admins, by default, do NOT have permission
|
# NOTE: Admins, by default, DO have permission
|
||||||
# to view/edit members. This must be enabled explicitly
|
# to view/edit members.
|
||||||
# in the "additional permissions" section for a portfolio
|
|
||||||
# permission.
|
|
||||||
#
|
|
||||||
# Scenarios to test include;
|
# Scenarios to test include;
|
||||||
# (1) - User is not admin and can view portfolio, but not the members table
|
# (1) - User is not admin and can view portfolio, but not the members table
|
||||||
# (1) - User is admin and can view portfolio, but not the members table
|
# (1) - User is admin and can view portfolio, as well as the members table
|
||||||
|
|
||||||
# --- non-admin
|
# --- non-admin
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
@ -713,11 +710,9 @@ class TestPortfolio(WebTest):
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify that the user cannot access the members page
|
# Admins should have access to this page by default
|
||||||
# This will redirect the user to the members page.
|
|
||||||
response = self.client.get(reverse("members"), follow=True)
|
response = self.client.get(reverse("members"), follow=True)
|
||||||
# Assert the response is a 403 Forbidden
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
|
@ -940,6 +935,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1052,6 +1048,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1060,6 +1057,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1137,7 +1135,10 @@ class TestPortfolio(WebTest):
|
||||||
"""Test the nav contains a dropdown with a link to create and another link to view requests
|
"""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"""
|
Also test for the existence of the Create a new request btn on the requests page"""
|
||||||
UserPortfolioPermission.objects.get_or_create(
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
)
|
)
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
# create and submit a domain request
|
# create and submit a domain request
|
||||||
|
@ -2124,7 +2125,10 @@ class TestRequestingEntity(WebTest):
|
||||||
portfolio=self.portfolio_2,
|
portfolio=self.portfolio_2,
|
||||||
)
|
)
|
||||||
self.portfolio_role = UserPortfolioPermission.objects.create(
|
self.portfolio_role = UserPortfolioPermission.objects.create(
|
||||||
portfolio=self.portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
portfolio=self.portfolio,
|
||||||
|
user=self.user,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
)
|
)
|
||||||
# Login the current user
|
# Login the current user
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
|
|
@ -26,7 +26,7 @@ from registrar.views.domain_request import DomainRequestWizard, Step
|
||||||
|
|
||||||
from .common import less_console_noise
|
from .common import less_console_noise
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices, UserPortfolioPermissionChoices
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -47,10 +47,12 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
DomainRequest.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
UserPortfolioPermission.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
self.federal_agency.delete()
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_request_form_intro_acknowledgement(self):
|
def test_domain_request_form_intro_acknowledgement(self):
|
||||||
|
@ -2753,7 +2755,10 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
"""Tests that a portfolio user with edit request permissions can edit and add new requests"""
|
"""Tests that a portfolio user with edit request permissions can edit and add new requests"""
|
||||||
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
|
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
|
||||||
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
user=self.user,
|
||||||
|
portfolio=portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
)
|
)
|
||||||
|
|
||||||
# This user should be allowed to create new domain requests
|
# This user should be allowed to create new domain requests
|
||||||
|
@ -2765,11 +2770,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
|
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
|
||||||
self.assertEqual(edit_page.status_code, 200)
|
self.assertEqual(edit_page.status_code, 200)
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
DomainRequest.objects.all().delete()
|
|
||||||
portfolio_perm.delete()
|
|
||||||
portfolio.delete()
|
|
||||||
|
|
||||||
def test_non_creator_access(self):
|
def test_non_creator_access(self):
|
||||||
"""Tests that a user cannot edit a domain request they didn't create"""
|
"""Tests that a user cannot edit a domain request they didn't create"""
|
||||||
p = "password"
|
p = "password"
|
||||||
|
@ -2863,7 +2863,10 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
"""Tests that the withdraw button on portfolio redirects to the portfolio domain requests page"""
|
"""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")
|
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
|
||||||
UserPortfolioPermission.objects.get_or_create(
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
user=self.user,
|
||||||
|
portfolio=portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
)
|
)
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
@ -3007,6 +3010,7 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
|
||||||
user=self.user,
|
user=self.user,
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check portfolio-specific breadcrumb
|
# Check portfolio-specific breadcrumb
|
||||||
|
@ -3165,6 +3169,9 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
|
||||||
user=self.user,
|
user=self.user,
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.app.get(f"/domain-request/{domain_request.id}/edit/")
|
response = self.app.get(f"/domain-request/{domain_request.id}/edit/")
|
||||||
|
|
|
@ -116,6 +116,10 @@ class TransferUserView(View):
|
||||||
if model_class.objects.filter(user=current_user, domain=obj.domain).exists():
|
if model_class.objects.filter(user=current_user, domain=obj.domain).exists():
|
||||||
continue # Skip the update to avoid a duplicate
|
continue # Skip the update to avoid a duplicate
|
||||||
|
|
||||||
|
if model_class == UserPortfolioPermission:
|
||||||
|
if model_class.objects.filter(user=current_user, portfolio=obj.portfolio).exists():
|
||||||
|
continue # Skip the update to avoid a duplicate
|
||||||
|
|
||||||
# Update the field on the object and save it
|
# Update the field on the object and save it
|
||||||
setattr(obj, field_name, current_user)
|
setattr(obj, field_name, current_user)
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
npm rebuild
|
npm rebuild
|
||||||
dir=./registrar/assets
|
dir=./registrar/assets
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue