mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 17:47:02 +02:00
Merge branch 'main' of https://github.com/cisagov/manage.get.gov into nobox/update-the-readme-for-feature-flag-portfolio-permissions
This commit is contained in:
commit
22e56fd063
38 changed files with 1650 additions and 473 deletions
36
.github/ISSUE_TEMPLATE/sub-issue.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/sub-issue.yml
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
name: Sub-issue
|
||||||
|
description: Describe an idea, problem, or feature that is related to a parent issue.
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
id: title-help
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> Titles should be short, descriptive, and compelling. Use sentence case (don't capitalize unnecessarily).
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Sub-issue description
|
||||||
|
description: |
|
||||||
|
Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and [good formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax).
|
||||||
|
|
||||||
|
For stories, use the user story format (e.g., As a user, I want, So that).
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: acceptance-criteria
|
||||||
|
attributes:
|
||||||
|
label: Acceptance criteria
|
||||||
|
description: If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate.
|
||||||
|
placeholder: "- [ ]"
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome."
|
||||||
|
- type: markdown
|
||||||
|
id: note
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> We may edit the text in this issue to document our understanding and clarify the product work.
|
||||||
|
|
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:
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -157,15 +157,15 @@ export function initAddNewMemberPageListeners() {
|
||||||
|
|
||||||
// Populate permission details based on access level
|
// Populate permission details based on access level
|
||||||
if (selectedAccess && selectedAccess.value === 'admin') {
|
if (selectedAccess && selectedAccess.value === 'admin') {
|
||||||
populatePermissionDetails('new-member-admin-permissions')
|
populatePermissionDetails('new-member-admin-permissions');
|
||||||
} else {
|
} else {
|
||||||
populatePermissionDetails('new-member-basic-permissions')
|
populatePermissionDetails('new-member-basic-permissions');
|
||||||
}
|
}
|
||||||
|
|
||||||
//------- Show the modal
|
//------- Show the modal
|
||||||
let modalTrigger = document.querySelector("#invite_member_trigger");
|
let modalTrigger = document.querySelector("#invite_member_trigger");
|
||||||
if (modalTrigger) {
|
if (modalTrigger) {
|
||||||
modalTrigger.click()
|
modalTrigger.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
|
import { hideElement, showElement } from './helpers.js';
|
||||||
|
|
||||||
function setupUrbanizationToggle(stateTerritoryField) {
|
function setupUrbanizationToggle(stateTerritoryField) {
|
||||||
var urbanizationField = document.getElementById('urbanization-field');
|
let urbanizationField = document.getElementById('urbanization-field');
|
||||||
|
if (!urbanizationField) {
|
||||||
|
console.error("Cannot find expect field: #urbanization-field");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleUrbanizationField() {
|
function toggleUrbanizationField() {
|
||||||
// Checking specifically for Puerto Rico only
|
// Checking specifically for Puerto Rico only
|
||||||
if (stateTerritoryField.value === 'PR') {
|
if (stateTerritoryField.value === 'PR') {
|
||||||
urbanizationField.style.display = 'block';
|
showElement(urbanizationField);
|
||||||
} else {
|
} else {
|
||||||
urbanizationField.style.display = 'none';
|
hideElement(urbanizationField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -295,7 +295,6 @@ class Command(BaseCommand):
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"Could not load additional TransitionDomain data. {err}")
|
logger.error(f"Could not load additional TransitionDomain data. {err}")
|
||||||
raise err
|
raise err
|
||||||
# TODO: handle this better...needs more logging
|
|
||||||
|
|
||||||
def handle( # noqa: C901
|
def handle( # noqa: C901
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-11-27 21:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0138_alter_domaininvitation_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="action_needed_reason",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("eligibility_unclear", "Unclear organization eligibility"),
|
||||||
|
("questionable_senior_official", "Questionable senior official"),
|
||||||
|
("already_has_a_domain", "Already has a domain"),
|
||||||
|
("bad_name", "Doesn’t meet naming requirements"),
|
||||||
|
("other", "Other (no auto-email sent)"),
|
||||||
|
],
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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()
|
||||||
|
|
|
@ -280,7 +280,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
|
|
||||||
ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility")
|
ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility")
|
||||||
QUESTIONABLE_SENIOR_OFFICIAL = ("questionable_senior_official", "Questionable senior official")
|
QUESTIONABLE_SENIOR_OFFICIAL = ("questionable_senior_official", "Questionable senior official")
|
||||||
ALREADY_HAS_DOMAINS = ("already_has_domains", "Already has domains")
|
ALREADY_HAS_A_DOMAIN = ("already_has_a_domain", "Already has a domain")
|
||||||
BAD_NAME = ("bad_name", "Doesn’t meet naming requirements")
|
BAD_NAME = ("bad_name", "Doesn’t meet naming requirements")
|
||||||
OTHER = ("other", "Other (no auto-email sent)")
|
OTHER = ("other", "Other (no auto-email sent)")
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
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))
|
||||||
|
|
|
@ -65,6 +65,7 @@ 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
|
from django.forms import ValidationError
|
||||||
|
@ -2452,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"""
|
||||||
|
@ -2506,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")
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
domain_request.action_needed()
|
domain_request.action_needed()
|
||||||
domain_request.action_needed_reason = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
domain_request.action_needed_reason = DomainRequest.ActionNeededReasons.ALREADY_HAS_A_DOMAIN
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
# Let's just change the action needed reason
|
# Let's just change the action needed reason
|
||||||
|
@ -230,7 +230,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"In review",
|
"In review",
|
||||||
"Rejected - Purpose requirements not met",
|
"Rejected - Purpose requirements not met",
|
||||||
"Action needed - Unclear organization eligibility",
|
"Action needed - Unclear organization eligibility",
|
||||||
"Action needed - Already has domains",
|
"Action needed - Already has a domain",
|
||||||
"In review",
|
"In review",
|
||||||
"Submitted",
|
"Submitted",
|
||||||
"Started",
|
"Started",
|
||||||
|
@ -241,7 +241,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
assert_status_count(normalized_content, "Started", 1)
|
assert_status_count(normalized_content, "Started", 1)
|
||||||
assert_status_count(normalized_content, "Submitted", 1)
|
assert_status_count(normalized_content, "Submitted", 1)
|
||||||
assert_status_count(normalized_content, "In review", 2)
|
assert_status_count(normalized_content, "In review", 2)
|
||||||
assert_status_count(normalized_content, "Action needed - Already has domains", 1)
|
assert_status_count(normalized_content, "Action needed - Already has a domain", 1)
|
||||||
assert_status_count(normalized_content, "Action needed - Unclear organization eligibility", 1)
|
assert_status_count(normalized_content, "Action needed - Unclear organization eligibility", 1)
|
||||||
assert_status_count(normalized_content, "Rejected - Purpose requirements not met", 1)
|
assert_status_count(normalized_content, "Rejected - Purpose requirements not met", 1)
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -685,9 +685,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Create a sample domain request
|
# Create a sample domain request
|
||||||
domain_request = completed_domain_request(status=in_review, user=_creator)
|
domain_request = completed_domain_request(status=in_review, user=_creator)
|
||||||
|
|
||||||
# Test the email sent out for already_has_domains
|
# Test the email sent out for already_has_a_domain
|
||||||
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
already_has_a_domain = DomainRequest.ActionNeededReasons.ALREADY_HAS_A_DOMAIN
|
||||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains)
|
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_a_domain)
|
||||||
|
|
||||||
self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
@ -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
|
||||||
|
@ -880,18 +874,26 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"Email,Organization admin,Invited by,Joined date,Last active,Domain requests,"
|
"Email,Organization admin,Invited by,Joined date,Last active,Domain requests,"
|
||||||
"Member management,Domain management,Number of domains,Domains\n"
|
"Member management,Domain management,Number of domains,Domains\n"
|
||||||
# Content
|
# Content
|
||||||
|
"big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,"
|
||||||
|
"Viewer,True,1,cdomain1.gov\n"
|
||||||
|
"cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,"
|
||||||
|
"Viewer,Viewer,False,0,\n"
|
||||||
|
"icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,"
|
||||||
|
"Viewer Requester,Manager,False,0,\n"
|
||||||
"meoward@rocks.com,False,big_lebowski@dude.co,2022-04-01,Invalid date,None,"
|
"meoward@rocks.com,False,big_lebowski@dude.co,2022-04-01,Invalid date,None,"
|
||||||
'Manager,True,2,"adomain2.gov,cdomain1.gov"\n'
|
'Manager,True,2,"adomain2.gov,cdomain1.gov"\n'
|
||||||
"big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,Viewer,True,1,cdomain1.gov\n"
|
"nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,"
|
||||||
"tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,None,False,0,\n"
|
"None,Manager,False,0,\n"
|
||||||
"icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,Manager,False,0,\n"
|
"nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,"
|
||||||
"cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer,Viewer,False,0,\n"
|
"None,Viewer,False,0,\n"
|
||||||
"nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Manager,False,0,\n"
|
"nonexistentmember_3@igorville.gov,False,help@get.gov,Unretrieved,Invited,"
|
||||||
"nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Viewer,False,0,\n"
|
"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,Invited,"
|
||||||
"nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,"
|
"Viewer Requester,Manager,False,0,\n"
|
||||||
"Invited,Viewer Requester,Manager,False,0,\n"
|
"nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,"
|
||||||
"nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer,Viewer,False,0,\n"
|
"Viewer,Viewer,False,0,\n"
|
||||||
|
"tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,"
|
||||||
|
"None,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
|
||||||
|
|
|
@ -414,8 +414,11 @@ class MemberExport(BaseExport):
|
||||||
)
|
)
|
||||||
.values(*shared_columns)
|
.values(*shared_columns)
|
||||||
)
|
)
|
||||||
|
# Adding a order_by increases output predictability.
|
||||||
return convert_queryset_to_dict(permissions.union(invitations), is_model=False)
|
# Doesn't matter as much for normal use, but makes tests easier.
|
||||||
|
# We should also just be ordering by default anyway.
|
||||||
|
members = permissions.union(invitations).order_by("email_display")
|
||||||
|
return convert_queryset_to_dict(members, is_model=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_invited_by_query(cls, object_id_query):
|
def get_invited_by_query(cls, object_id_query):
|
||||||
|
@ -525,6 +528,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 +726,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 +752,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 +766,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 +790,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 +861,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 +879,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 +893,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 +936,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 +1041,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 +1092,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 +1106,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 +1146,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 +1179,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 +1193,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 +1234,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 +1611,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 +1808,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 +1860,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 +2015,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
|
||||||
|
|
|
@ -626,34 +626,3 @@ class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
||||||
if permission_exists:
|
if permission_exists:
|
||||||
messages.warning(self.request, "User is already a member of this portfolio.")
|
messages.warning(self.request, "User is already a member of this portfolio.")
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
# look up a user with that email
|
|
||||||
try:
|
|
||||||
requested_user = User.objects.get(email=requested_email)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
# no matching user, go make an invitation
|
|
||||||
return self._make_invitation(requested_email, requestor)
|
|
||||||
else:
|
|
||||||
# If user already exists, check to see if they are part of the portfolio already
|
|
||||||
# If they are already part of the portfolio, raise an error. Otherwise, send an invite.
|
|
||||||
existing_user = UserPortfolioPermission.objects.get(user=requested_user, portfolio=self.object)
|
|
||||||
if existing_user:
|
|
||||||
messages.warning(self.request, "User is already a member of this portfolio.")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self._send_portfolio_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.")
|
|
||||||
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()
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
10038 OUTOFSCOPE http://app:8080/permissions
|
10038 OUTOFSCOPE http://app:8080/permissions
|
||||||
10038 OUTOFSCOPE http://app:8080/suborganization/
|
10038 OUTOFSCOPE http://app:8080/suborganization/
|
||||||
10038 OUTOFSCOPE http://app:8080/transfer/
|
10038 OUTOFSCOPE http://app:8080/transfer/
|
||||||
|
10038 OUTOFSCOPE http://app:8080/prototype-dns
|
||||||
# This URL always returns 404, so include it as well.
|
# This URL always returns 404, so include it as well.
|
||||||
10038 OUTOFSCOPE http://app:8080/todo
|
10038 OUTOFSCOPE http://app:8080/todo
|
||||||
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue