mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 18:09:25 +02:00
Merge branch 'main' into el/2372-change-rejection-title
This commit is contained in:
commit
7b0d7d3b4f
47 changed files with 3435 additions and 766 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*
|
||||||
|
|
|
@ -378,3 +378,18 @@ Then, copy the variables under the section labled `s3`.
|
||||||
## Request Flow FSM Diagram
|
## Request Flow FSM Diagram
|
||||||
|
|
||||||
The [.gov Domain Request & Domain Status Digram](https://miro.com/app/board/uXjVMuqbLOk=/?moveToWidget=3458764594819017396&cot=14) visualizes the domain request flow and resulting domain objects.
|
The [.gov Domain Request & Domain Status Digram](https://miro.com/app/board/uXjVMuqbLOk=/?moveToWidget=3458764594819017396&cot=14) visualizes the domain request flow and resulting domain objects.
|
||||||
|
|
||||||
|
|
||||||
|
## Testing the prototype add DNS record feature (delete this after we are done testing!)
|
||||||
|
We are currently testing using cloudflare to add DNS records. Specifically, an A record. To use this, you will need to enable the
|
||||||
|
`prototype_dns_flag` waffle flag and navigate to `igorville.gov`, `dns.gov`, or `domainops.gov`. Click manage, then click DNS. From there, click the `Prototype DNS record creator` button.
|
||||||
|
|
||||||
|
Before we can send data to cloudflare, you will need these values in your .env file:
|
||||||
|
```
|
||||||
|
REGISTRY_TENANT_KEY = {tenant key}
|
||||||
|
REGISTRY_SERVICE_EMAIL = {An email address}
|
||||||
|
REGISTRY_TENANT_NAME = {Name of the bucket, i.e. "CISA" }
|
||||||
|
```
|
||||||
|
You can obtain these by following the steps outlined in the [dns hosting discovery doc](https://docs.google.com/document/d/1Yq5d2M3MgM2vPhUBZ0k5wOmCQst4vND9-2qEZ55-h-Y/edit?tab=t.0), BUT it is far easier to just get these from someone else. Reach out to Zander for this information if you do not have it.
|
||||||
|
|
||||||
|
Alternatively, if you are testing on a sandbox, you will need to add those to getgov-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.
|
||||||
|
|
|
@ -59,6 +59,9 @@ services:
|
||||||
- AWS_S3_BUCKET_NAME
|
- AWS_S3_BUCKET_NAME
|
||||||
# File encryption credentials
|
# File encryption credentials
|
||||||
- SECRET_ENCRYPT_METADATA
|
- SECRET_ENCRYPT_METADATA
|
||||||
|
- REGISTRY_TENANT_KEY
|
||||||
|
- REGISTRY_SERVICE_EMAIL
|
||||||
|
- REGISTRY_TENANT_NAME
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
ports:
|
ports:
|
||||||
|
@ -85,6 +88,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
|
1130
src/package-lock.json
generated
1130
src/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,14 @@ import logging
|
||||||
import copy
|
import copy
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Value, CharField, Q
|
from django.db.models import (
|
||||||
|
Case,
|
||||||
|
CharField,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
Value,
|
||||||
|
When,
|
||||||
|
)
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from registrar.models.federal_agency import FederalAgency
|
from registrar.models.federal_agency import FederalAgency
|
||||||
|
@ -1467,21 +1474,57 @@ class DomainInformationResource(resources.ModelResource):
|
||||||
class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Customize domain information admin class."""
|
"""Customize domain information admin class."""
|
||||||
|
|
||||||
|
class GenericOrgFilter(admin.SimpleListFilter):
|
||||||
|
"""Custom Generic Organization filter that accomodates portfolio feature.
|
||||||
|
If we have a portfolio, use the portfolio's organization. If not, use the
|
||||||
|
organization in the Domain Information object."""
|
||||||
|
|
||||||
|
title = "generic organization"
|
||||||
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
converted_generic_orgs = set()
|
||||||
|
|
||||||
|
# Populate the set with tuples of (value, display value)
|
||||||
|
for domain_info in DomainInformation.objects.all():
|
||||||
|
converted_generic_org = domain_info.converted_generic_org_type # Actual value
|
||||||
|
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
|
||||||
|
|
||||||
|
if converted_generic_org:
|
||||||
|
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
||||||
|
|
||||||
|
# Sort the set by display value
|
||||||
|
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
||||||
|
|
||||||
|
# Filter queryset
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value(): # Check if a generic org is selected in the filter
|
||||||
|
return queryset.filter(
|
||||||
|
Q(portfolio__organization_type=self.value())
|
||||||
|
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
resource_classes = [DomainInformationResource]
|
resource_classes = [DomainInformationResource]
|
||||||
|
|
||||||
form = DomainInformationAdminForm
|
form = DomainInformationAdminForm
|
||||||
|
|
||||||
|
# Customize column header text
|
||||||
|
@admin.display(description=_("Generic Org Type"))
|
||||||
|
def converted_generic_org_type(self, obj):
|
||||||
|
return obj.converted_generic_org_type_display
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"domain",
|
"domain",
|
||||||
"generic_org_type",
|
"converted_generic_org_type",
|
||||||
"created_at",
|
"created_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
orderable_fk_fields = [("domain", "name")]
|
orderable_fk_fields = [("domain", "name")]
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = ["generic_org_type"]
|
list_filter = [GenericOrgFilter]
|
||||||
|
|
||||||
# Search
|
# Search
|
||||||
search_fields = [
|
search_fields = [
|
||||||
|
@ -1661,24 +1704,23 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
converted_generic_orgs = set()
|
||||||
|
|
||||||
|
# Populate the set with tuples of (value, display value)
|
||||||
for domain_request in DomainRequest.objects.all():
|
for domain_request in DomainRequest.objects.all():
|
||||||
converted_generic_org = domain_request.converted_generic_org_type
|
converted_generic_org = domain_request.converted_generic_org_type # Actual value
|
||||||
if converted_generic_org:
|
converted_generic_org_display = domain_request.converted_generic_org_type_display # Display value
|
||||||
converted_generic_orgs.add(converted_generic_org)
|
|
||||||
|
|
||||||
return sorted((org, org) for org in converted_generic_orgs)
|
if converted_generic_org:
|
||||||
|
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
||||||
|
|
||||||
|
# Sort the set by display value
|
||||||
|
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
||||||
|
|
||||||
# Filter queryset
|
# Filter queryset
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value(): # Check if a generic org is selected in the filter
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
# Filter based on the generic org value returned by converted_generic_org_type
|
Q(portfolio__organization_type=self.value())
|
||||||
id__in=[
|
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||||
domain_request.id
|
|
||||||
for domain_request in queryset
|
|
||||||
if domain_request.converted_generic_org_type
|
|
||||||
and domain_request.converted_generic_org_type == self.value()
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -1693,24 +1735,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_federal_types = set()
|
converted_federal_types = set()
|
||||||
|
|
||||||
|
# Populate the set with tuples of (value, display value)
|
||||||
for domain_request in DomainRequest.objects.all():
|
for domain_request in DomainRequest.objects.all():
|
||||||
converted_federal_type = domain_request.converted_federal_type
|
converted_federal_type = domain_request.converted_federal_type # Actual value
|
||||||
if converted_federal_type:
|
converted_federal_type_display = domain_request.converted_federal_type_display # Display value
|
||||||
converted_federal_types.add(converted_federal_type)
|
|
||||||
|
|
||||||
return sorted((type, type) for type in converted_federal_types)
|
if converted_federal_type:
|
||||||
|
converted_federal_types.add(
|
||||||
|
(converted_federal_type, converted_federal_type_display) # Value, Display
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort the set by display value
|
||||||
|
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
|
||||||
|
|
||||||
# Filter queryset
|
# Filter queryset
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if federal Type is selected in the filter
|
if self.value(): # Check if a federal type is selected in the filter
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
# Filter based on the federal type returned by converted_federal_type
|
Q(portfolio__federal_agency__federal_type=self.value())
|
||||||
id__in=[
|
| Q(portfolio__isnull=True, federal_type=self.value())
|
||||||
domain_request.id
|
|
||||||
for domain_request in queryset
|
|
||||||
if domain_request.converted_federal_type
|
|
||||||
and domain_request.converted_federal_type == self.value()
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -1776,7 +1819,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
@admin.display(description=_("Generic Org Type"))
|
@admin.display(description=_("Generic Org Type"))
|
||||||
def converted_generic_org_type(self, obj):
|
def converted_generic_org_type(self, obj):
|
||||||
return obj.converted_generic_org_type
|
return obj.converted_generic_org_type_display
|
||||||
|
|
||||||
@admin.display(description=_("Organization Name"))
|
@admin.display(description=_("Organization Name"))
|
||||||
def converted_organization_name(self, obj):
|
def converted_organization_name(self, obj):
|
||||||
|
@ -1788,7 +1831,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
@admin.display(description=_("Federal Type"))
|
@admin.display(description=_("Federal Type"))
|
||||||
def converted_federal_type(self, obj):
|
def converted_federal_type(self, obj):
|
||||||
return obj.converted_federal_type
|
return obj.converted_federal_type_display
|
||||||
|
|
||||||
@admin.display(description=_("City"))
|
@admin.display(description=_("City"))
|
||||||
def converted_city(self, obj):
|
def converted_city(self, obj):
|
||||||
|
@ -2679,6 +2722,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
resource_classes = [DomainResource]
|
resource_classes = [DomainResource]
|
||||||
|
|
||||||
|
# ------- FILTERS
|
||||||
class ElectionOfficeFilter(admin.SimpleListFilter):
|
class ElectionOfficeFilter(admin.SimpleListFilter):
|
||||||
"""Define a custom filter for is_election_board"""
|
"""Define a custom filter for is_election_board"""
|
||||||
|
|
||||||
|
@ -2697,18 +2741,135 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if self.value() == "0":
|
if self.value() == "0":
|
||||||
return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None))
|
return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None))
|
||||||
|
|
||||||
|
class GenericOrgFilter(admin.SimpleListFilter):
|
||||||
|
"""Custom Generic Organization filter that accomodates portfolio feature.
|
||||||
|
If we have a portfolio, use the portfolio's organization. If not, use the
|
||||||
|
organization in the Domain Information object."""
|
||||||
|
|
||||||
|
title = "generic organization"
|
||||||
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
converted_generic_orgs = set()
|
||||||
|
|
||||||
|
# Populate the set with tuples of (value, display value)
|
||||||
|
for domain_info in DomainInformation.objects.all():
|
||||||
|
converted_generic_org = domain_info.converted_generic_org_type # Actual value
|
||||||
|
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
|
||||||
|
|
||||||
|
if converted_generic_org:
|
||||||
|
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
||||||
|
|
||||||
|
# Sort the set by display value
|
||||||
|
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
||||||
|
|
||||||
|
# Filter queryset
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value(): # Check if a generic org is selected in the filter
|
||||||
|
return queryset.filter(
|
||||||
|
Q(domain_info__portfolio__organization_type=self.value())
|
||||||
|
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
class FederalTypeFilter(admin.SimpleListFilter):
|
||||||
|
"""Custom Federal Type filter that accomodates portfolio feature.
|
||||||
|
If we have a portfolio, use the portfolio's federal type. If not, use the
|
||||||
|
federal type in the Domain Information object."""
|
||||||
|
|
||||||
|
title = "federal type"
|
||||||
|
parameter_name = "converted_federal_types"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
converted_federal_types = set()
|
||||||
|
|
||||||
|
# Populate the set with tuples of (value, display value)
|
||||||
|
for domain_info in DomainInformation.objects.all():
|
||||||
|
converted_federal_type = domain_info.converted_federal_type # Actual value
|
||||||
|
converted_federal_type_display = domain_info.converted_federal_type_display # Display value
|
||||||
|
|
||||||
|
if converted_federal_type:
|
||||||
|
converted_federal_types.add(
|
||||||
|
(converted_federal_type, converted_federal_type_display) # Value, Display
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort the set by display value
|
||||||
|
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
|
||||||
|
|
||||||
|
# Filter queryset
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value(): # Check if a federal type is selected in the filter
|
||||||
|
return queryset.filter(
|
||||||
|
Q(domain_info__portfolio__federal_agency__federal_type=self.value())
|
||||||
|
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_annotated_queryset(self, queryset):
|
||||||
|
return queryset.annotate(
|
||||||
|
converted_generic_org_type=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__organization_type")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("domain_info__generic_org_type"),
|
||||||
|
),
|
||||||
|
converted_federal_agency=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(
|
||||||
|
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("domain_info__portfolio__federal_agency__agency"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("domain_info__federal_agency__agency"),
|
||||||
|
),
|
||||||
|
converted_federal_type=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(
|
||||||
|
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("domain_info__portfolio__federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("domain_info__federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
converted_organization_name=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__organization_name")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("domain_info__organization_name"),
|
||||||
|
),
|
||||||
|
converted_city=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__city")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("domain_info__city"),
|
||||||
|
),
|
||||||
|
converted_state_territory=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__state_territory")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("domain_info__state_territory"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filters
|
||||||
|
list_filter = [GenericOrgFilter, FederalTypeFilter, ElectionOfficeFilter, "state"]
|
||||||
|
|
||||||
|
# ------- END FILTERS
|
||||||
|
|
||||||
|
# Inlines
|
||||||
inlines = [DomainInformationInline]
|
inlines = [DomainInformationInline]
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"name",
|
"name",
|
||||||
"generic_org_type",
|
"converted_generic_org_type",
|
||||||
"federal_type",
|
"converted_federal_type",
|
||||||
"federal_agency",
|
"converted_federal_agency",
|
||||||
"organization_name",
|
"converted_organization_name",
|
||||||
"custom_election_board",
|
"custom_election_board",
|
||||||
"city",
|
"converted_city",
|
||||||
"state_territory",
|
"converted_state_territory",
|
||||||
"state",
|
"state",
|
||||||
"expiration_date",
|
"expiration_date",
|
||||||
"created_at",
|
"created_at",
|
||||||
|
@ -2723,28 +2884,81 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------- Domain Information Fields
|
||||||
|
|
||||||
|
# --- Generic Org Type
|
||||||
|
# Use converted value in the table
|
||||||
|
@admin.display(description=_("Generic Org Type"))
|
||||||
|
def converted_generic_org_type(self, obj):
|
||||||
|
return obj.domain_info.converted_generic_org_type_display
|
||||||
|
|
||||||
|
converted_generic_org_type.admin_order_field = "converted_generic_org_type" # type: ignore
|
||||||
|
|
||||||
|
# Use native value for the change form
|
||||||
def generic_org_type(self, obj):
|
def generic_org_type(self, obj):
|
||||||
return obj.domain_info.get_generic_org_type_display()
|
return obj.domain_info.get_generic_org_type_display()
|
||||||
|
|
||||||
generic_org_type.admin_order_field = "domain_info__generic_org_type" # type: ignore
|
# --- Federal Agency
|
||||||
|
@admin.display(description=_("Federal Agency"))
|
||||||
|
def converted_federal_agency(self, obj):
|
||||||
|
return obj.domain_info.converted_federal_agency
|
||||||
|
|
||||||
|
converted_federal_agency.admin_order_field = "converted_federal_agency" # type: ignore
|
||||||
|
|
||||||
|
# Use native value for the change form
|
||||||
def federal_agency(self, obj):
|
def federal_agency(self, obj):
|
||||||
if obj.domain_info:
|
if obj.domain_info:
|
||||||
return obj.domain_info.federal_agency
|
return obj.domain_info.federal_agency
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
federal_agency.admin_order_field = "domain_info__federal_agency" # type: ignore
|
# --- Federal Type
|
||||||
|
# Use converted value in the table
|
||||||
|
@admin.display(description=_("Federal Type"))
|
||||||
|
def converted_federal_type(self, obj):
|
||||||
|
return obj.domain_info.converted_federal_type_display
|
||||||
|
|
||||||
|
converted_federal_type.admin_order_field = "converted_federal_type" # type: ignore
|
||||||
|
|
||||||
|
# Use native value for the change form
|
||||||
def federal_type(self, obj):
|
def federal_type(self, obj):
|
||||||
return obj.domain_info.federal_type if obj.domain_info else None
|
return obj.domain_info.federal_type if obj.domain_info else None
|
||||||
|
|
||||||
federal_type.admin_order_field = "domain_info__federal_type" # type: ignore
|
# --- Organization Name
|
||||||
|
# Use converted value in the table
|
||||||
|
@admin.display(description=_("Organization Name"))
|
||||||
|
def converted_organization_name(self, obj):
|
||||||
|
return obj.domain_info.converted_organization_name
|
||||||
|
|
||||||
|
converted_organization_name.admin_order_field = "converted_organization_name" # type: ignore
|
||||||
|
|
||||||
|
# Use native value for the change form
|
||||||
def organization_name(self, obj):
|
def organization_name(self, obj):
|
||||||
return obj.domain_info.organization_name if obj.domain_info else None
|
return obj.domain_info.organization_name if obj.domain_info else None
|
||||||
|
|
||||||
organization_name.admin_order_field = "domain_info__organization_name" # type: ignore
|
# --- City
|
||||||
|
# Use converted value in the table
|
||||||
|
@admin.display(description=_("City"))
|
||||||
|
def converted_city(self, obj):
|
||||||
|
return obj.domain_info.converted_city
|
||||||
|
|
||||||
|
converted_city.admin_order_field = "converted_city" # type: ignore
|
||||||
|
|
||||||
|
# Use native value for the change form
|
||||||
|
def city(self, obj):
|
||||||
|
return obj.domain_info.city if obj.domain_info else None
|
||||||
|
|
||||||
|
# --- State
|
||||||
|
# Use converted value in the table
|
||||||
|
@admin.display(description=_("State / territory"))
|
||||||
|
def converted_state_territory(self, obj):
|
||||||
|
return obj.domain_info.converted_state_territory
|
||||||
|
|
||||||
|
converted_state_territory.admin_order_field = "converted_state_territory" # type: ignore
|
||||||
|
|
||||||
|
# Use native value for the change form
|
||||||
|
def state_territory(self, obj):
|
||||||
|
return obj.domain_info.state_territory if obj.domain_info else None
|
||||||
|
|
||||||
def dnssecdata(self, obj):
|
def dnssecdata(self, obj):
|
||||||
return "Yes" if obj.dnssecdata else "No"
|
return "Yes" if obj.dnssecdata else "No"
|
||||||
|
@ -2777,23 +2991,14 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore
|
custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore
|
||||||
custom_election_board.short_description = "Election office" # type: ignore
|
custom_election_board.short_description = "Election office" # type: ignore
|
||||||
|
|
||||||
def city(self, obj):
|
# Search
|
||||||
return obj.domain_info.city if obj.domain_info else None
|
|
||||||
|
|
||||||
city.admin_order_field = "domain_info__city" # type: ignore
|
|
||||||
|
|
||||||
@admin.display(description=_("State / territory"))
|
|
||||||
def state_territory(self, obj):
|
|
||||||
return obj.domain_info.state_territory if obj.domain_info else None
|
|
||||||
|
|
||||||
state_territory.admin_order_field = "domain_info__state_territory" # type: ignore
|
|
||||||
|
|
||||||
# Filters
|
|
||||||
list_filter = ["domain_info__generic_org_type", "domain_info__federal_type", ElectionOfficeFilter, "state"]
|
|
||||||
|
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
search_help_text = "Search by domain name."
|
search_help_text = "Search by domain name."
|
||||||
|
|
||||||
|
# Change Form
|
||||||
change_form_template = "django/admin/domain_change_form.html"
|
change_form_template = "django/admin/domain_change_form.html"
|
||||||
|
|
||||||
|
# Readonly Fields
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
"state",
|
"state",
|
||||||
"expiration_date",
|
"expiration_date",
|
||||||
|
@ -3058,7 +3263,8 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
||||||
request params."""
|
request params."""
|
||||||
qs = super().get_queryset(request)
|
initial_qs = super().get_queryset(request)
|
||||||
|
qs = self.get_annotated_queryset(initial_qs)
|
||||||
# Check if a 'portfolio' parameter is passed in the request
|
# Check if a 'portfolio' parameter is passed in the request
|
||||||
portfolio_id = request.GET.get("portfolio")
|
portfolio_id = request.GET.get("portfolio")
|
||||||
if portfolio_id:
|
if portfolio_id:
|
||||||
|
@ -3579,6 +3785,14 @@ class WaffleFlagAdmin(FlagAdmin):
|
||||||
model = models.WaffleFlag
|
model = models.WaffleFlag
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
# Hack to get the dns_prototype_flag to auto populate when you navigate to
|
||||||
|
# the waffle flag page.
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["dns_prototype_flag"] = flag_is_active_for_user(request.user, "dns_prototype_flag")
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
list_display = ["name", "portfolio"]
|
list_display = ["name", "portfolio"]
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { initDomainRequestsTable } from './table-domain-requests.js';
|
||||||
import { initMembersTable } from './table-members.js';
|
import { initMembersTable } from './table-members.js';
|
||||||
import { initMemberDomainsTable } from './table-member-domains.js';
|
import { initMemberDomainsTable } from './table-member-domains.js';
|
||||||
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
|
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
|
||||||
|
import { initAddNewMemberPageListeners } from './portfolio-member-page.js';
|
||||||
|
|
||||||
initDomainValidators();
|
initDomainValidators();
|
||||||
|
|
||||||
|
@ -42,3 +43,4 @@ initMembersTable();
|
||||||
initMemberDomainsTable();
|
initMemberDomainsTable();
|
||||||
|
|
||||||
initPortfolioMemberPageToggle();
|
initPortfolioMemberPageToggle();
|
||||||
|
initAddNewMemberPageListeners();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { uswdsInitializeModals } from './helpers-uswds.js';
|
import { uswdsInitializeModals } from './helpers-uswds.js';
|
||||||
|
import { getCsrfToken } from './helpers.js';
|
||||||
import { generateKebabHTML } from './table-base.js';
|
import { generateKebabHTML } from './table-base.js';
|
||||||
import { MembersTable } from './table-members.js';
|
import { MembersTable } from './table-members.js';
|
||||||
|
|
||||||
|
@ -41,3 +42,131 @@ export function initPortfolioMemberPageToggle() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks up specialized listeners for handling form validation and modals
|
||||||
|
* on the Add New Member page.
|
||||||
|
*/
|
||||||
|
export function initAddNewMemberPageListeners() {
|
||||||
|
add_member_form = document.getElementById("add_member_form")
|
||||||
|
if (!add_member_form){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById("confirm_new_member_submit").addEventListener("click", function() {
|
||||||
|
// Upon confirmation, submit the form
|
||||||
|
document.getElementById("add_member_form").submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("add_member_form").addEventListener("submit", function(event) {
|
||||||
|
event.preventDefault(); // Prevents the form from submitting
|
||||||
|
const form = document.getElementById("add_member_form")
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
// Check if the form is valid
|
||||||
|
// If the form is valid, open the confirmation modal
|
||||||
|
// If the form is invalid, submit it to trigger error
|
||||||
|
fetch(form.action, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"X-CSRFToken": getCsrfToken()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.is_valid) {
|
||||||
|
// If the form is valid, show the confirmation modal before submitting
|
||||||
|
openAddMemberConfirmationModal();
|
||||||
|
} else {
|
||||||
|
// If the form is not valid, trigger error messages by firing a submit event
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Helper function to capitalize the first letter in a string (for display purposes)
|
||||||
|
*/
|
||||||
|
function capitalizeFirstLetter(text) {
|
||||||
|
if (!text) return ''; // Return empty string if input is falsy
|
||||||
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Populates contents of the "Add Member" confirmation modal
|
||||||
|
*/
|
||||||
|
function populatePermissionDetails(permission_details_div_id) {
|
||||||
|
const permissionDetailsContainer = document.getElementById("permission_details");
|
||||||
|
permissionDetailsContainer.innerHTML = ""; // Clear previous content
|
||||||
|
|
||||||
|
// Get all permission sections (divs with h3 and radio inputs)
|
||||||
|
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
|
||||||
|
|
||||||
|
permissionSections.forEach(section => {
|
||||||
|
// Find the <h3> element text
|
||||||
|
const sectionTitle = section.textContent;
|
||||||
|
|
||||||
|
// Find the associated radio buttons container (next fieldset)
|
||||||
|
const fieldset = section.nextElementSibling;
|
||||||
|
|
||||||
|
if (fieldset && fieldset.tagName.toLowerCase() === 'fieldset') {
|
||||||
|
// Get the selected radio button within this fieldset
|
||||||
|
const selectedRadio = fieldset.querySelector('input[type="radio"]:checked');
|
||||||
|
|
||||||
|
// If a radio button is selected, get its label text
|
||||||
|
let selectedPermission = "No permission selected";
|
||||||
|
if (selectedRadio) {
|
||||||
|
const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
|
||||||
|
selectedPermission = label ? label.textContent : "No permission selected";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new elements for the modal content
|
||||||
|
const titleElement = document.createElement("h4");
|
||||||
|
titleElement.textContent = sectionTitle;
|
||||||
|
titleElement.classList.add("text-primary");
|
||||||
|
titleElement.classList.add("margin-bottom-0");
|
||||||
|
|
||||||
|
const permissionElement = document.createElement("p");
|
||||||
|
permissionElement.textContent = selectedPermission;
|
||||||
|
permissionElement.classList.add("margin-top-0");
|
||||||
|
|
||||||
|
// Append to the modal content container
|
||||||
|
permissionDetailsContainer.appendChild(titleElement);
|
||||||
|
permissionDetailsContainer.appendChild(permissionElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Updates and opens the "Add Member" confirmation modal.
|
||||||
|
*/
|
||||||
|
function openAddMemberConfirmationModal() {
|
||||||
|
//------- Populate modal details
|
||||||
|
// Get email value
|
||||||
|
let emailValue = document.getElementById('id_email').value;
|
||||||
|
document.getElementById('modalEmail').textContent = emailValue;
|
||||||
|
|
||||||
|
// Get selected radio button for access level
|
||||||
|
let selectedAccess = document.querySelector('input[name="member_access_level"]:checked');
|
||||||
|
// Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button)
|
||||||
|
// This value does not have the first letter capitalized so let's capitalize it
|
||||||
|
let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected";
|
||||||
|
document.getElementById('modalAccessLevel').textContent = accessText;
|
||||||
|
|
||||||
|
// Populate permission details based on access level
|
||||||
|
if (selectedAccess && selectedAccess.value === 'admin') {
|
||||||
|
populatePermissionDetails('new-member-admin-permissions');
|
||||||
|
} else {
|
||||||
|
populatePermissionDetails('new-member-basic-permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
//------- Show the modal
|
||||||
|
let modalTrigger = document.querySelector("#invite_member_trigger");
|
||||||
|
if (modalTrigger) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,11 @@ secret_registry_key = b64decode(secret("REGISTRY_KEY", ""))
|
||||||
secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "")
|
secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "")
|
||||||
secret_registry_hostname = secret("REGISTRY_HOSTNAME")
|
secret_registry_hostname = secret("REGISTRY_HOSTNAME")
|
||||||
|
|
||||||
|
# PROTOTYPE: Used for DNS hosting
|
||||||
|
secret_registry_tenant_key = secret("REGISTRY_TENANT_KEY", None)
|
||||||
|
secret_registry_tenant_name = secret("REGISTRY_TENANT_NAME", None)
|
||||||
|
secret_registry_service_email = secret("REGISTRY_SERVICE_EMAIL", None)
|
||||||
|
|
||||||
# region: Basic Django Config-----------------------------------------------###
|
# region: Basic Django Config-----------------------------------------------###
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / "subdir".
|
# Build paths inside the project like this: BASE_DIR / "subdir".
|
||||||
|
@ -685,6 +690,9 @@ SECRET_REGISTRY_CERT = secret_registry_cert
|
||||||
SECRET_REGISTRY_KEY = secret_registry_key
|
SECRET_REGISTRY_KEY = secret_registry_key
|
||||||
SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase
|
SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase
|
||||||
SECRET_REGISTRY_HOSTNAME = secret_registry_hostname
|
SECRET_REGISTRY_HOSTNAME = secret_registry_hostname
|
||||||
|
SECRET_REGISTRY_TENANT_KEY = secret_registry_tenant_key
|
||||||
|
SECRET_REGISTRY_TENANT_NAME = secret_registry_tenant_name
|
||||||
|
SECRET_REGISTRY_SERVICE_EMAIL = secret_registry_service_email
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# region: Security and Privacy----------------------------------------------###
|
# region: Security and Privacy----------------------------------------------###
|
||||||
|
|
|
@ -46,8 +46,8 @@ DOMAIN_REQUEST_NAMESPACE = views.DomainRequestWizard.URL_NAMESPACE
|
||||||
# dynamically generate the other domain_request_urls
|
# dynamically generate the other domain_request_urls
|
||||||
domain_request_urls = [
|
domain_request_urls = [
|
||||||
path("", RedirectView.as_view(pattern_name="domain-request:start"), name="redirect-to-start"),
|
path("", RedirectView.as_view(pattern_name="domain-request:start"), name="redirect-to-start"),
|
||||||
path("start/", views.DomainRequestWizard.as_view(), name="start"),
|
path("start/", views.DomainRequestWizard.as_view(), name=views.DomainRequestWizard.NEW_URL_NAME),
|
||||||
path("finished/", views.Finished.as_view(), name="finished"),
|
path("finished/", views.Finished.as_view(), name=views.DomainRequestWizard.FINISHED_URL_NAME),
|
||||||
]
|
]
|
||||||
for step, view in [
|
for step, view in [
|
||||||
# add/remove steps here
|
# add/remove steps here
|
||||||
|
@ -255,11 +255,6 @@ urlpatterns = [
|
||||||
ExportDataTypeRequests.as_view(),
|
ExportDataTypeRequests.as_view(),
|
||||||
name="export_data_type_requests",
|
name="export_data_type_requests",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"reports/export_data_type_requests/",
|
|
||||||
ExportDataTypeRequests.as_view(),
|
|
||||||
name="export_data_type_requests",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"domain-request/<int:id>/edit/",
|
"domain-request/<int:id>/edit/",
|
||||||
views.DomainRequestWizard.as_view(),
|
views.DomainRequestWizard.as_view(),
|
||||||
|
@ -298,6 +293,7 @@ urlpatterns = [
|
||||||
name="todo",
|
name="todo",
|
||||||
),
|
),
|
||||||
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
|
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
|
||||||
|
path("domain/<int:pk>/prototype-dns", views.PrototypeDomainDNSRecordView.as_view(), name="prototype-domain-dns"),
|
||||||
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
||||||
path(
|
path(
|
||||||
"domain/<int:pk>/dns",
|
"domain/<int:pk>/dns",
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
|
@ -527,7 +527,12 @@ class DotGovDomainForm(RegistrarForm):
|
||||||
class PurposeForm(RegistrarForm):
|
class PurposeForm(RegistrarForm):
|
||||||
purpose = forms.CharField(
|
purpose = forms.CharField(
|
||||||
label="Purpose",
|
label="Purpose",
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"aria-label": "What is the purpose of your requested domain? Describe how you’ll use your .gov domain. \
|
||||||
|
Will it be used for a website, email, or something else? You can enter up to 2000 characters."
|
||||||
|
}
|
||||||
|
),
|
||||||
validators=[
|
validators=[
|
||||||
MaxLengthValidator(
|
MaxLengthValidator(
|
||||||
2000,
|
2000,
|
||||||
|
@ -794,6 +799,22 @@ class AnythingElseForm(BaseDeletableRegistrarForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioAnythingElseForm(BaseDeletableRegistrarForm):
|
||||||
|
"""The form for the portfolio additional details page. Tied to the anything_else field."""
|
||||||
|
|
||||||
|
anything_else = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label="Anything else?",
|
||||||
|
widget=forms.Textarea(),
|
||||||
|
validators=[
|
||||||
|
MaxLengthValidator(
|
||||||
|
2000,
|
||||||
|
message="Response must be less than 2000 characters.",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AnythingElseYesNoForm(BaseYesNoForm):
|
class AnythingElseYesNoForm(BaseYesNoForm):
|
||||||
"""Yes/no toggle for the anything else question on additional details"""
|
"""Yes/no toggle for the anything else question on additional details"""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import ipaddress
|
||||||
import re
|
import re
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
|
@ -426,13 +426,14 @@ class DomainInformation(TimeStampedModel):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# ----- Portfolio Properties -----
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_organization_name(self):
|
def converted_organization_name(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.organization_name
|
return self.portfolio.organization_name
|
||||||
return self.organization_name
|
return self.organization_name
|
||||||
|
|
||||||
# ----- Portfolio Properties -----
|
|
||||||
@property
|
@property
|
||||||
def converted_generic_org_type(self):
|
def converted_generic_org_type(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
|
@ -442,8 +443,8 @@ class DomainInformation(TimeStampedModel):
|
||||||
@property
|
@property
|
||||||
def converted_federal_agency(self):
|
def converted_federal_agency(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.federal_agency
|
return self.portfolio.federal_agency.agency
|
||||||
return self.federal_agency
|
return self.federal_agency.agency
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_federal_type(self):
|
def converted_federal_type(self):
|
||||||
|
@ -454,20 +455,20 @@ class DomainInformation(TimeStampedModel):
|
||||||
@property
|
@property
|
||||||
def converted_senior_official(self):
|
def converted_senior_official(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.senior_official
|
return self.portfolio.display_senior_official
|
||||||
return self.senior_official
|
return self.display_senior_official
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_address_line1(self):
|
def converted_address_line1(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.address_line1
|
return self.portfolio.display_address_line1
|
||||||
return self.address_line1
|
return self.display_address_line1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_address_line2(self):
|
def converted_address_line2(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.address_line2
|
return self.portfolio.display_address_line2
|
||||||
return self.address_line2
|
return self.display_address_line2
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_city(self):
|
def converted_city(self):
|
||||||
|
@ -478,17 +479,30 @@ class DomainInformation(TimeStampedModel):
|
||||||
@property
|
@property
|
||||||
def converted_state_territory(self):
|
def converted_state_territory(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.state_territory
|
return self.portfolio.get_state_territory_display()
|
||||||
return self.state_territory
|
return self.get_state_territory_display()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_zipcode(self):
|
def converted_zipcode(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.zipcode
|
return self.portfolio.display_zipcode
|
||||||
return self.zipcode
|
return self.display_zipcode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_urbanization(self):
|
def converted_urbanization(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.urbanization
|
return self.portfolio.display_urbanization
|
||||||
return self.urbanization
|
return self.display_urbanization
|
||||||
|
|
||||||
|
# ----- Portfolio Properties (display values)-----
|
||||||
|
@property
|
||||||
|
def converted_generic_org_type_display(self):
|
||||||
|
if self.portfolio:
|
||||||
|
return self.portfolio.get_organization_type_display()
|
||||||
|
return self.get_generic_org_type_display()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def converted_federal_type_display(self):
|
||||||
|
if self.portfolio:
|
||||||
|
return self.portfolio.federal_agency.get_federal_type_display()
|
||||||
|
return self.get_federal_type_display()
|
||||||
|
|
|
@ -1437,6 +1437,18 @@ class DomainRequest(TimeStampedModel):
|
||||||
return self.portfolio.federal_type
|
return self.portfolio.federal_type
|
||||||
return self.federal_type
|
return self.federal_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def converted_address_line1(self):
|
||||||
|
if self.portfolio:
|
||||||
|
return self.portfolio.address_line1
|
||||||
|
return self.address_line1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def converted_address_line2(self):
|
||||||
|
if self.portfolio:
|
||||||
|
return self.portfolio.address_line2
|
||||||
|
return self.address_line2
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_city(self):
|
def converted_city(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
|
@ -1449,8 +1461,33 @@ class DomainRequest(TimeStampedModel):
|
||||||
return self.portfolio.state_territory
|
return self.portfolio.state_territory
|
||||||
return self.state_territory
|
return self.state_territory
|
||||||
|
|
||||||
|
@property
|
||||||
|
def converted_urbanization(self):
|
||||||
|
if self.portfolio:
|
||||||
|
return self.portfolio.urbanization
|
||||||
|
return self.urbanization
|
||||||
|
|
||||||
|
@property
|
||||||
|
def converted_zipcode(self):
|
||||||
|
if self.portfolio:
|
||||||
|
return self.portfolio.zipcode
|
||||||
|
return self.zipcode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_senior_official(self):
|
def converted_senior_official(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.senior_official
|
return self.portfolio.senior_official
|
||||||
return self.senior_official
|
return self.senior_official
|
||||||
|
|
||||||
|
# ----- Portfolio Properties (display values)-----
|
||||||
|
@property
|
||||||
|
def converted_generic_org_type_display(self):
|
||||||
|
if self.portfolio:
|
||||||
|
return self.portfolio.get_organization_type_display()
|
||||||
|
return self.get_generic_org_type_display()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def converted_federal_type_display(self):
|
||||||
|
if self.portfolio:
|
||||||
|
return self.portfolio.federal_agency.get_federal_type_display()
|
||||||
|
return self.get_federal_type_display()
|
||||||
|
|
|
@ -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."
|
||||||
|
)
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
<p>The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to a DNS hosting service and provide us with your name server information.</p>
|
<p>The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to a DNS hosting service and provide us with your name server information.</p>
|
||||||
|
|
||||||
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
|
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
|
||||||
|
|
||||||
|
|
||||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||||
<ul class="usa-list">
|
<ul class="usa-list">
|
||||||
|
@ -35,6 +36,9 @@
|
||||||
|
|
||||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||||
<li><a href="{{ url }}">DNSSEC</a></li>
|
<li><a href="{{ url }}">DNSSEC</a></li>
|
||||||
|
{% if dns_prototype_flag and is_valid_domain %}
|
||||||
|
<li><a href="{% url 'prototype-domain-dns' pk=domain.id %}">Prototype DNS record creator</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% endblock %} {# domain_content #}
|
{% endblock %} {# domain_content #}
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"None" %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -2,18 +2,18 @@
|
||||||
{% load static field_helpers %}
|
{% load static field_helpers %}
|
||||||
|
|
||||||
{% block form_required_fields_help_text %}
|
{% block form_required_fields_help_text %}
|
||||||
{% include "includes/required_fields.html" %}
|
{% comment %} Empty - this step is not required {% endcomment %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
|
|
||||||
<fieldset class="usa-fieldset margin-top-2">
|
<fieldset class="usa-fieldset">
|
||||||
<h2>Is there anything else you’d like us to know about your domain request?</h2>
|
<h2 class="margin-top-0 margin-bottom-0">Is there anything else you’d like us to know about your domain request?</h2>
|
||||||
</legend>
|
</legend>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="margin-top-3" id="anything-else">
|
<div id="anything-else">
|
||||||
<p><em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em></p>
|
<p><em>This question is optional.</em></p>
|
||||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.0.anything_else %}
|
{% input_with_errors forms.0.anything_else %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
<form class="usa-form usa-form--large" method="post" id="add_member_form" novalidate>
|
||||||
|
|
||||||
<fieldset class="usa-fieldset margin-top-2">
|
<fieldset class="usa-fieldset margin-top-2">
|
||||||
<legend>
|
<legend>
|
||||||
<h2>Email</h2>
|
<h2>Email</h2>
|
||||||
|
@ -80,12 +81,17 @@
|
||||||
<h2>Admin access permissions</h2>
|
<h2>Admin access permissions</h2>
|
||||||
<p>Member permissions available for admin-level acccess.</p>
|
<p>Member permissions available for admin-level acccess.</p>
|
||||||
|
|
||||||
<h3 class="margin-bottom-0">Organization domain requests</h3>
|
<h3 class="summary-item__title
|
||||||
|
text-primary-dark
|
||||||
|
margin-bottom-0">Organization domain requests</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||||
{% input_with_errors form.admin_org_domain_request_permissions %}
|
{% input_with_errors form.admin_org_domain_request_permissions %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<h3 class="margin-bottom-0 margin-top-3">Organization members</h3>
|
<h3 class="summary-item__title
|
||||||
|
text-primary-dark
|
||||||
|
margin-bottom-0
|
||||||
|
margin-top-3">Organization members</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||||
{% input_with_errors form.admin_org_members_permissions %}
|
{% input_with_errors form.admin_org_members_permissions %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -94,8 +100,12 @@
|
||||||
<!-- Basic access form -->
|
<!-- Basic access form -->
|
||||||
<div id="new-member-basic-permissions" class="margin-top-2">
|
<div id="new-member-basic-permissions" class="margin-top-2">
|
||||||
<h2>Basic member permissions</h2>
|
<h2>Basic member permissions</h2>
|
||||||
<p>Member permissions available for basic-level access</p>
|
<p>Member permissions available for basic-level acccess.</p>
|
||||||
{% input_with_errors form.basic_org_domain_request_permissions %}
|
|
||||||
|
<h3 class="margin-bottom-0">Organization domain requests</h3>
|
||||||
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||||
|
{% input_with_errors form.basic_org_domain_request_permissions %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit/cancel buttons -->
|
<!-- Submit/cancel buttons -->
|
||||||
|
@ -108,10 +118,76 @@
|
||||||
aria-label="Cancel adding new member"
|
aria-label="Cancel adding new member"
|
||||||
>Cancel
|
>Cancel
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="usa-button">Invite Member</button>
|
<a
|
||||||
|
id="invite_member_trigger"
|
||||||
|
href="#invite-member-modal"
|
||||||
|
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||||
|
aria-controls="invite-member-modal"
|
||||||
|
data-open-modal
|
||||||
|
>Trigger invite member modal</a>
|
||||||
|
<button id="invite_new_member_submit" type="submit" class="usa-button">Invite Member</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="invite-member-modal"
|
||||||
|
aria-labelledby="invite-member-heading"
|
||||||
|
aria-describedby="confirm-invite-description"
|
||||||
|
style="display: none;"
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="invite-member-heading">
|
||||||
|
Invite this member to the organization?
|
||||||
|
</h2>
|
||||||
|
<h3 class="summary-item__title
|
||||||
|
text-primary-dark">Member information and permissions</h3>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<!-- Display email as a header and access level -->
|
||||||
|
<h4 class="text-primary">Email</h4>
|
||||||
|
<p class="margin-top-0" id="modalEmail"></p>
|
||||||
|
|
||||||
|
<h4 class="text-primary">Member Access</h4>
|
||||||
|
<p class="margin-top-0" id="modalAccessLevel"></p>
|
||||||
|
|
||||||
|
<!-- Dynamic Permissions Details -->
|
||||||
|
<div id="permission_details"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button id="confirm_new_member_submit" type="submit" class="usa-button">Yes, invite member</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled"
|
||||||
|
data-close-modal
|
||||||
|
onclick="closeModal()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
onclick="closeModal()"
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock portfolio_content%}
|
{% endblock portfolio_content%}
|
||||||
|
|
||||||
|
|
||||||
|
|
34
src/registrar/templates/prototype_domain_dns.html
Normal file
34
src/registrar/templates/prototype_domain_dns.html
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{% extends "domain_base.html" %}
|
||||||
|
{% load static field_helpers url_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Prototype DNS | {{ domain.name }} | {% endblock %}
|
||||||
|
|
||||||
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
|
<h1>Add DNS records</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is a prototype that demonstrates adding an 'A' record to a zone.
|
||||||
|
Do note that this just adds records, but does not update or delete existing ones.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can only use this functionality on a limited set of domains:
|
||||||
|
<strong>
|
||||||
|
igorville.gov, dns.gov (non-prod), and domainops.gov (non-prod).
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% input_with_errors form.name %}
|
||||||
|
{% input_with_errors form.content %}
|
||||||
|
{% input_with_errors form.ttl %}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button"
|
||||||
|
>
|
||||||
|
Add record
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %} {# domain_content #}
|
|
@ -563,9 +563,12 @@ class MockDb(TestCase):
|
||||||
|
|
||||||
cls.federal_agency_1, _ = FederalAgency.objects.get_or_create(agency="World War I Centennial Commission")
|
cls.federal_agency_1, _ = FederalAgency.objects.get_or_create(agency="World War I Centennial Commission")
|
||||||
cls.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home")
|
cls.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home")
|
||||||
|
cls.federal_agency_3, _ = FederalAgency.objects.get_or_create(
|
||||||
|
agency="Portfolio 1 Federal Agency", federal_type="executive"
|
||||||
|
)
|
||||||
|
|
||||||
cls.portfolio_1, _ = Portfolio.objects.get_or_create(
|
cls.portfolio_1, _ = Portfolio.objects.get_or_create(
|
||||||
creator=cls.custom_superuser, federal_agency=cls.federal_agency_1
|
creator=cls.custom_superuser, federal_agency=cls.federal_agency_3, organization_type="federal"
|
||||||
)
|
)
|
||||||
|
|
||||||
current_date = get_time_aware_date(datetime(2024, 4, 2))
|
current_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -728,9 +728,9 @@ class TestDomainAdminWithClient(TestCase):
|
||||||
response = self.client.get("/admin/registrar/domain/")
|
response = self.client.get("/admin/registrar/domain/")
|
||||||
# There are 4 template references to Federal (4) plus four references in the table
|
# There are 4 template references to Federal (4) plus four references in the table
|
||||||
# for our actual domain_request
|
# for our actual domain_request
|
||||||
self.assertContains(response, "Federal", count=56)
|
self.assertContains(response, "Federal", count=57)
|
||||||
# This may be a bit more robust
|
# This may be a bit more robust
|
||||||
self.assertContains(response, '<td class="field-generic_org_type">Federal</td>', count=1)
|
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
|
||||||
# Now let's make sure the long description does not exist
|
# Now let's make sure the long description does not exist
|
||||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||||
|
|
||||||
|
|
|
@ -576,9 +576,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
|
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
|
||||||
# There are 2 template references to Federal (4) and two in the results data
|
# There are 2 template references to Federal (4) and two in the results data
|
||||||
# of the request
|
# of the request
|
||||||
self.assertContains(response, "Federal", count=51)
|
self.assertContains(response, "Federal", count=55)
|
||||||
# This may be a bit more robust
|
# This may be a bit more robust
|
||||||
self.assertContains(response, '<td class="field-converted_generic_org_type">federal</td>', count=1)
|
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
|
||||||
# Now let's make sure the long description does not exist
|
# Now let's make sure the long description does not exist
|
||||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||||
|
|
||||||
|
@ -1693,7 +1693,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"notes",
|
"notes",
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
]
|
]
|
||||||
self.maxDiff = None
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
def test_readonly_fields_for_analyst(self):
|
def test_readonly_fields_for_analyst(self):
|
||||||
|
@ -1702,7 +1701,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
request.user = self.staffuser
|
request.user = self.staffuser
|
||||||
|
|
||||||
readonly_fields = self.admin.get_readonly_fields(request)
|
readonly_fields = self.admin.get_readonly_fields(request)
|
||||||
self.maxDiff = None
|
|
||||||
expected_fields = [
|
expected_fields = [
|
||||||
"portfolio_senior_official",
|
"portfolio_senior_official",
|
||||||
"portfolio_organization_type",
|
"portfolio_organization_type",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -63,7 +63,6 @@ class TestGroups(TestCase):
|
||||||
|
|
||||||
# Get the codenames of actual permissions associated with the group
|
# Get the codenames of actual permissions associated with the group
|
||||||
actual_permissions = [p.codename for p in cisa_analysts_group.permissions.all()]
|
actual_permissions = [p.codename for p in cisa_analysts_group.permissions.all()]
|
||||||
self.maxDiff = None
|
|
||||||
|
|
||||||
# Assert that the actual permissions match the expected permissions
|
# Assert that the actual permissions match the expected permissions
|
||||||
self.assertListEqual(actual_permissions, expected_permissions)
|
self.assertListEqual(actual_permissions, expected_permissions)
|
||||||
|
|
|
@ -71,8 +71,8 @@ class CsvReportsTest(MockDbForSharedTests):
|
||||||
fake_open = mock_open()
|
fake_open = mock_open()
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
|
call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
|
||||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
|
||||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
]
|
]
|
||||||
|
@ -93,8 +93,8 @@ class CsvReportsTest(MockDbForSharedTests):
|
||||||
fake_open = mock_open()
|
fake_open = mock_open()
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
|
call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
|
||||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
|
||||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
|
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
|
||||||
|
@ -251,32 +251,35 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# We expect READY domains,
|
# We expect READY domains,
|
||||||
# sorted alphabetially by domain name
|
# sorted alphabetially by domain name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO,"
|
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,"
|
||||||
"SO email,Security contact email,Domain managers,Invited domain managers\n"
|
"Organization name,City,State,SO,SO email,"
|
||||||
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,,(blank),,,"
|
"Security contact email,Domain managers,Invited domain managers\n"
|
||||||
"meoward@rocks.com,\n"
|
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
|
||||||
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,"
|
"Portfolio 1 Federal Agency,,,, ,,(blank),"
|
||||||
',,,(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
|
|
||||||
"woofwardthethird@rocks.com\n"
|
|
||||||
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,"
|
|
||||||
"squeaker@rocks.com\n"
|
|
||||||
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
|
|
||||||
"bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
|
|
||||||
"bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
|
|
||||||
"ddomain3.gov,On hold,(blank),2023-11-15,Federal,Armed Forces Retirement Home,,,,,,"
|
|
||||||
"security@mail.gov,,\n"
|
|
||||||
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
|
|
||||||
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
|
|
||||||
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
|
|
||||||
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,,(blank),,,"
|
|
||||||
"meoward@rocks.com,squeaker@rocks.com\n"
|
"meoward@rocks.com,squeaker@rocks.com\n"
|
||||||
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,,(blank),,,meoward@rocks.com,\n"
|
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
|
||||||
|
"Portfolio 1 Federal Agency,,,, ,,(blank),"
|
||||||
|
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
||||||
|
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
|
||||||
|
"World War I Centennial Commission,,,, ,,(blank),"
|
||||||
|
"meoward@rocks.com,\n"
|
||||||
|
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,"
|
||||||
|
"squeaker@rocks.com\n"
|
||||||
|
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
|
"bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
|
"bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
|
"ddomain3.gov,On hold,(blank),2023-11-15,Federal,"
|
||||||
|
"Armed Forces Retirement Home,,,, ,,security@mail.gov,,\n"
|
||||||
|
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
|
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
|
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
|
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,(blank),meoward@rocks.com,\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.maxDiff = None
|
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -312,20 +315,17 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# We expect only domains associated with the user
|
# We expect only domains associated with the user
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
|
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
|
||||||
"City,State,SO,SO email,"
|
"City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
|
||||||
"Security contact email,Domain managers,Invited domain managers\n"
|
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
|
||||||
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,"
|
|
||||||
'(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
|
|
||||||
"woofwardthethird@rocks.com\n"
|
|
||||||
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,(blank),"
|
|
||||||
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
|
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
|
||||||
|
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
|
||||||
|
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.maxDiff = None
|
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -493,17 +493,17 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# sorted alphabetially by domain name
|
# sorted alphabetially by domain name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||||
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
"defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
|
||||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
"cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
"adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
|
||||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
"ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
|
||||||
"zdomain12.gov,Interstate,,,,,(blank)\n"
|
"zdomain12.gov,Interstate,,,,,(blank)\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.maxDiff = None
|
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -533,16 +533,16 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# sorted alphabetially by domain name
|
# sorted alphabetially by domain name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||||
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
"defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
|
||||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
"cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
"adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
|
||||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
"ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.maxDiff = None
|
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -587,13 +587,13 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,"
|
"Domain name,Domain type,Agency,Organization name,City,"
|
||||||
"State,Status,Expiration date, Deleted\n"
|
"State,Status,Expiration date, Deleted\n"
|
||||||
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
|
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Ready,(blank)\n"
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
|
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
||||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
|
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
|
||||||
"zdomain12.govInterstateReady(blank)\n"
|
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
||||||
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
|
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
|
||||||
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank),2024-04-02\n"
|
"sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
||||||
"xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
"xdomain7.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
||||||
)
|
)
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
|
@ -611,7 +611,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
|
|
||||||
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
|
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
|
||||||
She should show twice in this report but not in test_DomainManaged."""
|
She should show twice in this report but not in test_DomainManaged."""
|
||||||
self.maxDiff = None
|
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
|
@ -646,7 +645,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.maxDiff = None
|
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -683,7 +681,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -721,10 +718,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
# @less_console_noise_decorator
|
||||||
def test_domain_request_data_full(self):
|
def test_domain_request_data_full(self):
|
||||||
"""Tests the full domain request report."""
|
"""Tests the full domain request report."""
|
||||||
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
|
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
|
||||||
|
@ -766,35 +762,34 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
csv_file.seek(0)
|
csv_file.seek(0)
|
||||||
# Read the content into a variable
|
# Read the content into a variable
|
||||||
csv_content = csv_file.read()
|
csv_content = csv_file.read()
|
||||||
|
|
||||||
expected_content = (
|
expected_content = (
|
||||||
# Header
|
# Header
|
||||||
"Domain request,Status,Domain type,Federal type,"
|
"Domain request,Status,Domain type,Federal type,Federal agency,Organization name,Election office,"
|
||||||
"Federal agency,Organization name,Election office,City,State/territory,"
|
"City,State/territory,Region,Creator first name,Creator last name,Creator email,"
|
||||||
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
|
"Creator approved domains count,Creator active requests count,Alternative domains,SO first name,"
|
||||||
"Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
|
"SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
|
||||||
"SO title/role,Request purpose,Request additional details,Other contacts,"
|
|
||||||
"CISA regional representative,Current websites,Investigator\n"
|
"CISA regional representative,Current websites,Investigator\n"
|
||||||
# Content
|
# Content
|
||||||
"city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
"city5.gov,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
||||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
"city2.gov,In review,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
|
||||||
"testy@town.com,"
|
"Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
"city3.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,"
|
||||||
'city3.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"cheeseville.gov, city1.gov,'
|
'"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
|
||||||
'igorville.gov",Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,CISA-first-name '
|
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
||||||
"CISA-last-name "
|
'Testy Tester testy2@town.com",'
|
||||||
'| There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, Testy Tester '
|
'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
||||||
'testy2@town.com"'
|
"city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,"
|
||||||
',test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
"Tester,testy@town.com,"
|
||||||
"city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
|
"Testy Tester testy2@town.com,"
|
||||||
"testy2@town.com"
|
"cisaRep@igorville.gov,city.com,\n"
|
||||||
",cisaRep@igorville.gov,city.com,\n"
|
"city6.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
|
||||||
"city6.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
"Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
|
||||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
|
|
||||||
"testy2@town.com,"
|
|
||||||
"cisaRep@igorville.gov,city.com,\n"
|
"cisaRep@igorville.gov,city.com,\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
|
@ -862,7 +857,6 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# Create a request and add the user to the request
|
# Create a request and add the user to the request
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
self.maxDiff = None
|
|
||||||
# Add portfolio to session
|
# Add portfolio to session
|
||||||
request = GenericTestHelper._mock_user_request_for_factory(request)
|
request = GenericTestHelper._mock_user_request_for_factory(request)
|
||||||
request.session["portfolio"] = self.portfolio_1
|
request.session["portfolio"] = self.portfolio_1
|
||||||
|
@ -885,13 +879,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)
|
||||||
|
@ -2384,3 +2388,136 @@ class TestRequestingEntity(WebTest):
|
||||||
self.assertContains(response, "Requesting entity")
|
self.assertContains(response, "Requesting entity")
|
||||||
self.assertContains(response, "moon")
|
self.assertContains(response, "moon")
|
||||||
self.assertContains(response, "kepler, AL")
|
self.assertContains(response, "kepler, AL")
|
||||||
|
|
||||||
|
|
||||||
|
class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
# Create Portfolio
|
||||||
|
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio")
|
||||||
|
|
||||||
|
# Add an invited member who has been invited to manage domains
|
||||||
|
cls.invited_member_email = "invited@example.com"
|
||||||
|
cls.invitation = PortfolioInvitation.objects.create(
|
||||||
|
email=cls.invited_member_email,
|
||||||
|
portfolio=cls.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.new_member_email = "new_user@example.com"
|
||||||
|
|
||||||
|
# Assign permissions to the user making requests
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=cls.user,
|
||||||
|
portfolio=cls.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
PortfolioInvitation.objects.all().delete()
|
||||||
|
UserPortfolioPermission.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_invite_for_new_users(self):
|
||||||
|
"""Tests the member invitation flow for new users."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Simulate a session to ensure continuity
|
||||||
|
session_id = self.client.session.session_key
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Simulate submission of member invite for new user
|
||||||
|
final_response = self.client.post(
|
||||||
|
reverse("new-member"),
|
||||||
|
{
|
||||||
|
"member_access_level": "basic",
|
||||||
|
"basic_org_domain_request_permissions": "view_only",
|
||||||
|
"email": self.new_member_email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the final submission is successful
|
||||||
|
self.assertEqual(final_response.status_code, 302) # redirects after success
|
||||||
|
|
||||||
|
# Validate Database Changes
|
||||||
|
portfolio_invite = PortfolioInvitation.objects.filter(
|
||||||
|
email=self.new_member_email, portfolio=self.portfolio
|
||||||
|
).first()
|
||||||
|
self.assertIsNotNone(portfolio_invite)
|
||||||
|
self.assertEqual(portfolio_invite.email, self.new_member_email)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_invite_for_previously_invited_member(self):
|
||||||
|
"""Tests the member invitation flow for existing portfolio member."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Simulate a session to ensure continuity
|
||||||
|
session_id = self.client.session.session_key
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
invite_count_before = PortfolioInvitation.objects.count()
|
||||||
|
|
||||||
|
# Simulate submission of member invite for user who has already been invited
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("new-member"),
|
||||||
|
{
|
||||||
|
"member_access_level": "basic",
|
||||||
|
"basic_org_domain_request_permissions": "view_only",
|
||||||
|
"email": self.invited_member_email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302) # Redirects
|
||||||
|
|
||||||
|
# TODO: verify messages
|
||||||
|
|
||||||
|
# Validate Database has not changed
|
||||||
|
invite_count_after = PortfolioInvitation.objects.count()
|
||||||
|
self.assertEqual(invite_count_after, invite_count_before)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_invite_for_existing_member(self):
|
||||||
|
"""Tests the member invitation flow for existing portfolio member."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Simulate a session to ensure continuity
|
||||||
|
session_id = self.client.session.session_key
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
invite_count_before = PortfolioInvitation.objects.count()
|
||||||
|
|
||||||
|
# Simulate submission of member invite for user who has already been invited
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("new-member"),
|
||||||
|
{
|
||||||
|
"member_access_level": "basic",
|
||||||
|
"basic_org_domain_request_permissions": "view_only",
|
||||||
|
"email": self.user.email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302) # Redirects
|
||||||
|
|
||||||
|
# TODO: verify messages
|
||||||
|
|
||||||
|
# Validate Database has not changed
|
||||||
|
invite_count_after = PortfolioInvitation.objects.count()
|
||||||
|
self.assertEqual(invite_count_after, invite_count_before)
|
||||||
|
|
|
@ -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/")
|
||||||
|
|
|
@ -525,6 +525,115 @@ class DomainExport(BaseExport):
|
||||||
# Return the model class that this export handles
|
# Return the model class that this export handles
|
||||||
return DomainInformation
|
return DomainInformation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_computed_fields(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Get a dict of computed fields.
|
||||||
|
"""
|
||||||
|
# NOTE: These computed fields imitate @Property functions in the Domain model and Portfolio model where needed.
|
||||||
|
# This is for performance purposes. Since we are working with dictionary values and not
|
||||||
|
# model objects as we export data, trying to reinstate model objects in order to grab @property
|
||||||
|
# values negatively impacts performance. Therefore, we will follow best practice and use annotations
|
||||||
|
return {
|
||||||
|
"converted_generic_org_type": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("generic_org_type"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_federal_agency": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(
|
||||||
|
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("portfolio__federal_agency__agency"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("federal_agency__agency"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_federal_type": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
# NOTE: this is an @Property funciton in portfolio.
|
||||||
|
When(
|
||||||
|
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("portfolio__federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("federal_type"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_organization_name": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__organization_name")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("organization_name"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_city": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__city")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("city"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_state_territory": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__state_territory")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("state_territory"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_so_email": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=F("senior_official__email"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_senior_official_last_name": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__last_name")),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=F("senior_official__last_name"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_senior_official_first_name": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__first_name")),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=F("senior_official__first_name"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_senior_official_title": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__title")),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=F("senior_official__title"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_so_name": Case(
|
||||||
|
# When portfolio is present, use that senior official instead
|
||||||
|
When(
|
||||||
|
Q(portfolio__isnull=False) & Q(portfolio__senior_official__isnull=False),
|
||||||
|
then=Concat(
|
||||||
|
Coalesce(F("portfolio__senior_official__first_name"), Value("")),
|
||||||
|
Value(" "),
|
||||||
|
Coalesce(F("portfolio__senior_official__last_name"), Value("")),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=Concat(
|
||||||
|
Coalesce(F("senior_official__first_name"), Value("")),
|
||||||
|
Value(" "),
|
||||||
|
Coalesce(F("senior_official__last_name"), Value("")),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_queryset(cls, queryset, **kwargs):
|
def update_queryset(cls, queryset, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -614,10 +723,10 @@ class DomainExport(BaseExport):
|
||||||
if first_ready_on is None:
|
if first_ready_on is None:
|
||||||
first_ready_on = "(blank)"
|
first_ready_on = "(blank)"
|
||||||
|
|
||||||
# organization_type has generic_org_type AND is_election
|
# organization_type has organization_type AND is_election
|
||||||
domain_org_type = model.get("organization_type")
|
domain_org_type = model.get("converted_generic_org_type")
|
||||||
human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
|
human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
|
||||||
domain_federal_type = model.get("federal_type")
|
domain_federal_type = model.get("converted_federal_type")
|
||||||
human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
|
human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
|
||||||
domain_type = human_readable_domain_org_type
|
domain_type = human_readable_domain_org_type
|
||||||
if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
|
if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
|
||||||
|
@ -640,12 +749,12 @@ class DomainExport(BaseExport):
|
||||||
"First ready on": first_ready_on,
|
"First ready on": first_ready_on,
|
||||||
"Expiration date": expiration_date,
|
"Expiration date": expiration_date,
|
||||||
"Domain type": domain_type,
|
"Domain type": domain_type,
|
||||||
"Agency": model.get("federal_agency__agency"),
|
"Agency": model.get("converted_federal_agency"),
|
||||||
"Organization name": model.get("organization_name"),
|
"Organization name": model.get("converted_organization_name"),
|
||||||
"City": model.get("city"),
|
"City": model.get("converted_city"),
|
||||||
"State": model.get("state_territory"),
|
"State": model.get("converted_state_territory"),
|
||||||
"SO": model.get("so_name"),
|
"SO": model.get("converted_so_name"),
|
||||||
"SO email": model.get("senior_official__email"),
|
"SO email": model.get("converted_so_email"),
|
||||||
"Security contact email": security_contact_email,
|
"Security contact email": security_contact_email,
|
||||||
"Created at": model.get("domain__created_at"),
|
"Created at": model.get("domain__created_at"),
|
||||||
"Deleted": model.get("domain__deleted"),
|
"Deleted": model.get("domain__deleted"),
|
||||||
|
@ -654,8 +763,23 @@ class DomainExport(BaseExport):
|
||||||
}
|
}
|
||||||
|
|
||||||
row = [FIELDS.get(column, "") for column in columns]
|
row = [FIELDS.get(column, "") for column in columns]
|
||||||
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
def get_filtered_domain_infos_by_org(domain_infos_to_filter, org_to_filter_by):
|
||||||
|
"""Returns a list of Domain Requests that has been filtered by the given organization value."""
|
||||||
|
|
||||||
|
annotated_queryset = domain_infos_to_filter.annotate(
|
||||||
|
converted_generic_org_type=Case(
|
||||||
|
# Recreate the logic of the converted_generic_org_type property
|
||||||
|
# here in annotations
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||||
|
default=F("generic_org_type"),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return annotated_queryset.filter(converted_generic_org_type=org_to_filter_by)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_sliced_domains(cls, filter_condition):
|
def get_sliced_domains(cls, filter_condition):
|
||||||
"""Get filtered domains counts sliced by org type and election office.
|
"""Get filtered domains counts sliced by org type and election office.
|
||||||
|
@ -663,23 +787,51 @@ class DomainExport(BaseExport):
|
||||||
when a domain has more that one manager.
|
when a domain has more that one manager.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
domains = DomainInformation.objects.all().filter(**filter_condition).distinct()
|
domain_informations = DomainInformation.objects.all().filter(**filter_condition).distinct()
|
||||||
domains_count = domains.count()
|
domains_count = domain_informations.count()
|
||||||
federal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
|
federal = (
|
||||||
interstate = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).count()
|
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.FEDERAL)
|
||||||
state_or_territory = (
|
.distinct()
|
||||||
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
|
.count()
|
||||||
|
)
|
||||||
|
interstate = cls.get_filtered_domain_infos_by_org(
|
||||||
|
domain_informations, DomainRequest.OrganizationChoices.INTERSTATE
|
||||||
|
).count()
|
||||||
|
state_or_territory = (
|
||||||
|
cls.get_filtered_domain_infos_by_org(
|
||||||
|
domain_informations, DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
tribal = (
|
||||||
|
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.TRIBAL)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
county = (
|
||||||
|
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.COUNTY)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
city = (
|
||||||
|
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.CITY)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
)
|
)
|
||||||
tribal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
|
|
||||||
county = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
|
|
||||||
city = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
|
|
||||||
special_district = (
|
special_district = (
|
||||||
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count()
|
cls.get_filtered_domain_infos_by_org(
|
||||||
|
domain_informations, DomainRequest.OrganizationChoices.SPECIAL_DISTRICT
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
)
|
)
|
||||||
school_district = (
|
school_district = (
|
||||||
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count()
|
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.SCHOOL_DISTRICT)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
)
|
)
|
||||||
election_board = domains.filter(is_election_board=True).distinct().count()
|
election_board = domain_informations.filter(is_election_board=True).distinct().count()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
domains_count,
|
domains_count,
|
||||||
|
@ -706,6 +858,7 @@ class DomainDataType(DomainExport):
|
||||||
"""
|
"""
|
||||||
Overrides the columns for CSV export specific to DomainExport.
|
Overrides the columns for CSV export specific to DomainExport.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"Domain name",
|
"Domain name",
|
||||||
"Status",
|
"Status",
|
||||||
|
@ -723,6 +876,13 @@ class DomainDataType(DomainExport):
|
||||||
"Invited domain managers",
|
"Invited domain managers",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_annotations_for_sort(cls):
|
||||||
|
"""
|
||||||
|
Get a dict of annotations to make available for sorting.
|
||||||
|
"""
|
||||||
|
return cls.get_computed_fields()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_sort_fields(cls):
|
def get_sort_fields(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -730,9 +890,9 @@ class DomainDataType(DomainExport):
|
||||||
"""
|
"""
|
||||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||||
return [
|
return [
|
||||||
"organization_type",
|
"converted_generic_org_type",
|
||||||
Coalesce("federal_type", Value("ZZZZZ")),
|
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
||||||
"federal_agency",
|
"converted_federal_agency",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -773,20 +933,6 @@ class DomainDataType(DomainExport):
|
||||||
"""
|
"""
|
||||||
return ["domain__permissions"]
|
return ["domain__permissions"]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_computed_fields(cls, delimiter=", ", **kwargs):
|
|
||||||
"""
|
|
||||||
Get a dict of computed fields.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"so_name": Concat(
|
|
||||||
Coalesce(F("senior_official__first_name"), Value("")),
|
|
||||||
Value(" "),
|
|
||||||
Coalesce(F("senior_official__last_name"), Value("")),
|
|
||||||
output_field=CharField(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_related_table_fields(cls):
|
def get_related_table_fields(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -892,7 +1038,7 @@ class DomainRequestsDataType:
|
||||||
cls.safe_get(getattr(request, "region_field", None)),
|
cls.safe_get(getattr(request, "region_field", None)),
|
||||||
request.status,
|
request.status,
|
||||||
cls.safe_get(getattr(request, "election_office", None)),
|
cls.safe_get(getattr(request, "election_office", None)),
|
||||||
request.federal_type,
|
request.converted_federal_type,
|
||||||
cls.safe_get(getattr(request, "domain_type", None)),
|
cls.safe_get(getattr(request, "domain_type", None)),
|
||||||
cls.safe_get(getattr(request, "additional_details", None)),
|
cls.safe_get(getattr(request, "additional_details", None)),
|
||||||
cls.safe_get(getattr(request, "creator_approved_domains_count", None)),
|
cls.safe_get(getattr(request, "creator_approved_domains_count", None)),
|
||||||
|
@ -943,6 +1089,13 @@ class DomainDataFull(DomainExport):
|
||||||
"Security contact email",
|
"Security contact email",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_annotations_for_sort(cls, delimiter=", "):
|
||||||
|
"""
|
||||||
|
Get a dict of annotations to make available for sorting.
|
||||||
|
"""
|
||||||
|
return cls.get_computed_fields()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_sort_fields(cls):
|
def get_sort_fields(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -950,9 +1103,9 @@ class DomainDataFull(DomainExport):
|
||||||
"""
|
"""
|
||||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||||
return [
|
return [
|
||||||
"organization_type",
|
"converted_generic_org_type",
|
||||||
Coalesce("federal_type", Value("ZZZZZ")),
|
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
||||||
"federal_agency",
|
"converted_federal_agency",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -990,20 +1143,6 @@ class DomainDataFull(DomainExport):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_computed_fields(cls, delimiter=", ", **kwargs):
|
|
||||||
"""
|
|
||||||
Get a dict of computed fields.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"so_name": Concat(
|
|
||||||
Coalesce(F("senior_official__first_name"), Value("")),
|
|
||||||
Value(" "),
|
|
||||||
Coalesce(F("senior_official__last_name"), Value("")),
|
|
||||||
output_field=CharField(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_related_table_fields(cls):
|
def get_related_table_fields(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1037,6 +1176,13 @@ class DomainDataFederal(DomainExport):
|
||||||
"Security contact email",
|
"Security contact email",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_annotations_for_sort(cls, delimiter=", "):
|
||||||
|
"""
|
||||||
|
Get a dict of annotations to make available for sorting.
|
||||||
|
"""
|
||||||
|
return cls.get_computed_fields()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_sort_fields(cls):
|
def get_sort_fields(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1044,9 +1190,9 @@ class DomainDataFederal(DomainExport):
|
||||||
"""
|
"""
|
||||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||||
return [
|
return [
|
||||||
"organization_type",
|
"converted_generic_org_type",
|
||||||
Coalesce("federal_type", Value("ZZZZZ")),
|
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
||||||
"federal_agency",
|
"converted_federal_agency",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1085,20 +1231,6 @@ class DomainDataFederal(DomainExport):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_computed_fields(cls, delimiter=", ", **kwargs):
|
|
||||||
"""
|
|
||||||
Get a dict of computed fields.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"so_name": Concat(
|
|
||||||
Coalesce(F("senior_official__first_name"), Value("")),
|
|
||||||
Value(" "),
|
|
||||||
Coalesce(F("senior_official__last_name"), Value("")),
|
|
||||||
output_field=CharField(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_related_table_fields(cls):
|
def get_related_table_fields(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1476,24 +1608,180 @@ class DomainRequestExport(BaseExport):
|
||||||
# Return the model class that this export handles
|
# Return the model class that this export handles
|
||||||
return DomainRequest
|
return DomainRequest
|
||||||
|
|
||||||
|
def get_filtered_domain_requests_by_org(domain_requests_to_filter, org_to_filter_by):
|
||||||
|
"""Returns a list of Domain Requests that has been filtered by the given organization value"""
|
||||||
|
annotated_queryset = domain_requests_to_filter.annotate(
|
||||||
|
converted_generic_org_type=Case(
|
||||||
|
# Recreate the logic of the converted_generic_org_type property
|
||||||
|
# here in annotations
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||||
|
default=F("generic_org_type"),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return annotated_queryset.filter(converted_generic_org_type=org_to_filter_by)
|
||||||
|
|
||||||
|
# return domain_requests_to_filter.filter(
|
||||||
|
# # Filter based on the generic org value returned by converted_generic_org_type
|
||||||
|
# id__in=[
|
||||||
|
# domainRequest.id
|
||||||
|
# for domainRequest in domain_requests_to_filter
|
||||||
|
# if domainRequest.converted_generic_org_type
|
||||||
|
# and domainRequest.converted_generic_org_type == org_to_filter_by
|
||||||
|
# ]
|
||||||
|
# )
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_computed_fields(cls, delimiter=", ", **kwargs):
|
||||||
|
"""
|
||||||
|
Get a dict of computed fields.
|
||||||
|
"""
|
||||||
|
# NOTE: These computed fields imitate @Property functions in the Domain model and Portfolio model where needed.
|
||||||
|
# This is for performance purposes. Since we are working with dictionary values and not
|
||||||
|
# model objects as we export data, trying to reinstate model objects in order to grab @property
|
||||||
|
# values negatively impacts performance. Therefore, we will follow best practice and use annotations
|
||||||
|
return {
|
||||||
|
"converted_generic_org_type": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("generic_org_type"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_federal_agency": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(
|
||||||
|
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("portfolio__federal_agency__agency"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("federal_agency__agency"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_federal_type": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
# NOTE: this is an @Property funciton in portfolio.
|
||||||
|
When(
|
||||||
|
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("portfolio__federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("federal_type"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_organization_name": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__organization_name")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("organization_name"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_city": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__city")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("city"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_state_territory": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__state_territory")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("state_territory"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_so_email": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=F("senior_official__email"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_senior_official_last_name": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__last_name")),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=F("senior_official__last_name"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_senior_official_first_name": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__first_name")),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=F("senior_official__first_name"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_senior_official_title": Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__senior_official__title")),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=F("senior_official__title"),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
"converted_so_name": Case(
|
||||||
|
# When portfolio is present, use that senior official instead
|
||||||
|
When(
|
||||||
|
Q(portfolio__isnull=False) & Q(portfolio__senior_official__isnull=False),
|
||||||
|
then=Concat(
|
||||||
|
Coalesce(F("portfolio__senior_official__first_name"), Value("")),
|
||||||
|
Value(" "),
|
||||||
|
Coalesce(F("portfolio__senior_official__last_name"), Value("")),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned senior official
|
||||||
|
default=Concat(
|
||||||
|
Coalesce(F("senior_official__first_name"), Value("")),
|
||||||
|
Value(" "),
|
||||||
|
Coalesce(F("senior_official__last_name"), Value("")),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
output_field=CharField(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_sliced_requests(cls, filter_condition):
|
def get_sliced_requests(cls, filter_condition):
|
||||||
"""Get filtered requests counts sliced by org type and election office."""
|
"""Get filtered requests counts sliced by org type and election office."""
|
||||||
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
|
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
|
||||||
requests_count = requests.count()
|
requests_count = requests.count()
|
||||||
federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
|
federal = (
|
||||||
interstate = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).distinct().count()
|
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.FEDERAL)
|
||||||
state_or_territory = (
|
.distinct()
|
||||||
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
|
.count()
|
||||||
|
)
|
||||||
|
interstate = (
|
||||||
|
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.INTERSTATE)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
state_or_territory = (
|
||||||
|
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.STATE_OR_TERRITORY)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
tribal = (
|
||||||
|
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.TRIBAL)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
county = (
|
||||||
|
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.COUNTY)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
city = (
|
||||||
|
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.CITY).distinct().count()
|
||||||
)
|
)
|
||||||
tribal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
|
|
||||||
county = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
|
|
||||||
city = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
|
|
||||||
special_district = (
|
special_district = (
|
||||||
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count()
|
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.SPECIAL_DISTRICT)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
)
|
)
|
||||||
school_district = (
|
school_district = (
|
||||||
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count()
|
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.SCHOOL_DISTRICT)
|
||||||
|
.distinct()
|
||||||
|
.count()
|
||||||
)
|
)
|
||||||
election_board = requests.filter(is_election_board=True).distinct().count()
|
election_board = requests.filter(is_election_board=True).distinct().count()
|
||||||
|
|
||||||
|
@ -1517,11 +1805,11 @@ class DomainRequestExport(BaseExport):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Handle the federal_type field. Defaults to the wrong format.
|
# Handle the federal_type field. Defaults to the wrong format.
|
||||||
federal_type = model.get("federal_type")
|
federal_type = model.get("converted_federal_type")
|
||||||
human_readable_federal_type = BranchChoices.get_branch_label(federal_type) if federal_type else None
|
human_readable_federal_type = BranchChoices.get_branch_label(federal_type) if federal_type else None
|
||||||
|
|
||||||
# Handle the org_type field
|
# Handle the org_type field
|
||||||
org_type = model.get("generic_org_type") or model.get("organization_type")
|
org_type = model.get("converted_generic_org_type")
|
||||||
human_readable_org_type = DomainRequest.OrganizationChoices.get_org_label(org_type) if org_type else None
|
human_readable_org_type = DomainRequest.OrganizationChoices.get_org_label(org_type) if org_type else None
|
||||||
|
|
||||||
# Handle the status field. Defaults to the wrong format.
|
# Handle the status field. Defaults to the wrong format.
|
||||||
|
@ -1569,19 +1857,19 @@ class DomainRequestExport(BaseExport):
|
||||||
"Other contacts": model.get("all_other_contacts"),
|
"Other contacts": model.get("all_other_contacts"),
|
||||||
"Current websites": model.get("all_current_websites"),
|
"Current websites": model.get("all_current_websites"),
|
||||||
# Untouched FK fields - passed into the request dict.
|
# Untouched FK fields - passed into the request dict.
|
||||||
"Federal agency": model.get("federal_agency__agency"),
|
"Federal agency": model.get("converted_federal_agency"),
|
||||||
"SO first name": model.get("senior_official__first_name"),
|
"SO first name": model.get("converted_senior_official_first_name"),
|
||||||
"SO last name": model.get("senior_official__last_name"),
|
"SO last name": model.get("converted_senior_official_last_name"),
|
||||||
"SO email": model.get("senior_official__email"),
|
"SO email": model.get("converted_so_email"),
|
||||||
"SO title/role": model.get("senior_official__title"),
|
"SO title/role": model.get("converted_senior_official_title"),
|
||||||
"Creator first name": model.get("creator__first_name"),
|
"Creator first name": model.get("creator__first_name"),
|
||||||
"Creator last name": model.get("creator__last_name"),
|
"Creator last name": model.get("creator__last_name"),
|
||||||
"Creator email": model.get("creator__email"),
|
"Creator email": model.get("creator__email"),
|
||||||
"Investigator": model.get("investigator__email"),
|
"Investigator": model.get("investigator__email"),
|
||||||
# Untouched fields
|
# Untouched fields
|
||||||
"Organization name": model.get("organization_name"),
|
"Organization name": model.get("converted_organization_name"),
|
||||||
"City": model.get("city"),
|
"City": model.get("converted_city"),
|
||||||
"State/territory": model.get("state_territory"),
|
"State/territory": model.get("converted_state_territory"),
|
||||||
"Request purpose": model.get("purpose"),
|
"Request purpose": model.get("purpose"),
|
||||||
"CISA regional representative": model.get("cisa_representative_email"),
|
"CISA regional representative": model.get("cisa_representative_email"),
|
||||||
"Last submitted date": model.get("last_submitted_date"),
|
"Last submitted date": model.get("last_submitted_date"),
|
||||||
|
@ -1724,24 +2012,34 @@ class DomainRequestDataFull(DomainRequestExport):
|
||||||
"""
|
"""
|
||||||
Get a dict of computed fields.
|
Get a dict of computed fields.
|
||||||
"""
|
"""
|
||||||
return {
|
# Get computed fields from the parent class
|
||||||
"creator_approved_domains_count": cls.get_creator_approved_domains_count_query(),
|
computed_fields = super().get_computed_fields()
|
||||||
"creator_active_requests_count": cls.get_creator_active_requests_count_query(),
|
|
||||||
"all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True),
|
# Add additional computed fields
|
||||||
"all_alternative_domains": StringAgg("alternative_domains__website", delimiter=delimiter, distinct=True),
|
computed_fields.update(
|
||||||
# Coerce the other contacts object to "{first_name} {last_name} {email}"
|
{
|
||||||
"all_other_contacts": StringAgg(
|
"creator_approved_domains_count": cls.get_creator_approved_domains_count_query(),
|
||||||
Concat(
|
"creator_active_requests_count": cls.get_creator_active_requests_count_query(),
|
||||||
"other_contacts__first_name",
|
"all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True),
|
||||||
Value(" "),
|
"all_alternative_domains": StringAgg(
|
||||||
"other_contacts__last_name",
|
"alternative_domains__website", delimiter=delimiter, distinct=True
|
||||||
Value(" "),
|
|
||||||
"other_contacts__email",
|
|
||||||
),
|
),
|
||||||
delimiter=delimiter,
|
# Coerce the other contacts object to "{first_name} {last_name} {email}"
|
||||||
distinct=True,
|
"all_other_contacts": StringAgg(
|
||||||
),
|
Concat(
|
||||||
}
|
"other_contacts__first_name",
|
||||||
|
Value(" "),
|
||||||
|
"other_contacts__last_name",
|
||||||
|
Value(" "),
|
||||||
|
"other_contacts__email",
|
||||||
|
),
|
||||||
|
delimiter=delimiter,
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return computed_fields
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_related_table_fields(cls):
|
def get_related_table_fields(cls):
|
||||||
|
|
|
@ -13,6 +13,7 @@ from .domain import (
|
||||||
DomainAddUserView,
|
DomainAddUserView,
|
||||||
DomainInvitationCancelView,
|
DomainInvitationCancelView,
|
||||||
DomainDeleteUserView,
|
DomainDeleteUserView,
|
||||||
|
PrototypeDomainDNSRecordView,
|
||||||
)
|
)
|
||||||
from .user_profile import UserProfileView, FinishProfileSetupView
|
from .user_profile import UserProfileView, FinishProfileSetupView
|
||||||
from .health import *
|
from .health import *
|
||||||
|
|
|
@ -7,7 +7,7 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionCancelView).
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import logging
|
import logging
|
||||||
|
import requests
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
@ -64,6 +64,7 @@ from epplibwrapper import (
|
||||||
|
|
||||||
from ..utility.email import send_templated_email, EmailSendingError
|
from ..utility.email import send_templated_email, EmailSendingError
|
||||||
from .utility import DomainPermissionView, DomainInvitationPermissionCancelView
|
from .utility import DomainPermissionView, DomainInvitationPermissionCancelView
|
||||||
|
from django import forms
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -454,6 +455,216 @@ class DomainDNSView(DomainBaseView):
|
||||||
"""DNS Information View."""
|
"""DNS Information View."""
|
||||||
|
|
||||||
template_name = "domain_dns.html"
|
template_name = "domain_dns.html"
|
||||||
|
valid_domains = ["igorville.gov", "domainops.gov", "dns.gov"]
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""Adds custom context."""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["dns_prototype_flag"] = flag_is_active_for_user(self.request.user, "dns_prototype_flag")
|
||||||
|
context["is_valid_domain"] = self.object.name in self.valid_domains
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class PrototypeDomainDNSRecordForm(forms.Form):
|
||||||
|
"""Form for adding DNS records in prototype."""
|
||||||
|
|
||||||
|
name = forms.CharField(label="DNS record name (A record)", required=True, help_text="DNS record name")
|
||||||
|
|
||||||
|
content = forms.GenericIPAddressField(
|
||||||
|
label="IPv4 Address",
|
||||||
|
required=True,
|
||||||
|
protocol="IPv4",
|
||||||
|
)
|
||||||
|
|
||||||
|
ttl = forms.ChoiceField(
|
||||||
|
label="TTL",
|
||||||
|
choices=[
|
||||||
|
(1, "Automatic"),
|
||||||
|
(60, "1 minute"),
|
||||||
|
(300, "5 minutes"),
|
||||||
|
(1800, "30 minutes"),
|
||||||
|
(3600, "1 hour"),
|
||||||
|
(7200, "2 hours"),
|
||||||
|
(18000, "5 hours"),
|
||||||
|
(43200, "12 hours"),
|
||||||
|
(86400, "1 day"),
|
||||||
|
],
|
||||||
|
initial=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PrototypeDomainDNSRecordView(DomainFormBaseView):
|
||||||
|
template_name = "prototype_domain_dns.html"
|
||||||
|
form_class = PrototypeDomainDNSRecordForm
|
||||||
|
valid_domains = ["igorville.gov", "domainops.gov", "dns.gov"]
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
has_permission = super().has_permission()
|
||||||
|
if not has_permission:
|
||||||
|
return False
|
||||||
|
|
||||||
|
flag_enabled = flag_is_active_for_user(self.request.user, "dns_prototype_flag")
|
||||||
|
if not flag_enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.object = self.get_object()
|
||||||
|
if self.object.name not in self.valid_domains:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse("prototype-domain-dns", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
def find_by_name(self, items, name):
|
||||||
|
"""Find an item by name in a list of dictionaries."""
|
||||||
|
return next((item.get("id") for item in items if item.get("name") == name), None)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""Handle form submission."""
|
||||||
|
self.object = self.get_object()
|
||||||
|
form = self.get_form()
|
||||||
|
errors = []
|
||||||
|
if form.is_valid():
|
||||||
|
try:
|
||||||
|
if settings.IS_PRODUCTION and self.object.name != "igorville.gov":
|
||||||
|
raise Exception(f"create dns record was called for domain {self.name}")
|
||||||
|
|
||||||
|
if not settings.IS_PRODUCTION and self.object.name not in self.valid_domains:
|
||||||
|
raise Exception(
|
||||||
|
f"Can only create DNS records for: {self.valid_domains}."
|
||||||
|
" Create one in a test environment if it doesn't already exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
base_url = "https://api.cloudflare.com/client/v4"
|
||||||
|
headers = {
|
||||||
|
"X-Auth-Email": settings.SECRET_REGISTRY_SERVICE_EMAIL,
|
||||||
|
"X-Auth-Key": settings.SECRET_REGISTRY_TENANT_KEY,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
params = {"tenant_name": settings.SECRET_REGISTRY_TENANT_NAME}
|
||||||
|
|
||||||
|
# 1. Get tenant details
|
||||||
|
tenant_response = requests.get(f"{base_url}/user/tenants", headers=headers, params=params, timeout=5)
|
||||||
|
tenant_response_json = tenant_response.json()
|
||||||
|
logger.info(f"Found tenant: {tenant_response_json}")
|
||||||
|
tenant_id = tenant_response_json["result"][0]["tenant_tag"]
|
||||||
|
errors = tenant_response_json.get("errors", [])
|
||||||
|
tenant_response.raise_for_status()
|
||||||
|
|
||||||
|
# 2. Create or get a account under tenant
|
||||||
|
|
||||||
|
# Check to see if the account already exists. Filters accounts by tenant_id / account_name.
|
||||||
|
account_name = f"account-{self.object.name}"
|
||||||
|
params = {"tenant_id": tenant_id, "name": account_name}
|
||||||
|
|
||||||
|
account_response = requests.get(f"{base_url}/accounts", headers=headers, params=params, timeout=5)
|
||||||
|
account_response_json = account_response.json()
|
||||||
|
logger.debug(f"account get: {account_response_json}")
|
||||||
|
errors = account_response_json.get("errors", [])
|
||||||
|
account_response.raise_for_status()
|
||||||
|
|
||||||
|
# See if we already made an account.
|
||||||
|
# This maybe doesn't need to be a for loop (1 record or 0) but alas, here we are
|
||||||
|
accounts = account_response_json.get("result", [])
|
||||||
|
account_id = self.find_by_name(accounts, account_name)
|
||||||
|
|
||||||
|
# If we didn't, create one
|
||||||
|
if not account_id:
|
||||||
|
account_response = requests.post(
|
||||||
|
f"{base_url}/accounts",
|
||||||
|
headers=headers,
|
||||||
|
json={"name": account_name, "type": "enterprise", "unit": {"id": tenant_id}},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
account_response_json = account_response.json()
|
||||||
|
logger.info(f"Created account: {account_response_json}")
|
||||||
|
account_id = account_response_json["result"]["id"]
|
||||||
|
errors = account_response_json.get("errors", [])
|
||||||
|
account_response.raise_for_status()
|
||||||
|
|
||||||
|
# 3. Create or get a zone under account
|
||||||
|
|
||||||
|
# Try to find an existing zone first by searching on the current id
|
||||||
|
zone_name = self.object.name
|
||||||
|
params = {"account.id": account_id, "name": zone_name}
|
||||||
|
zone_response = requests.get(f"{base_url}/zones", headers=headers, params=params, timeout=5)
|
||||||
|
zone_response_json = zone_response.json()
|
||||||
|
logger.debug(f"get zone: {zone_response_json}")
|
||||||
|
errors = zone_response_json.get("errors", [])
|
||||||
|
zone_response.raise_for_status()
|
||||||
|
|
||||||
|
# Get the zone id
|
||||||
|
zones = zone_response_json.get("result", [])
|
||||||
|
zone_id = self.find_by_name(zones, zone_name)
|
||||||
|
|
||||||
|
# Create one if it doesn't presently exist
|
||||||
|
if not zone_id:
|
||||||
|
zone_response = requests.post(
|
||||||
|
f"{base_url}/zones",
|
||||||
|
headers=headers,
|
||||||
|
json={"name": zone_name, "account": {"id": account_id}, "type": "full"},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
zone_response_json = zone_response.json()
|
||||||
|
logger.info(f"Created zone: {zone_response_json}")
|
||||||
|
zone_id = zone_response_json.get("result", {}).get("id")
|
||||||
|
errors = zone_response_json.get("errors", [])
|
||||||
|
zone_response.raise_for_status()
|
||||||
|
|
||||||
|
# 4. Add or get a zone subscription
|
||||||
|
|
||||||
|
# See if one already exists
|
||||||
|
subscription_response = requests.get(
|
||||||
|
f"{base_url}/zones/{zone_id}/subscription", headers=headers, timeout=5
|
||||||
|
)
|
||||||
|
subscription_response_json = subscription_response.json()
|
||||||
|
logger.debug(f"get subscription: {subscription_response_json}")
|
||||||
|
|
||||||
|
# Create a subscription if one doesn't exist already.
|
||||||
|
# If it doesn't, we get this error message (code 1207):
|
||||||
|
# Add a core subscription first and try again. The zone does not have an active core subscription.
|
||||||
|
# Note that status code and error code are different here.
|
||||||
|
if subscription_response.status_code == 404:
|
||||||
|
subscription_response = requests.post(
|
||||||
|
f"{base_url}/zones/{zone_id}/subscription",
|
||||||
|
headers=headers,
|
||||||
|
json={"rate_plan": {"id": "PARTNERS_ENT"}, "frequency": "annual"},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
subscription_response.raise_for_status()
|
||||||
|
subscription_response_json = subscription_response.json()
|
||||||
|
logger.info(f"Created subscription: {subscription_response_json}")
|
||||||
|
else:
|
||||||
|
subscription_response.raise_for_status()
|
||||||
|
|
||||||
|
# # 5. Create DNS record
|
||||||
|
# # Format the DNS record according to Cloudflare's API requirements
|
||||||
|
dns_response = requests.post(
|
||||||
|
f"{base_url}/zones/{zone_id}/dns_records",
|
||||||
|
headers=headers,
|
||||||
|
json={
|
||||||
|
"type": "A",
|
||||||
|
"name": form.cleaned_data["name"],
|
||||||
|
"content": form.cleaned_data["content"],
|
||||||
|
"ttl": int(form.cleaned_data["ttl"]),
|
||||||
|
"comment": "Test record (will need clean up)",
|
||||||
|
},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
dns_response_json = dns_response.json()
|
||||||
|
logger.info(f"Created DNS record: {dns_response_json}")
|
||||||
|
errors = dns_response_json.get("errors", [])
|
||||||
|
dns_response.raise_for_status()
|
||||||
|
dns_name = dns_response_json["result"]["name"]
|
||||||
|
messages.success(request, f"DNS A record '{dns_name}' created successfully.")
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"Error creating DNS A record for {self.object.name}: {err}")
|
||||||
|
messages.error(request, f"An error occurred: {err}")
|
||||||
|
finally:
|
||||||
|
if errors:
|
||||||
|
messages.error(request, f"Request errors: {errors}")
|
||||||
|
return super().post(request)
|
||||||
|
|
||||||
|
|
||||||
class DomainNameserversView(DomainFormBaseView):
|
class DomainNameserversView(DomainFormBaseView):
|
||||||
|
|
|
@ -53,7 +53,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
URL_NAMESPACE = "domain-request"
|
URL_NAMESPACE = "domain-request"
|
||||||
# name for accessing /domain-request/<id>/edit
|
# name for accessing /domain-request/<id>/edit
|
||||||
EDIT_URL_NAME = "edit-domain-request"
|
EDIT_URL_NAME = "edit-domain-request"
|
||||||
NEW_URL_NAME = "/request/start/"
|
NEW_URL_NAME = "start"
|
||||||
|
FINISHED_URL_NAME = "finished"
|
||||||
|
|
||||||
# region: Titles
|
# region: Titles
|
||||||
# We need to pass our human-readable step titles as context to the templates.
|
# We need to pass our human-readable step titles as context to the templates.
|
||||||
|
@ -313,7 +314,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
# send users "to the domain request wizard" without needing to know which view
|
# send users "to the domain request wizard" without needing to know which view
|
||||||
# is first in the list of steps.
|
# is first in the list of steps.
|
||||||
if self.__class__ == DomainRequestWizard:
|
if self.__class__ == DomainRequestWizard:
|
||||||
if request.path_info == self.NEW_URL_NAME:
|
if current_url == self.NEW_URL_NAME:
|
||||||
# Clear context so the prop getter won't create a request here.
|
# Clear context so the prop getter won't create a request here.
|
||||||
# Creating a request will be handled in the post method for the
|
# Creating a request will be handled in the post method for the
|
||||||
# intro page.
|
# intro page.
|
||||||
|
@ -614,7 +615,7 @@ class RequestingEntity(DomainRequestWizard):
|
||||||
class PortfolioAdditionalDetails(DomainRequestWizard):
|
class PortfolioAdditionalDetails(DomainRequestWizard):
|
||||||
template_name = "portfolio_domain_request_additional_details.html"
|
template_name = "portfolio_domain_request_additional_details.html"
|
||||||
|
|
||||||
forms = [forms.AnythingElseForm]
|
forms = [forms.PortfolioAnythingElseForm]
|
||||||
|
|
||||||
|
|
||||||
# Non-portfolio pages
|
# Non-portfolio pages
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from django.http import Http404, JsonResponse
|
from django.http import Http404, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
@ -11,6 +12,7 @@ from registrar.models import Portfolio, User
|
||||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
|
from registrar.utility.email import EmailSendingError
|
||||||
from registrar.views.utility.mixins import PortfolioMemberPermission
|
from registrar.views.utility.mixins import PortfolioMemberPermission
|
||||||
from registrar.views.utility.permission_views import (
|
from registrar.views.utility.permission_views import (
|
||||||
PortfolioDomainRequestsPermissionView,
|
PortfolioDomainRequestsPermissionView,
|
||||||
|
@ -25,6 +27,7 @@ from registrar.views.utility.permission_views import (
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -492,138 +495,134 @@ class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
||||||
"""Handle POST requests to process form submission."""
|
"""Handle POST requests to process form submission."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
return self.form_valid(form)
|
return self.form_valid(form)
|
||||||
else:
|
else:
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def is_ajax(self):
|
||||||
|
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
"""Handle the case when the form is invalid."""
|
if self.is_ajax():
|
||||||
return self.render_to_response(self.get_context_data(form=form))
|
return JsonResponse({"is_valid": False}) # Return a JSON response
|
||||||
|
else:
|
||||||
|
return super().form_invalid(form) # Handle non-AJAX requests normally
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
|
||||||
|
if self.is_ajax():
|
||||||
|
return JsonResponse({"is_valid": True}) # Return a JSON response
|
||||||
|
else:
|
||||||
|
return self.submit_new_member(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
"""Redirect to members table."""
|
"""Redirect to members table."""
|
||||||
return reverse("members")
|
return reverse("members")
|
||||||
|
|
||||||
##########################################
|
def _send_portfolio_invitation_email(self, email: str, requestor: User, add_success=True):
|
||||||
# TODO: future ticket #2854
|
"""Performs the sending of the member invitation email
|
||||||
# (save/invite new member)
|
email: string- email to send to
|
||||||
##########################################
|
add_success: bool- default True indicates:
|
||||||
|
adding a success message to the view if the email sending succeeds
|
||||||
|
|
||||||
# def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True):
|
raises EmailSendingError
|
||||||
# """Performs the sending of the member invitation email
|
"""
|
||||||
# email: string- email to send to
|
|
||||||
# add_success: bool- default True indicates:
|
|
||||||
# adding a success message to the view if the email sending succeeds
|
|
||||||
|
|
||||||
# raises EmailSendingError
|
# Set a default email address to send to for staff
|
||||||
# """
|
requestor_email = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
# # Set a default email address to send to for staff
|
# Check if the email requestor has a valid email address
|
||||||
# requestor_email = settings.DEFAULT_FROM_EMAIL
|
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||||
|
requestor_email = requestor.email
|
||||||
|
elif not requestor.is_staff:
|
||||||
|
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
||||||
|
logger.error(
|
||||||
|
f"Can't send email to '{email}' on domain '{self.object}'."
|
||||||
|
f"No email exists for the requestor '{requestor.username}'.",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
# # Check if the email requestor has a valid email address
|
# Check to see if an invite has already been sent
|
||||||
# if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
try:
|
||||||
# requestor_email = requestor.email
|
invite = PortfolioInvitation.objects.get(email=email, portfolio=self.object)
|
||||||
# elif not requestor.is_staff:
|
if invite: # We have an existin invite
|
||||||
# messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
# check if the invite has already been accepted
|
||||||
# logger.error(
|
if invite.status == PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED:
|
||||||
# f"Can't send email to '{email}' on domain '{self.object}'."
|
add_success = False
|
||||||
# f"No email exists for the requestor '{requestor.username}'.",
|
messages.warning(
|
||||||
# exc_info=True,
|
self.request,
|
||||||
# )
|
f"{email} is already a manager for this portfolio.",
|
||||||
# return None
|
)
|
||||||
|
else:
|
||||||
|
add_success = False
|
||||||
|
# it has been sent but not accepted
|
||||||
|
messages.warning(self.request, f"{email} has already been invited to this portfolio")
|
||||||
|
return
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"_send_portfolio_invitation_email() => An error occured: {err}")
|
||||||
|
|
||||||
# # Check to see if an invite has already been sent
|
try:
|
||||||
# try:
|
logger.debug("requestor email: " + requestor_email)
|
||||||
# invite = MemberInvitation.objects.get(email=email, domain=self.object)
|
|
||||||
# # check if the invite has already been accepted
|
|
||||||
# if invite.status == MemberInvitation.MemberInvitationStatus.RETRIEVED:
|
|
||||||
# add_success = False
|
|
||||||
# messages.warning(
|
|
||||||
# self.request,
|
|
||||||
# f"{email} is already a manager for this domain.",
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# add_success = False
|
|
||||||
# # else if it has been sent but not accepted
|
|
||||||
# messages.warning(self.request, f"{email} has already been invited to this domain")
|
|
||||||
# except Exception:
|
|
||||||
# logger.error("An error occured")
|
|
||||||
|
|
||||||
# try:
|
# send_templated_email(
|
||||||
# send_templated_email(
|
# "emails/portfolio_invitation.txt",
|
||||||
# "emails/member_invitation.txt",
|
# "emails/portfolio_invitation_subject.txt",
|
||||||
# "emails/member_invitation_subject.txt",
|
# to_address=email,
|
||||||
# to_address=email,
|
# context={
|
||||||
# context={
|
# "portfolio": self.object,
|
||||||
# "portfolio": self.object,
|
# "requestor_email": requestor_email,
|
||||||
# "requestor_email": requestor_email,
|
# },
|
||||||
# },
|
# )
|
||||||
# )
|
except EmailSendingError as exc:
|
||||||
# except EmailSendingError as exc:
|
logger.warn(
|
||||||
# logger.warn(
|
"Could not sent email invitation to %s for domain %s",
|
||||||
# "Could not sent email invitation to %s for domain %s",
|
email,
|
||||||
# email,
|
self.object,
|
||||||
# self.object,
|
exc_info=True,
|
||||||
# exc_info=True,
|
)
|
||||||
# )
|
raise EmailSendingError("Could not send email invitation.") from exc
|
||||||
# raise EmailSendingError("Could not send email invitation.") from exc
|
else:
|
||||||
# else:
|
if add_success:
|
||||||
# if add_success:
|
messages.success(self.request, f"{email} has been invited.")
|
||||||
# messages.success(self.request, f"{email} has been invited to this domain.")
|
|
||||||
|
|
||||||
# def _make_invitation(self, email_address: str, requestor: User):
|
def _make_invitation(self, email_address: str, requestor: User, add_success=True):
|
||||||
# """Make a Member invitation for this email and redirect with a message."""
|
"""Make a Member invitation for this email and redirect with a message."""
|
||||||
# try:
|
try:
|
||||||
# self._send_member_invitation_email(email=email_address, requestor=requestor)
|
self._send_portfolio_invitation_email(email=email_address, requestor=requestor, add_success=add_success)
|
||||||
# except EmailSendingError:
|
except EmailSendingError:
|
||||||
# messages.warning(self.request, "Could not send email invitation.")
|
logger.warn(
|
||||||
# else:
|
"Could not send email invitation (EmailSendingError)",
|
||||||
# # (NOTE: only create a MemberInvitation if the e-mail sends correctly)
|
self.object,
|
||||||
# MemberInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
exc_info=True,
|
||||||
# return redirect(self.get_success_url())
|
)
|
||||||
|
messages.warning(self.request, "Could not send email invitation.")
|
||||||
|
except Exception:
|
||||||
|
logger.warn(
|
||||||
|
"Could not send email invitation (Other Exception)",
|
||||||
|
self.object,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
messages.warning(self.request, "Could not send email invitation.")
|
||||||
|
else:
|
||||||
|
# (NOTE: only create a MemberInvitation if the e-mail sends correctly)
|
||||||
|
PortfolioInvitation.objects.get_or_create(email=email_address, portfolio=self.object)
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
# def form_valid(self, form):
|
def submit_new_member(self, form):
|
||||||
|
"""Add the specified user as a member
|
||||||
|
for this portfolio.
|
||||||
|
Throws EmailSendingError."""
|
||||||
|
requested_email = form.cleaned_data["email"]
|
||||||
|
requestor = self.request.user
|
||||||
|
|
||||||
# """Add the specified user as a member
|
requested_user = User.objects.filter(email=requested_email).first()
|
||||||
# for this portfolio.
|
permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=self.object).exists()
|
||||||
# Throws EmailSendingError."""
|
if not requested_user or not permission_exists:
|
||||||
# requested_email = form.cleaned_data["email"]
|
return self._make_invitation(requested_email, requestor)
|
||||||
# requestor = self.request.user
|
else:
|
||||||
# # look up a user with that email
|
if permission_exists:
|
||||||
# try:
|
messages.warning(self.request, "User is already a member of this portfolio.")
|
||||||
# requested_user = User.objects.get(email=requested_email)
|
return redirect(self.get_success_url())
|
||||||
# except User.DoesNotExist:
|
|
||||||
# # no matching user, go make an invitation
|
|
||||||
# return self._make_invitation(requested_email, requestor)
|
|
||||||
# else:
|
|
||||||
# # if user already exists then just send an email
|
|
||||||
# try:
|
|
||||||
# self._send_member_invitation_email(requested_email, requestor, add_success=False)
|
|
||||||
# except EmailSendingError:
|
|
||||||
# logger.warn(
|
|
||||||
# "Could not send email invitation (EmailSendingError)",
|
|
||||||
# self.object,
|
|
||||||
# exc_info=True,
|
|
||||||
# )
|
|
||||||
# messages.warning(self.request, "Could not send email invitation.")
|
|
||||||
# except Exception:
|
|
||||||
# logger.warn(
|
|
||||||
# "Could not send email invitation (Other Exception)",
|
|
||||||
# self.object,
|
|
||||||
# exc_info=True,
|
|
||||||
# )
|
|
||||||
# messages.warning(self.request, "Could not send email invitation.")
|
|
||||||
|
|
||||||
# try:
|
|
||||||
# UserPortfolioPermission.objects.create(
|
|
||||||
# user=requested_user,
|
|
||||||
# portfolio=self.object,
|
|
||||||
# role=UserDomainRole.Roles.MANAGER,
|
|
||||||
# )
|
|
||||||
# except IntegrityError:
|
|
||||||
# messages.warning(self.request, f"{requested_email} is already a member of this portfolio")
|
|
||||||
# else:
|
|
||||||
# messages.success(self.request, f"Added user {requested_email}.")
|
|
||||||
# return redirect(self.get_success_url())
|
|
||||||
|
|
|
@ -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