mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 17:47:02 +02:00
Merge branch 'main' into za/create-federal-portfolio-force-create-suborg
This commit is contained in:
commit
163df2e390
31 changed files with 739 additions and 135 deletions
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
|
@ -3,10 +3,6 @@ name: Testing
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
- '**.md'
|
|
||||||
- '.gitignore'
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
|
@ -954,3 +954,40 @@ To create a specific portfolio:
|
||||||
|
|
||||||
#### Step 1: Running the script
|
#### Step 1: Running the script
|
||||||
```docker-compose exec app ./manage.py patch_suborganizations```
|
```docker-compose exec app ./manage.py patch_suborganizations```
|
||||||
|
|
||||||
|
|
||||||
|
## Remove Non-whitelisted Portfolios
|
||||||
|
This script removes Portfolio entries from the database that are not part of a predefined list of allowed portfolios (`ALLOWED_PORTFOLIOS`).
|
||||||
|
It performs the following actions:
|
||||||
|
1. Prompts the user for confirmation before proceeding with deletions.
|
||||||
|
2. Updates related objects such as `DomainInformation`, `Domain`, and `DomainRequest` to set their `portfolio` field to `None` to prevent integrity errors.
|
||||||
|
3. Deletes associated objects such as `PortfolioInvitation`, `UserPortfolioPermission`, and `Suborganization`.
|
||||||
|
4. Logs a detailed summary of all cascading deletions and orphaned objects.
|
||||||
|
|
||||||
|
### Running on sandboxes
|
||||||
|
|
||||||
|
#### Step 1: Login to CloudFoundry
|
||||||
|
```cf login -a api.fr.cloud.gov --sso```
|
||||||
|
|
||||||
|
#### Step 2: SSH into your environment
|
||||||
|
```cf ssh getgov-{space}```
|
||||||
|
|
||||||
|
Example: `cf ssh getgov-nl`
|
||||||
|
|
||||||
|
#### Step 3: Create a shell instance
|
||||||
|
```/tmp/lifecycle/shell```
|
||||||
|
|
||||||
|
#### Step 4: Running the script
|
||||||
|
To remove portfolios:
|
||||||
|
```./manage.py remove_unused_portfolios```
|
||||||
|
|
||||||
|
If you wish to enable debug mode for additional logging:
|
||||||
|
```./manage.py remove_unused_portfolios --debug```
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
|
||||||
|
#### Step 1: Running the script
|
||||||
|
```docker-compose exec app ./manage.py remove_unused_portfolios```
|
||||||
|
|
||||||
|
To enable debug mode locally:
|
||||||
|
```docker-compose exec app ./manage.py remove_unused_portfolios --debug```
|
|
@ -1222,9 +1222,9 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
class SeniorOfficialAdmin(ListHeaderAdmin):
|
class SeniorOfficialAdmin(ListHeaderAdmin):
|
||||||
"""Custom Senior Official Admin class."""
|
"""Custom Senior Official Admin class."""
|
||||||
|
|
||||||
search_fields = ["first_name", "last_name", "email"]
|
search_fields = ["first_name", "last_name", "email", "federal_agency__agency"]
|
||||||
search_help_text = "Search by first name, last name or email."
|
search_help_text = "Search by first name, last name or email."
|
||||||
list_display = ["first_name", "last_name", "email", "federal_agency"]
|
list_display = ["federal_agency", "first_name", "last_name", "email"]
|
||||||
|
|
||||||
# this ordering effects the ordering of results
|
# this ordering effects the ordering of results
|
||||||
# in autocomplete_fields for Senior Official
|
# in autocomplete_fields for Senior Official
|
||||||
|
@ -1678,22 +1678,25 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
DomainInformation.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(portfolio__organization_type__isnull=False, then="portfolio__organization_type"),
|
||||||
|
When(portfolio__isnull=True, generic_org_type__isnull=False, then="generic_org_type"),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_info in DomainInformation.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
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):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__organization_type=self.value())
|
Q(portfolio__organization_type=self.value())
|
||||||
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||||
|
@ -2031,22 +2034,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
DomainRequest.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(portfolio__organization_type__isnull=False, then="portfolio__organization_type"),
|
||||||
|
When(portfolio__isnull=True, generic_org_type__isnull=False, then="generic_org_type"),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_request in DomainRequest.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
converted_generic_org = domain_request.converted_generic_org_type # Actual value
|
|
||||||
converted_generic_org_display = domain_request.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):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__organization_type=self.value())
|
Q(portfolio__organization_type=self.value())
|
||||||
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||||
|
@ -2062,24 +2068,39 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_federal_types"
|
parameter_name = "converted_federal_types"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_federal_types = set()
|
# Annotate the queryset for efficient filtering
|
||||||
|
queryset = (
|
||||||
# Populate the set with tuples of (value, display value)
|
DomainRequest.objects.annotate(
|
||||||
for domain_request in DomainRequest.objects.all():
|
converted_federal_type=Case(
|
||||||
converted_federal_type = domain_request.converted_federal_type # Actual value
|
When(
|
||||||
converted_federal_type_display = domain_request.converted_federal_type_display # Display value
|
portfolio__isnull=False,
|
||||||
|
portfolio__federal_agency__federal_type__isnull=False,
|
||||||
if converted_federal_type:
|
then="portfolio__federal_agency__federal_type",
|
||||||
converted_federal_types.add(
|
),
|
||||||
(converted_federal_type, converted_federal_type_display) # Value, Display
|
When(
|
||||||
|
portfolio__isnull=True,
|
||||||
|
federal_agency__federal_type__isnull=False,
|
||||||
|
then="federal_agency__federal_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_federal_type", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Sort the set by display value
|
# Filter out empty values and return sorted unique entries
|
||||||
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
|
return sorted(
|
||||||
|
[
|
||||||
|
(federal_type, BranchChoices.get_branch_label(federal_type))
|
||||||
|
for federal_type in queryset
|
||||||
|
if federal_type
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a federal type is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__federal_agency__federal_type=self.value())
|
Q(portfolio__federal_agency__federal_type=self.value())
|
||||||
| Q(portfolio__isnull=True, federal_type=self.value())
|
| Q(portfolio__isnull=True, federal_type=self.value())
|
||||||
|
@ -3226,59 +3247,86 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
Domain.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__organization_type__isnull=False,
|
||||||
|
then="domain_info__portfolio__organization_type",
|
||||||
|
),
|
||||||
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=True,
|
||||||
|
domain_info__generic_org_type__isnull=False,
|
||||||
|
then="domain_info__generic_org_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_info in DomainInformation.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
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):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(domain_info__portfolio__organization_type=self.value())
|
Q(domain_info__portfolio__organization_type=self.value())
|
||||||
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
|
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
class FederalTypeFilter(admin.SimpleListFilter):
|
class FederalTypeFilter(admin.SimpleListFilter):
|
||||||
"""Custom Federal Type filter that accomodates portfolio feature.
|
"""Custom Federal Type filter that accomodates portfolio feature.
|
||||||
If we have a portfolio, use the portfolio's federal type. If not, use the
|
If we have a portfolio, use the portfolio's federal type. If not, use the
|
||||||
federal type in the Domain Information object."""
|
organization in the Domain Request object."""
|
||||||
|
|
||||||
title = "federal type"
|
title = "federal type"
|
||||||
parameter_name = "converted_federal_types"
|
parameter_name = "converted_federal_types"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_federal_types = set()
|
# Annotate the queryset for efficient filtering
|
||||||
|
queryset = (
|
||||||
# Populate the set with tuples of (value, display value)
|
Domain.objects.annotate(
|
||||||
for domain_info in DomainInformation.objects.all():
|
converted_federal_type=Case(
|
||||||
converted_federal_type = domain_info.converted_federal_type # Actual value
|
When(
|
||||||
converted_federal_type_display = domain_info.converted_federal_type_display # Display value
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=False,
|
||||||
if converted_federal_type:
|
then=F("domain_info__portfolio__federal_agency__federal_type"),
|
||||||
converted_federal_types.add(
|
),
|
||||||
(converted_federal_type, converted_federal_type_display) # Value, Display
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=True,
|
||||||
|
domain_info__federal_type__isnull=False,
|
||||||
|
then="domain_info__federal_agency__federal_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_federal_type", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Sort the set by display value
|
# Filter out empty values and return sorted unique entries
|
||||||
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
|
return sorted(
|
||||||
|
[
|
||||||
|
(federal_type, BranchChoices.get_branch_label(federal_type))
|
||||||
|
for federal_type in queryset
|
||||||
|
if federal_type
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a federal type is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(domain_info__portfolio__federal_agency__federal_type=self.value())
|
Q(domain_info__portfolio__federal_type=self.value())
|
||||||
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
|
| Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value())
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -323,22 +323,50 @@ class DomainRequestFixture:
|
||||||
cls._create_domain_requests(users)
|
cls._create_domain_requests(users)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_domain_requests(cls, users):
|
def _create_domain_requests(cls, users): # noqa: C901
|
||||||
"""Creates DomainRequests given a list of users."""
|
"""Creates DomainRequests given a list of users."""
|
||||||
|
total_domain_requests_to_make = len(users) # 100000
|
||||||
|
|
||||||
|
# Check if the database is already populated with the desired
|
||||||
|
# number of entries.
|
||||||
|
# (Prevents re-adding more entries to an already populated database,
|
||||||
|
# which happens when restarting Docker src)
|
||||||
|
domain_requests_already_made = DomainRequest.objects.count()
|
||||||
|
|
||||||
domain_requests_to_create = []
|
domain_requests_to_create = []
|
||||||
for user in users:
|
if domain_requests_already_made < total_domain_requests_to_make:
|
||||||
for request_data in cls.DOMAINREQUESTS:
|
for user in users:
|
||||||
# Prepare DomainRequest objects
|
for request_data in cls.DOMAINREQUESTS:
|
||||||
try:
|
# Prepare DomainRequest objects
|
||||||
domain_request = DomainRequest(
|
try:
|
||||||
creator=user,
|
domain_request = DomainRequest(
|
||||||
organization_name=request_data["organization_name"],
|
creator=user,
|
||||||
)
|
organization_name=request_data["organization_name"],
|
||||||
cls._set_non_foreign_key_fields(domain_request, request_data)
|
)
|
||||||
cls._set_foreign_key_fields(domain_request, request_data, user)
|
cls._set_non_foreign_key_fields(domain_request, request_data)
|
||||||
domain_requests_to_create.append(domain_request)
|
cls._set_foreign_key_fields(domain_request, request_data, user)
|
||||||
except Exception as e:
|
domain_requests_to_create.append(domain_request)
|
||||||
logger.warning(e)
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
|
||||||
|
num_additional_requests_to_make = (
|
||||||
|
total_domain_requests_to_make - domain_requests_already_made - len(domain_requests_to_create)
|
||||||
|
)
|
||||||
|
if num_additional_requests_to_make > 0:
|
||||||
|
for _ in range(num_additional_requests_to_make):
|
||||||
|
random_user = random.choice(users) # nosec
|
||||||
|
try:
|
||||||
|
random_request_type = random.choice(cls.DOMAINREQUESTS) # nosec
|
||||||
|
# Prepare DomainRequest objects
|
||||||
|
domain_request = DomainRequest(
|
||||||
|
creator=random_user,
|
||||||
|
organization_name=random_request_type["organization_name"],
|
||||||
|
)
|
||||||
|
cls._set_non_foreign_key_fields(domain_request, random_request_type)
|
||||||
|
cls._set_foreign_key_fields(domain_request, random_request_type, random_user)
|
||||||
|
domain_requests_to_create.append(domain_request)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error creating random domain request: {e}")
|
||||||
|
|
||||||
# Bulk create domain requests
|
# Bulk create domain requests
|
||||||
cls._bulk_create_requests(domain_requests_to_create)
|
cls._bulk_create_requests(domain_requests_to_create)
|
||||||
|
|
238
src/registrar/management/commands/remove_unused_portfolios.py
Normal file
238
src/registrar/management/commands/remove_unused_portfolios.py
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.db import transaction
|
||||||
|
from registrar.management.commands.utility.terminal_helper import (
|
||||||
|
TerminalColors,
|
||||||
|
TerminalHelper,
|
||||||
|
)
|
||||||
|
from registrar.models import (
|
||||||
|
Portfolio,
|
||||||
|
DomainGroup,
|
||||||
|
DomainInformation,
|
||||||
|
DomainRequest,
|
||||||
|
PortfolioInvitation,
|
||||||
|
Suborganization,
|
||||||
|
UserPortfolioPermission,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ALLOWED_PORTFOLIOS = [
|
||||||
|
"Department of Veterans Affairs",
|
||||||
|
"Department of the Treasury",
|
||||||
|
"National Archives and Records Administration",
|
||||||
|
"Department of Defense",
|
||||||
|
"Office of Personnel Management",
|
||||||
|
"National Aeronautics and Space Administration",
|
||||||
|
"City and County of San Francisco",
|
||||||
|
"State of Arizona, Executive Branch",
|
||||||
|
"Department of the Interior",
|
||||||
|
"Department of State",
|
||||||
|
"Department of Justice",
|
||||||
|
"Capitol Police",
|
||||||
|
"Administrative Office of the Courts",
|
||||||
|
"Supreme Court of the United States",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Remove all Portfolio entries with names not in the allowed list."
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""
|
||||||
|
OPTIONAL ARGUMENTS:
|
||||||
|
--debug
|
||||||
|
A boolean (default to true), which activates additional print statements
|
||||||
|
"""
|
||||||
|
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
|
||||||
|
|
||||||
|
def prompt_delete_entries(self, portfolios_to_delete, debug_on):
|
||||||
|
"""Brings up a prompt in the terminal asking
|
||||||
|
if the user wishes to delete data in the
|
||||||
|
Portfolio table. If the user confirms,
|
||||||
|
deletes the data in the Portfolio table"""
|
||||||
|
|
||||||
|
entries_to_remove_by_name = list(portfolios_to_delete.values_list("organization_name", flat=True))
|
||||||
|
formatted_entries = "\n\t\t".join(entries_to_remove_by_name)
|
||||||
|
confirm_delete = TerminalHelper.query_yes_no(
|
||||||
|
f"""
|
||||||
|
{TerminalColors.FAIL}
|
||||||
|
WARNING: You are about to delete the following portfolios:
|
||||||
|
|
||||||
|
{formatted_entries}
|
||||||
|
|
||||||
|
Are you sure you want to continue?{TerminalColors.ENDC}"""
|
||||||
|
)
|
||||||
|
if confirm_delete:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.YELLOW}
|
||||||
|
----------Deleting entries----------
|
||||||
|
(please wait)
|
||||||
|
{TerminalColors.ENDC}"""
|
||||||
|
)
|
||||||
|
self.delete_entries(portfolios_to_delete, debug_on)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.OKCYAN}
|
||||||
|
----------No entries deleted----------
|
||||||
|
(exiting script)
|
||||||
|
{TerminalColors.ENDC}"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_entries(self, portfolios_to_delete, debug_on): # noqa: C901
|
||||||
|
# Log the number of entries being removed
|
||||||
|
count = portfolios_to_delete.count()
|
||||||
|
if count == 0:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.OKCYAN}
|
||||||
|
No entries to remove.
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If debug mode is on, print out entries being removed
|
||||||
|
if debug_on:
|
||||||
|
entries_to_remove_by_name = list(portfolios_to_delete.values_list("organization_name", flat=True))
|
||||||
|
formatted_entries = ", ".join(entries_to_remove_by_name)
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.YELLOW}
|
||||||
|
Entries to be removed: {formatted_entries}
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for portfolios with non-empty related objects
|
||||||
|
# (These will throw integrity errors if they are not updated)
|
||||||
|
portfolios_with_assignments = []
|
||||||
|
for portfolio in portfolios_to_delete:
|
||||||
|
has_assignments = any(
|
||||||
|
[
|
||||||
|
DomainGroup.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
DomainInformation.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
DomainRequest.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
PortfolioInvitation.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
Suborganization.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
UserPortfolioPermission.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if has_assignments:
|
||||||
|
portfolios_with_assignments.append(portfolio)
|
||||||
|
|
||||||
|
if portfolios_with_assignments:
|
||||||
|
formatted_entries = "\n\t\t".join(
|
||||||
|
f"{portfolio.organization_name}" for portfolio in portfolios_with_assignments
|
||||||
|
)
|
||||||
|
confirm_cascade_delete = TerminalHelper.query_yes_no(
|
||||||
|
f"""
|
||||||
|
{TerminalColors.FAIL}
|
||||||
|
WARNING: these entries have related objects.
|
||||||
|
|
||||||
|
{formatted_entries}
|
||||||
|
|
||||||
|
Deleting them will update any associated domains / domain requests to have no portfolio
|
||||||
|
and will cascade delete any associated portfolio invitations, portfolio permissions, domain groups,
|
||||||
|
and suborganizations. Any suborganizations that get deleted will also orphan (not delete) their
|
||||||
|
associated domains / domain requests.
|
||||||
|
|
||||||
|
Are you sure you want to continue?{TerminalColors.ENDC}"""
|
||||||
|
)
|
||||||
|
if not confirm_cascade_delete:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.OKCYAN}
|
||||||
|
Operation canceled by the user.
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# Try to delete the portfolios
|
||||||
|
try:
|
||||||
|
summary = []
|
||||||
|
for portfolio in portfolios_to_delete:
|
||||||
|
portfolio_summary = [f"---- CASCADE SUMMARY for {portfolio.organization_name} -----"]
|
||||||
|
if portfolio in portfolios_with_assignments:
|
||||||
|
domain_groups = DomainGroup.objects.filter(portfolio=portfolio)
|
||||||
|
domain_informations = DomainInformation.objects.filter(portfolio=portfolio)
|
||||||
|
domain_requests = DomainRequest.objects.filter(portfolio=portfolio)
|
||||||
|
portfolio_invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
|
||||||
|
suborganizations = Suborganization.objects.filter(portfolio=portfolio)
|
||||||
|
user_permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio)
|
||||||
|
|
||||||
|
if domain_groups.exists():
|
||||||
|
formatted_groups = "\n".join([str(group) for group in domain_groups])
|
||||||
|
portfolio_summary.append(f"{len(domain_groups)} Deleted DomainGroups:\n{formatted_groups}")
|
||||||
|
domain_groups.delete()
|
||||||
|
|
||||||
|
if domain_informations.exists():
|
||||||
|
formatted_domain_infos = "\n".join([str(info) for info in domain_informations])
|
||||||
|
portfolio_summary.append(
|
||||||
|
f"{len(domain_informations)} Orphaned DomainInformations:\n{formatted_domain_infos}"
|
||||||
|
)
|
||||||
|
domain_informations.update(portfolio=None)
|
||||||
|
|
||||||
|
if domain_requests.exists():
|
||||||
|
formatted_domain_reqs = "\n".join([str(req) for req in domain_requests])
|
||||||
|
portfolio_summary.append(
|
||||||
|
f"{len(domain_requests)} Orphaned DomainRequests:\n{formatted_domain_reqs}"
|
||||||
|
)
|
||||||
|
domain_requests.update(portfolio=None)
|
||||||
|
|
||||||
|
if portfolio_invitations.exists():
|
||||||
|
formatted_portfolio_invitations = "\n".join([str(inv) for inv in portfolio_invitations])
|
||||||
|
portfolio_summary.append(
|
||||||
|
f"{len(portfolio_invitations)} Deleted PortfolioInvitations:\n{formatted_portfolio_invitations}" # noqa
|
||||||
|
)
|
||||||
|
portfolio_invitations.delete()
|
||||||
|
|
||||||
|
if user_permissions.exists():
|
||||||
|
formatted_user_list = "\n".join(
|
||||||
|
[perm.user.get_formatted_name() for perm in user_permissions]
|
||||||
|
)
|
||||||
|
portfolio_summary.append(
|
||||||
|
f"Deleted UserPortfolioPermissions for the following users:\n{formatted_user_list}"
|
||||||
|
)
|
||||||
|
user_permissions.delete()
|
||||||
|
|
||||||
|
if suborganizations.exists():
|
||||||
|
portfolio_summary.append("Cascade Deleted Suborganizations:")
|
||||||
|
for suborg in suborganizations:
|
||||||
|
DomainInformation.objects.filter(sub_organization=suborg).update(sub_organization=None)
|
||||||
|
DomainRequest.objects.filter(sub_organization=suborg).update(sub_organization=None)
|
||||||
|
portfolio_summary.append(f"{suborg.name}")
|
||||||
|
suborg.delete()
|
||||||
|
|
||||||
|
portfolio.delete()
|
||||||
|
summary.append("\n\n".join(portfolio_summary))
|
||||||
|
summary_string = "\n\n".join(summary)
|
||||||
|
|
||||||
|
# Output a success message with detailed summary
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.OKCYAN}
|
||||||
|
Successfully removed {count} portfolios.
|
||||||
|
|
||||||
|
The following portfolio deletions had cascading effects;
|
||||||
|
|
||||||
|
{summary_string}
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
except IntegrityError as e:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.FAIL}
|
||||||
|
Could not delete some portfolios due to integrity constraints:
|
||||||
|
{e}
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# Get all Portfolio entries not in the allowed portfolios list
|
||||||
|
portfolios_to_delete = Portfolio.objects.exclude(organization_name__in=ALLOWED_PORTFOLIOS)
|
||||||
|
|
||||||
|
self.prompt_delete_entries(portfolios_to_delete, options.get("debug"))
|
|
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.federal_agency import FederalAgency
|
from registrar.models.federal_agency import FederalAgency
|
||||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices
|
||||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
|
@ -903,6 +904,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
email_template,
|
email_template,
|
||||||
email_template_subject,
|
email_template_subject,
|
||||||
bcc_address="",
|
bcc_address="",
|
||||||
|
cc_addresses: list[str] = [],
|
||||||
context=None,
|
context=None,
|
||||||
send_email=True,
|
send_email=True,
|
||||||
wrap_email=False,
|
wrap_email=False,
|
||||||
|
@ -955,12 +957,20 @@ class DomainRequest(TimeStampedModel):
|
||||||
|
|
||||||
if custom_email_content:
|
if custom_email_content:
|
||||||
context["custom_email_content"] = custom_email_content
|
context["custom_email_content"] = custom_email_content
|
||||||
|
|
||||||
|
if self.requesting_entity_is_portfolio() or self.requesting_entity_is_suborganization():
|
||||||
|
portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions( # type: ignore
|
||||||
|
permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS], include_admin=True
|
||||||
|
)
|
||||||
|
cc_addresses = list(portfolio_view_requests_users.values_list("email", flat=True))
|
||||||
|
|
||||||
send_templated_email(
|
send_templated_email(
|
||||||
email_template,
|
email_template,
|
||||||
email_template_subject,
|
email_template_subject,
|
||||||
recipient.email,
|
recipient.email,
|
||||||
context=context,
|
context=context,
|
||||||
bcc_address=bcc_address,
|
bcc_address=bcc_address,
|
||||||
|
cc_addresses=cc_addresses,
|
||||||
wrap_email=wrap_email,
|
wrap_email=wrap_email,
|
||||||
)
|
)
|
||||||
logger.info(f"The {new_status} email sent to: {recipient.email}")
|
logger.info(f"The {new_status} email sent to: {recipient.email}")
|
||||||
|
|
|
@ -4,6 +4,7 @@ from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.federal_agency import FederalAgency
|
from registrar.models.federal_agency import FederalAgency
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
@ -122,6 +123,16 @@ class Portfolio(TimeStampedModel):
|
||||||
if self.state_territory != self.StateTerritoryChoices.PUERTO_RICO and self.urbanization:
|
if self.state_territory != self.StateTerritoryChoices.PUERTO_RICO and self.urbanization:
|
||||||
self.urbanization = None
|
self.urbanization = None
|
||||||
|
|
||||||
|
# If the org type is federal, and org federal agency is not blank, and is a federal agency
|
||||||
|
# overwrite the organization name with the federal agency's agency
|
||||||
|
if (
|
||||||
|
self.organization_type == self.OrganizationChoices.FEDERAL
|
||||||
|
and self.federal_agency
|
||||||
|
and self.federal_agency != FederalAgency.get_non_federal_agency()
|
||||||
|
and self.federal_agency.agency
|
||||||
|
):
|
||||||
|
self.organization_name = self.federal_agency.agency
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -144,6 +155,25 @@ class Portfolio(TimeStampedModel):
|
||||||
).values_list("user__id", flat=True)
|
).values_list("user__id", flat=True)
|
||||||
return User.objects.filter(id__in=admin_ids)
|
return User.objects.filter(id__in=admin_ids)
|
||||||
|
|
||||||
|
def portfolio_users_with_permissions(self, permissions=[], include_admin=False):
|
||||||
|
"""Gets all users with specified additional permissions for this particular portfolio.
|
||||||
|
Returns a queryset of User."""
|
||||||
|
portfolio_users = self.portfolio_users
|
||||||
|
if permissions:
|
||||||
|
if include_admin:
|
||||||
|
portfolio_users = portfolio_users.filter(
|
||||||
|
Q(additional_permissions__overlap=permissions)
|
||||||
|
| Q(
|
||||||
|
roles__overlap=[
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
portfolio_users = portfolio_users.filter(additional_permissions__overlap=permissions)
|
||||||
|
user_ids = portfolio_users.values_list("user__id", flat=True)
|
||||||
|
return User.objects.filter(id__in=user_ids)
|
||||||
|
|
||||||
# == Getters for domains == #
|
# == Getters for domains == #
|
||||||
def get_domains(self, order_by=None):
|
def get_domains(self, order_by=None):
|
||||||
"""Returns all DomainInformations associated with this portfolio"""
|
"""Returns all DomainInformations associated with this portfolio"""
|
||||||
|
|
|
@ -55,7 +55,9 @@ class SeniorOfficial(TimeStampedModel):
|
||||||
return " ".join(names) if names else "Unknown"
|
return " ".join(names) if names else "Unknown"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.first_name or self.last_name:
|
if self.federal_agency and (self.first_name or self.last_name):
|
||||||
|
return self.get_formatted_name() + " of " + self.federal_agency.__str__()
|
||||||
|
elif self.first_name or self.last_name:
|
||||||
return self.get_formatted_name()
|
return self.get_formatted_name()
|
||||||
elif self.pk:
|
elif self.pk:
|
||||||
return str(self.pk)
|
return str(self.pk)
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
{% block title %}Add a domain manager | {% endblock %}
|
{% block title %}Add a domain manager | {% endblock %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -38,8 +41,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
|
|
||||||
<h1>Add a domain manager</h1>
|
<h1>Add a domain manager</h1>
|
||||||
{% if has_organization_feature_flag %}
|
{% if has_organization_feature_flag %}
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% for form in formset %}
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -38,10 +42,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for form in formset %}
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<h1 id="domain-dsdata">DS data</h1>
|
<h1 id="domain-dsdata">DS data</h1>
|
||||||
|
|
||||||
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
||||||
|
|
|
@ -4,6 +4,12 @@
|
||||||
{% block title %}DNS name servers | {{ domain.name }} | {% endblock %}
|
{% block title %}DNS name servers | {{ domain.name }} | {% endblock %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{# this is right after the messages block in the parent template #}
|
||||||
|
{% for form in formset %}
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -26,11 +32,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
{# this is right after the messages block in the parent template #}
|
|
||||||
{% for form in formset %}
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<h1>DNS name servers</h1>
|
<h1>DNS name servers</h1>
|
||||||
|
|
||||||
<p>Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.</p>
|
<p>Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.</p>
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
{% block title %}Security email | {{ domain.name }} | {% endblock %}
|
{% block title %}Security email | {{ domain.name }} | {% endblock %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -23,8 +26,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
|
|
||||||
<h1>Security email</h1>
|
<h1>Security email</h1>
|
||||||
|
|
||||||
<p>We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'about/data/' %}">.gov domain data</a> we provide.</p>
|
<p>We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'about/data/' %}">.gov domain data</a> we provide.</p>
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -24,10 +26,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
{# this is right after the messages block in the parent template #}
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
|
|
||||||
|
|
||||||
<h1>Suborganization</h1>
|
<h1>Suborganization</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -2,27 +2,23 @@
|
||||||
Hi,{% if domain_manager and domain_manager.first_name %} {{ domain_manager.first_name }}.{% endif %}
|
Hi,{% if domain_manager and domain_manager.first_name %} {{ domain_manager.first_name }}.{% endif %}
|
||||||
|
|
||||||
A domain manager was invited to {{ domain.name }}.
|
A domain manager was invited to {{ domain.name }}.
|
||||||
DOMAIN: {{ domain.name }}
|
|
||||||
INVITED BY: {{ requestor_email }}
|
INVITED BY: {{ requestor_email }}
|
||||||
INVITED ON: {{date}}
|
INVITED ON: {{date}}
|
||||||
MANAGER INVITED: {{ invited_email_address }}
|
MANAGER INVITED: {{ invited_email_address }}
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
NEXT STEPS
|
NEXT STEPS
|
||||||
|
|
||||||
The person who received the invitation will become a domain manager once they log in to the
|
The person who received the invitation will become a domain manager once they log in to the
|
||||||
.gov registrar. They'll need to access the registrar using a Login.gov account that's
|
.gov registrar. They'll need to access the registrar using a Login.gov account that's
|
||||||
associated with the invited email address.
|
associated with the invited email address.
|
||||||
|
|
||||||
If you need to cancel this invitation or remove the domain manager (because they've already
|
If you need to cancel this invitation or remove the domain manager, you can do that by going to
|
||||||
logged in), you can do that by going to this domain in the .gov registrar <https://manage.get.gov/>.
|
this domain in the .gov registrar <https://manage.get.gov/>.
|
||||||
|
|
||||||
|
|
||||||
WHY DID YOU RECEIVE THIS EMAIL?
|
WHY DID YOU RECEIVE THIS EMAIL?
|
||||||
|
|
||||||
You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever
|
You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever
|
||||||
someone is invited to manage that domain.
|
someone is invited to manage that domain.
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Your .gov domain request has been withdrawn and will not be reviewed by our team.
|
Your .gov domain request has been withdrawn and will not be reviewed by our team.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Withdrawn
|
STATUS: Withdrawn
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Congratulations! Your .gov domain request has been approved.
|
Congratulations! Your .gov domain request has been approved.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Approved
|
STATUS: Approved
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Your .gov domain request has been rejected.
|
Your .gov domain request has been rejected.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Rejected
|
STATUS: Rejected
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We received your .gov domain request.
|
We received your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Submitted
|
STATUS: Submitted
|
||||||
|
|
||||||
|
@ -11,13 +12,15 @@ STATUS: Submitted
|
||||||
|
|
||||||
NEXT STEPS
|
NEXT STEPS
|
||||||
We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience.
|
We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience.
|
||||||
|
{% if is_org_user %}
|
||||||
|
During our review we’ll verify that your requested domain meets our naming requirements.
|
||||||
|
{% else %}
|
||||||
During our review, we’ll verify that:
|
During our review, we’ll verify that:
|
||||||
- Your organization is eligible for a .gov domain
|
- Your organization is eligible for a .gov domain
|
||||||
- You work at the organization and/or can make requests on its behalf
|
- You work at the organization and/or can make requests on its behalf
|
||||||
- Your requested domain meets our naming requirements
|
- Your requested domain meets our naming requirements
|
||||||
|
{% endif %}
|
||||||
We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov>
|
We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov>.
|
||||||
|
|
||||||
|
|
||||||
NEED TO MAKE CHANGES?
|
NEED TO MAKE CHANGES?
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
|
|
||||||
<div id="main-content" class=" {% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
|
<div id="main-content" class=" {% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
|
||||||
<!-- Form messages -->
|
<!-- Form messages -->
|
||||||
|
|
|
@ -779,7 +779,7 @@ 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=57)
|
self.assertContains(response, "Federal", count=56)
|
||||||
# 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
|
||||||
|
|
|
@ -662,7 +662,7 @@ 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=55)
|
self.assertContains(response, "Federal", count=54)
|
||||||
# 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
|
||||||
|
|
|
@ -3,7 +3,10 @@ import boto3_mocking # type: ignore
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
from registrar.models.domain_group import DomainGroup
|
||||||
|
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||||
from registrar.models.senior_official import SeniorOfficial
|
from registrar.models.senior_official import SeniorOfficial
|
||||||
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
|
@ -2238,3 +2241,116 @@ class TestPatchSuborganizations(MockDbForIndividualTests):
|
||||||
self.assertEqual(self.domain_information_1.sub_organization, keep_org)
|
self.assertEqual(self.domain_information_1.sub_organization, keep_org)
|
||||||
self.assertEqual(self.domain_request_2.sub_organization, unrelated_org)
|
self.assertEqual(self.domain_request_2.sub_organization, unrelated_org)
|
||||||
self.assertEqual(self.domain_information_2.sub_organization, unrelated_org)
|
self.assertEqual(self.domain_information_2.sub_organization, unrelated_org)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRemovePortfolios(TestCase):
|
||||||
|
"""Test the remove_unused_portfolios command"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(username="testuser")
|
||||||
|
|
||||||
|
self.logger_patcher = patch("registrar.management.commands.export_tables.logger")
|
||||||
|
self.logger_mock = self.logger_patcher.start()
|
||||||
|
|
||||||
|
# Create mock database objects
|
||||||
|
self.portfolio_ok = Portfolio.objects.create(
|
||||||
|
organization_name="Department of Veterans Affairs", creator=self.user
|
||||||
|
)
|
||||||
|
self.unused_portfolio_with_related_objects = Portfolio.objects.create(
|
||||||
|
organization_name="Test with orphaned objects", creator=self.user
|
||||||
|
)
|
||||||
|
self.unused_portfolio_with_suborgs = Portfolio.objects.create(
|
||||||
|
organization_name="Test with suborg", creator=self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create related objects for unused_portfolio_with_related_objects
|
||||||
|
self.domain_information = DomainInformation.objects.create(
|
||||||
|
portfolio=self.unused_portfolio_with_related_objects, creator=self.user
|
||||||
|
)
|
||||||
|
self.domain_request = DomainRequest.objects.create(
|
||||||
|
portfolio=self.unused_portfolio_with_related_objects, creator=self.user
|
||||||
|
)
|
||||||
|
self.inv = PortfolioInvitation.objects.create(portfolio=self.unused_portfolio_with_related_objects)
|
||||||
|
self.group = DomainGroup.objects.create(
|
||||||
|
portfolio=self.unused_portfolio_with_related_objects, name="Test Domain Group"
|
||||||
|
)
|
||||||
|
self.perm = UserPortfolioPermission.objects.create(
|
||||||
|
portfolio=self.unused_portfolio_with_related_objects, user=self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a suborganization and suborg related objects for unused_portfolio_with_suborgs
|
||||||
|
self.suborganization = Suborganization.objects.create(
|
||||||
|
portfolio=self.unused_portfolio_with_suborgs, name="Test Suborg"
|
||||||
|
)
|
||||||
|
self.suborg_domain_information = DomainInformation.objects.create(
|
||||||
|
sub_organization=self.suborganization, creator=self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.logger_patcher.stop()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
Suborganization.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
|
||||||
|
def test_delete_unlisted_portfolios(self, mock_query_yes_no):
|
||||||
|
"""Test that portfolios not on the allowed list are deleted."""
|
||||||
|
mock_query_yes_no.return_value = True
|
||||||
|
|
||||||
|
# Ensure all portfolios exist before running the command
|
||||||
|
self.assertEqual(Portfolio.objects.count(), 3)
|
||||||
|
|
||||||
|
# Run the command
|
||||||
|
call_command("remove_unused_portfolios", debug=False)
|
||||||
|
|
||||||
|
# Check that the unlisted portfolio was removed
|
||||||
|
self.assertEqual(Portfolio.objects.count(), 1)
|
||||||
|
self.assertFalse(Portfolio.objects.filter(organization_name="Test with orphaned objects").exists())
|
||||||
|
self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists())
|
||||||
|
self.assertTrue(Portfolio.objects.filter(organization_name="Department of Veterans Affairs").exists())
|
||||||
|
|
||||||
|
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
|
||||||
|
def test_delete_entries_with_related_objects(self, mock_query_yes_no):
|
||||||
|
"""Test deletion with related objects being handled properly."""
|
||||||
|
mock_query_yes_no.return_value = True
|
||||||
|
|
||||||
|
# Ensure related objects exist before running the command
|
||||||
|
self.assertEqual(DomainInformation.objects.count(), 2)
|
||||||
|
self.assertEqual(DomainRequest.objects.count(), 1)
|
||||||
|
|
||||||
|
# Run the command
|
||||||
|
call_command("remove_unused_portfolios", debug=False)
|
||||||
|
|
||||||
|
# Check that related objects were updated
|
||||||
|
self.assertEqual(
|
||||||
|
DomainInformation.objects.filter(portfolio=self.unused_portfolio_with_related_objects).count(), 0
|
||||||
|
)
|
||||||
|
self.assertEqual(DomainRequest.objects.filter(portfolio=self.unused_portfolio_with_related_objects).count(), 0)
|
||||||
|
self.assertEqual(DomainInformation.objects.filter(portfolio=None).count(), 2)
|
||||||
|
self.assertEqual(DomainRequest.objects.filter(portfolio=None).count(), 1)
|
||||||
|
|
||||||
|
# Check that the portfolio was deleted
|
||||||
|
self.assertFalse(Portfolio.objects.filter(organization_name="Test with orphaned objects").exists())
|
||||||
|
|
||||||
|
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
|
||||||
|
def test_delete_entries_with_suborganizations(self, mock_query_yes_no):
|
||||||
|
"""Test that suborganizations and their related objects are deleted along with the portfolio."""
|
||||||
|
mock_query_yes_no.return_value = True
|
||||||
|
|
||||||
|
# Ensure suborganization and related objects exist before running the command
|
||||||
|
self.assertEqual(Suborganization.objects.count(), 1)
|
||||||
|
self.assertEqual(DomainInformation.objects.filter(sub_organization=self.suborganization).count(), 1)
|
||||||
|
|
||||||
|
# Run the command
|
||||||
|
call_command("remove_unused_portfolios", debug=False)
|
||||||
|
|
||||||
|
# Check that the suborganization was deleted
|
||||||
|
self.assertEqual(Suborganization.objects.filter(portfolio=self.unused_portfolio_with_suborgs).count(), 0)
|
||||||
|
|
||||||
|
# Check that deletion of suborganization had cascading effects (orphaned DomainInformation)
|
||||||
|
self.assertEqual(DomainInformation.objects.filter(sub_organization=self.suborganization).count(), 0)
|
||||||
|
|
||||||
|
# Check that the portfolio was deleted
|
||||||
|
self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists())
|
||||||
|
|
|
@ -2073,13 +2073,18 @@ class TestPortfolio(TestCase):
|
||||||
self.user, _ = User.objects.get_or_create(
|
self.user, _ = User.objects.get_or_create(
|
||||||
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World"
|
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World"
|
||||||
)
|
)
|
||||||
|
self.non_federal_agency, _ = FederalAgency.objects.get_or_create(agency="Non-Federal Agency")
|
||||||
|
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="Federal Agency")
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
|
self.federal_agency.delete()
|
||||||
|
# not deleting non_federal_agency so as not to interfere potentially with other tests
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_urbanization_field_resets_when_not_puetro_rico(self):
|
def test_urbanization_field_resets_when_not_puetro_rico(self):
|
||||||
"""The urbanization field should only be populated when the state is puetro rico.
|
"""The urbanization field should only be populated when the state is puetro rico.
|
||||||
Otherwise, this field should be empty."""
|
Otherwise, this field should be empty."""
|
||||||
|
@ -2100,6 +2105,7 @@ class TestPortfolio(TestCase):
|
||||||
self.assertEqual(portfolio.urbanization, None)
|
self.assertEqual(portfolio.urbanization, None)
|
||||||
self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.ALABAMA)
|
self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.ALABAMA)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_can_add_urbanization_field(self):
|
def test_can_add_urbanization_field(self):
|
||||||
"""Ensures that you can populate the urbanization field when conditions are right"""
|
"""Ensures that you can populate the urbanization field when conditions are right"""
|
||||||
# Create a portfolio that cannot have this field
|
# Create a portfolio that cannot have this field
|
||||||
|
@ -2121,6 +2127,32 @@ class TestPortfolio(TestCase):
|
||||||
self.assertEqual(portfolio.urbanization, "test123")
|
self.assertEqual(portfolio.urbanization, "test123")
|
||||||
self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.PUERTO_RICO)
|
self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.PUERTO_RICO)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_organization_name_updates_for_federal_agency(self):
|
||||||
|
# Create a Portfolio instance with a federal agency
|
||||||
|
portfolio = Portfolio(
|
||||||
|
creator=self.user,
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
federal_agency=self.federal_agency,
|
||||||
|
)
|
||||||
|
portfolio.save()
|
||||||
|
|
||||||
|
# Assert that organization_name is updated to the federal agency's name
|
||||||
|
self.assertEqual(portfolio.organization_name, "Federal Agency")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_organization_name_does_not_update_for_non_federal_agency(self):
|
||||||
|
# Create a Portfolio instance with a non-federal agency
|
||||||
|
portfolio = Portfolio(
|
||||||
|
creator=self.user,
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
federal_agency=self.non_federal_agency,
|
||||||
|
)
|
||||||
|
portfolio.save()
|
||||||
|
|
||||||
|
# Assert that organization_name remains None
|
||||||
|
self.assertIsNone(portfolio.organization_name)
|
||||||
|
|
||||||
|
|
||||||
class TestAllowedEmail(TestCase):
|
class TestAllowedEmail(TestCase):
|
||||||
"""Tests our allowed email whitelist"""
|
"""Tests our allowed email whitelist"""
|
||||||
|
|
|
@ -16,7 +16,9 @@ from registrar.models import (
|
||||||
AllowedEmail,
|
AllowedEmail,
|
||||||
Portfolio,
|
Portfolio,
|
||||||
Suborganization,
|
Suborganization,
|
||||||
|
UserPortfolioPermission,
|
||||||
)
|
)
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
|
|
||||||
import boto3_mocking
|
import boto3_mocking
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
|
@ -46,6 +48,14 @@ class TestDomainRequest(TestCase):
|
||||||
self.dummy_user_2, _ = User.objects.get_or_create(
|
self.dummy_user_2, _ = User.objects.get_or_create(
|
||||||
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World"
|
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.dummy_user_3, _ = User.objects.get_or_create(
|
||||||
|
username="portfolioadmin@igorville.com",
|
||||||
|
email="portfolioadmin@igorville.com",
|
||||||
|
first_name="Portfolio",
|
||||||
|
last_name="Admin",
|
||||||
|
)
|
||||||
|
|
||||||
self.started_domain_request = completed_domain_request(
|
self.started_domain_request = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
name="started.gov",
|
name="started.gov",
|
||||||
|
@ -273,7 +283,14 @@ class TestDomainRequest(TestCase):
|
||||||
self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED)
|
self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED)
|
||||||
|
|
||||||
def check_email_sent(
|
def check_email_sent(
|
||||||
self, domain_request, msg, action, expected_count, expected_content=None, expected_email="mayor@igorville.com"
|
self,
|
||||||
|
domain_request,
|
||||||
|
msg,
|
||||||
|
action,
|
||||||
|
expected_count,
|
||||||
|
expected_content=None,
|
||||||
|
expected_email="mayor@igorville.com",
|
||||||
|
expected_cc=[],
|
||||||
):
|
):
|
||||||
"""Check if an email was sent after performing an action."""
|
"""Check if an email was sent after performing an action."""
|
||||||
email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email)
|
email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email)
|
||||||
|
@ -292,6 +309,11 @@ class TestDomainRequest(TestCase):
|
||||||
]
|
]
|
||||||
self.assertEqual(len(sent_emails), expected_count)
|
self.assertEqual(len(sent_emails), expected_count)
|
||||||
|
|
||||||
|
if expected_cc:
|
||||||
|
sent_cc_adddresses = sent_emails[0]["kwargs"]["Destination"]["CcAddresses"]
|
||||||
|
for cc_address in expected_cc:
|
||||||
|
self.assertIn(cc_address, sent_cc_adddresses)
|
||||||
|
|
||||||
if expected_content:
|
if expected_content:
|
||||||
email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"]
|
email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn(expected_content, email_content)
|
self.assertIn(expected_content, email_content)
|
||||||
|
@ -1074,6 +1096,36 @@ class TestDomainRequest(TestCase):
|
||||||
self.assertEqual(domain_request2.generic_org_type, domain_request2.converted_generic_org_type)
|
self.assertEqual(domain_request2.generic_org_type, domain_request2.converted_generic_org_type)
|
||||||
self.assertEqual(domain_request2.federal_agency, domain_request2.converted_federal_agency)
|
self.assertEqual(domain_request2.federal_agency, domain_request2.converted_federal_agency)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_portfolio_domain_requests_cc_requests_viewers(self):
|
||||||
|
"""test that portfolio domain request emails cc portfolio members who have read requests access"""
|
||||||
|
fed_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first()
|
||||||
|
portfolio = Portfolio.objects.create(
|
||||||
|
organization_name="Test Portfolio",
|
||||||
|
creator=self.dummy_user_2,
|
||||||
|
federal_agency=fed_agency,
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
)
|
||||||
|
user_portfolio_permission = UserPortfolioPermission.objects.create( # noqa: F841
|
||||||
|
user=self.dummy_user_3, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
# Adds cc'ed email in this test's allow list
|
||||||
|
AllowedEmail.objects.create(email="portfolioadmin@igorville.com")
|
||||||
|
|
||||||
|
msg = "Create a domain request and submit it and see if email cc's portfolio admin and members who can view \
|
||||||
|
requests."
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
name="test.gov", user=self.dummy_user_2, portfolio=portfolio, organization_name="Test Portfolio"
|
||||||
|
)
|
||||||
|
self.check_email_sent(
|
||||||
|
domain_request,
|
||||||
|
msg,
|
||||||
|
"submit",
|
||||||
|
1,
|
||||||
|
expected_email="intern@igorville.com",
|
||||||
|
expected_cc=["portfolioadmin@igorville.com"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainRequestSuborganization(TestCase):
|
class TestDomainRequestSuborganization(TestCase):
|
||||||
"""Tests for the suborganization fields on domain requests"""
|
"""Tests for the suborganization fields on domain requests"""
|
||||||
|
|
|
@ -255,10 +255,10 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"Organization name,City,State,SO,SO email,"
|
"Organization name,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,"
|
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
|
||||||
"Portfolio 1 Federal Agency,,,, ,,(blank),"
|
"Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||||
"meoward@rocks.com,squeaker@rocks.com\n"
|
"meoward@rocks.com,squeaker@rocks.com\n"
|
||||||
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
|
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
|
||||||
"Portfolio 1 Federal Agency,,,, ,,(blank),"
|
"Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||||
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
||||||
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
|
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
|
||||||
"World War I Centennial Commission,,,, ,,(blank),"
|
"World War I Centennial Commission,,,, ,,(blank),"
|
||||||
|
@ -280,6 +280,7 @@ 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
|
||||||
|
@ -316,9 +317,11 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
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,Security contact email,Domain managers,Invited domain managers\n"
|
"City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
|
||||||
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
|
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,"
|
||||||
|
"Portfolio 1 Federal Agency,,, ,,(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),"
|
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,"
|
||||||
|
"Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||||
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -326,6 +329,7 @@ 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
|
||||||
|
@ -587,7 +591,7 @@ 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,Portfolio1FederalAgency,Ready,(blank)\n"
|
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Portfolio1FederalAgency,Ready,(blank)\n"
|
||||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
||||||
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
|
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
|
||||||
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
||||||
|
@ -601,6 +605,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
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
|
||||||
|
@ -780,9 +785,11 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"city5.gov,Approved,Federal,No,Executive,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0,"
|
"city5.gov,Approved,Federal,No,Executive,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0,"
|
||||||
"city1.gov,Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,There is more,"
|
"city1.gov,Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,There is more,"
|
||||||
"Testy Tester testy2@town.com,,city.com,\n"
|
"Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,SubOrg 1,,,,,,,0,"
|
"city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,"
|
||||||
"1,city1.gov,,,,,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
"N/A,,,2,SubOrg 1,,,,,,,0,1,city1.gov,,,,,Purpose of the site,There is more,"
|
||||||
"city3.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,,,,,0,1,"
|
"Testy Tester testy2@town.com,,city.com,\n"
|
||||||
|
"city3.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,"
|
||||||
|
"N/A,,,2,,,,,,,,0,1,"
|
||||||
'"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
|
'"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
|
||||||
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
||||||
'Testy Tester testy2@town.com",'
|
'Testy Tester testy2@town.com",'
|
||||||
|
@ -792,9 +799,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"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 testy2@town.com,"
|
"Testy Tester testy2@town.com,"
|
||||||
"cisaRep@igorville.gov,city.com,\n"
|
"cisaRep@igorville.gov,city.com,\n"
|
||||||
"city6.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,,,,,0,1,city1.gov,"
|
"city6.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,N/A,"
|
||||||
",,,,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
|
",,2,,,,,,,,0,1,city1.gov,,,,,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||||
"cisaRep@igorville.gov,city.com,\n"
|
"Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
|
|
|
@ -36,7 +36,7 @@ def send_templated_email( # noqa
|
||||||
|
|
||||||
to_address and bcc_address currently only support single addresses.
|
to_address and bcc_address currently only support single addresses.
|
||||||
|
|
||||||
cc_address is a list and can contain many addresses. Emails not in the
|
cc_addresses is a list and can contain many addresses. Emails not in the
|
||||||
whitelist (if applicable) will be filtered out before sending.
|
whitelist (if applicable) will be filtered out before sending.
|
||||||
|
|
||||||
template_name and subject_template_name are relative to the same template
|
template_name and subject_template_name are relative to the same template
|
||||||
|
|
|
@ -1336,6 +1336,8 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
|
||||||
# Is the user deleting themselves? If so, display a different message
|
# Is the user deleting themselves? If so, display a different message
|
||||||
delete_self = self.request.user == self.object.user
|
delete_self = self.request.user == self.object.user
|
||||||
|
|
||||||
|
# Email domain managers
|
||||||
|
|
||||||
# Add a success message
|
# Add a success message
|
||||||
messages.success(self.request, self.get_success_message(delete_self))
|
messages.success(self.request, self.get_success_message(delete_self))
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue