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/**
|
||||
**/assets/*
|
||||
!**/assets/src/
|
||||
!**/assets/sass/
|
||||
!**/assets/src/js/
|
||||
!**/assets/src/sass/
|
||||
!**/assets/img/registrar/
|
||||
public/
|
||||
credentials*
|
||||
|
|
|
@ -378,3 +378,18 @@ Then, copy the variables under the section labled `s3`.
|
|||
## 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.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
#### 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`
|
||||
|
||||
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
|
||||
|
||||
#### 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
|
||||
| | Parameter | Description |
|
||||
|:-:|:-------------------------- |:-------------------------------------------------------------------------------------------|
|
||||
| 1 | **federal_agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
||||
| 2 | **both** | If True, runs parse_requests and parse_domains. |
|
||||
| 3 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
||||
| 4 | **parse_domains** | If True, then the created portfolio is added to all related Domains. |
|
||||
| 1 | **agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
||||
| 2 | **branch** | Creates a portfolio for each federal agency in a branch: executive, legislative, judicial |
|
||||
| 3 | **both** | If True, runs parse_requests and parse_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.
|
||||
|
|
|
@ -59,6 +59,9 @@ services:
|
|||
- AWS_S3_BUCKET_NAME
|
||||
# File encryption credentials
|
||||
- SECRET_ENCRYPT_METADATA
|
||||
- REGISTRY_TENANT_KEY
|
||||
- REGISTRY_SERVICE_EMAIL
|
||||
- REGISTRY_TENANT_NAME
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports:
|
||||
|
|
|
@ -3,7 +3,14 @@ import logging
|
|||
import copy
|
||||
from typing import Optional
|
||||
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.http import HttpResponseRedirect
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
|
@ -1467,21 +1474,57 @@ class DomainInformationResource(resources.ModelResource):
|
|||
class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
"""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]
|
||||
|
||||
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
|
||||
list_display = [
|
||||
"domain",
|
||||
"generic_org_type",
|
||||
"converted_generic_org_type",
|
||||
"created_at",
|
||||
]
|
||||
|
||||
orderable_fk_fields = [("domain", "name")]
|
||||
|
||||
# Filters
|
||||
list_filter = ["generic_org_type"]
|
||||
list_filter = [GenericOrgFilter]
|
||||
|
||||
# Search
|
||||
search_fields = [
|
||||
|
@ -1661,24 +1704,23 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
def lookups(self, request, model_admin):
|
||||
converted_generic_orgs = set()
|
||||
|
||||
# Populate the set with tuples of (value, display value)
|
||||
for domain_request in DomainRequest.objects.all():
|
||||
converted_generic_org = domain_request.converted_generic_org_type
|
||||
if converted_generic_org:
|
||||
converted_generic_orgs.add(converted_generic_org)
|
||||
converted_generic_org = domain_request.converted_generic_org_type # Actual value
|
||||
converted_generic_org_display = domain_request.converted_generic_org_type_display # Display value
|
||||
|
||||
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
|
||||
def queryset(self, request, queryset):
|
||||
if self.value(): # Check if a generic org is selected in the filter
|
||||
return queryset.filter(
|
||||
# Filter based on the generic org value returned by converted_generic_org_type
|
||||
id__in=[
|
||||
domain_request.id
|
||||
for domain_request in queryset
|
||||
if domain_request.converted_generic_org_type
|
||||
and domain_request.converted_generic_org_type == self.value()
|
||||
]
|
||||
Q(portfolio__organization_type=self.value())
|
||||
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
@ -1693,24 +1735,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
def lookups(self, request, model_admin):
|
||||
converted_federal_types = set()
|
||||
|
||||
# Populate the set with tuples of (value, display value)
|
||||
for domain_request in DomainRequest.objects.all():
|
||||
converted_federal_type = domain_request.converted_federal_type
|
||||
if converted_federal_type:
|
||||
converted_federal_types.add(converted_federal_type)
|
||||
converted_federal_type = domain_request.converted_federal_type # Actual value
|
||||
converted_federal_type_display = domain_request.converted_federal_type_display # Display value
|
||||
|
||||
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
|
||||
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(
|
||||
# Filter based on the federal type returned by converted_federal_type
|
||||
id__in=[
|
||||
domain_request.id
|
||||
for domain_request in queryset
|
||||
if domain_request.converted_federal_type
|
||||
and domain_request.converted_federal_type == self.value()
|
||||
]
|
||||
Q(portfolio__federal_agency__federal_type=self.value())
|
||||
| Q(portfolio__isnull=True, federal_type=self.value())
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
@ -1776,7 +1819,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
@admin.display(description=_("Generic Org Type"))
|
||||
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"))
|
||||
def converted_organization_name(self, obj):
|
||||
|
@ -1788,7 +1831,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
@admin.display(description=_("Federal Type"))
|
||||
def converted_federal_type(self, obj):
|
||||
return obj.converted_federal_type
|
||||
return obj.converted_federal_type_display
|
||||
|
||||
@admin.display(description=_("City"))
|
||||
def converted_city(self, obj):
|
||||
|
@ -2679,6 +2722,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
resource_classes = [DomainResource]
|
||||
|
||||
# ------- FILTERS
|
||||
class ElectionOfficeFilter(admin.SimpleListFilter):
|
||||
"""Define a custom filter for is_election_board"""
|
||||
|
||||
|
@ -2697,18 +2741,135 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if self.value() == "0":
|
||||
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]
|
||||
|
||||
# Columns
|
||||
list_display = [
|
||||
"name",
|
||||
"generic_org_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"organization_name",
|
||||
"converted_generic_org_type",
|
||||
"converted_federal_type",
|
||||
"converted_federal_agency",
|
||||
"converted_organization_name",
|
||||
"custom_election_board",
|
||||
"city",
|
||||
"state_territory",
|
||||
"converted_city",
|
||||
"converted_state_territory",
|
||||
"state",
|
||||
"expiration_date",
|
||||
"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):
|
||||
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):
|
||||
if obj.domain_info:
|
||||
return obj.domain_info.federal_agency
|
||||
else:
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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.short_description = "Election office" # type: ignore
|
||||
|
||||
def city(self, obj):
|
||||
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
|
||||
search_fields = ["name"]
|
||||
search_help_text = "Search by domain name."
|
||||
|
||||
# Change Form
|
||||
change_form_template = "django/admin/domain_change_form.html"
|
||||
|
||||
# Readonly Fields
|
||||
readonly_fields = (
|
||||
"state",
|
||||
"expiration_date",
|
||||
|
@ -3058,7 +3263,8 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
def get_queryset(self, request):
|
||||
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
||||
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
|
||||
portfolio_id = request.GET.get("portfolio")
|
||||
if portfolio_id:
|
||||
|
@ -3579,6 +3785,14 @@ class WaffleFlagAdmin(FlagAdmin):
|
|||
model = models.WaffleFlag
|
||||
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):
|
||||
list_display = ["name", "portfolio"]
|
||||
|
|
|
@ -157,15 +157,15 @@ export function initAddNewMemberPageListeners() {
|
|||
|
||||
// Populate permission details based on access level
|
||||
if (selectedAccess && selectedAccess.value === 'admin') {
|
||||
populatePermissionDetails('new-member-admin-permissions')
|
||||
populatePermissionDetails('new-member-admin-permissions');
|
||||
} else {
|
||||
populatePermissionDetails('new-member-basic-permissions')
|
||||
populatePermissionDetails('new-member-basic-permissions');
|
||||
}
|
||||
|
||||
//------- Show the modal
|
||||
let modalTrigger = document.querySelector("#invite_member_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click()
|
||||
modalTrigger.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { hideElement, showElement } from './helpers.js';
|
||||
|
||||
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() {
|
||||
// Checking specifically for Puerto Rico only
|
||||
if (stateTerritoryField.value === 'PR') {
|
||||
urbanizationField.style.display = 'block';
|
||||
showElement(urbanizationField);
|
||||
} 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_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-----------------------------------------------###
|
||||
|
||||
# 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_PASSPHRASE = secret_registry_key_passphrase
|
||||
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
|
||||
# region: Security and Privacy----------------------------------------------###
|
||||
|
|
|
@ -46,8 +46,8 @@ DOMAIN_REQUEST_NAMESPACE = views.DomainRequestWizard.URL_NAMESPACE
|
|||
# dynamically generate the other domain_request_urls
|
||||
domain_request_urls = [
|
||||
path("", RedirectView.as_view(pattern_name="domain-request:start"), name="redirect-to-start"),
|
||||
path("start/", views.DomainRequestWizard.as_view(), name="start"),
|
||||
path("finished/", views.Finished.as_view(), name="finished"),
|
||||
path("start/", views.DomainRequestWizard.as_view(), name=views.DomainRequestWizard.NEW_URL_NAME),
|
||||
path("finished/", views.Finished.as_view(), name=views.DomainRequestWizard.FINISHED_URL_NAME),
|
||||
]
|
||||
for step, view in [
|
||||
# add/remove steps here
|
||||
|
@ -255,11 +255,6 @@ urlpatterns = [
|
|||
ExportDataTypeRequests.as_view(),
|
||||
name="export_data_type_requests",
|
||||
),
|
||||
path(
|
||||
"reports/export_data_type_requests/",
|
||||
ExportDataTypeRequests.as_view(),
|
||||
name="export_data_type_requests",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:id>/edit/",
|
||||
views.DomainRequestWizard.as_view(),
|
||||
|
@ -298,6 +293,7 @@ urlpatterns = [
|
|||
name="todo",
|
||||
),
|
||||
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>/dns",
|
||||
|
|
|
@ -527,7 +527,12 @@ class DotGovDomainForm(RegistrarForm):
|
|||
class PurposeForm(RegistrarForm):
|
||||
purpose = forms.CharField(
|
||||
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=[
|
||||
MaxLengthValidator(
|
||||
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):
|
||||
"""Yes/no toggle for the anything else question on additional details"""
|
||||
|
||||
|
|
|
@ -13,16 +13,29 @@ logger = logging.getLogger(__name__)
|
|||
class Command(BaseCommand):
|
||||
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):
|
||||
"""Add three arguments:
|
||||
1. agency_name => the value of FederalAgency.agency
|
||||
2. --parse_requests => if true, adds the given portfolio to each related DomainRequest
|
||||
3. --parse_domains => if true, adds the given portfolio to each related DomainInformation
|
||||
"""
|
||||
parser.add_argument(
|
||||
"agency_name",
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
"--agency_name",
|
||||
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(
|
||||
"--parse_requests",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
|
@ -39,7 +52,9 @@ class Command(BaseCommand):
|
|||
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_domains = options.get("parse_domains")
|
||||
both = options.get("both")
|
||||
|
@ -51,84 +66,94 @@ class Command(BaseCommand):
|
|||
if parse_requests or parse_domains:
|
||||
raise CommandError("You cannot pass --parse_requests or --parse_domains when passing --both.")
|
||||
|
||||
federal_agency = FederalAgency.objects.filter(agency__iexact=agency_name).first()
|
||||
if not federal_agency:
|
||||
raise ValueError(
|
||||
f"Cannot find the federal agency '{agency_name}' in our database. "
|
||||
"The value you enter for `agency_name` must be "
|
||||
"prepopulated in the FederalAgency table before proceeding."
|
||||
)
|
||||
federal_agency_filter = {"agency__iexact": agency_name} if agency_name else {"federal_type": branch}
|
||||
agencies = FederalAgency.objects.filter(**federal_agency_filter)
|
||||
if not agencies or agencies.count() < 1:
|
||||
if agency_name:
|
||||
raise CommandError(
|
||||
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)
|
||||
self.create_suborganizations(portfolio, federal_agency)
|
||||
for federal_agency in agencies:
|
||||
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:
|
||||
self.handle_portfolio_requests(portfolio, federal_agency)
|
||||
|
||||
if parse_domains or both:
|
||||
self.handle_portfolio_domains(portfolio, federal_agency)
|
||||
def create_portfolio(self, 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):
|
||||
"""Creates or modifies a portfolio record based on a federal agency."""
|
||||
portfolio_args = {
|
||||
"federal_agency": federal_agency,
|
||||
"organization_name": federal_agency.agency,
|
||||
"organization_type": DomainRequest.OrganizationChoices.FEDERAL,
|
||||
"creator": User.get_default_user(),
|
||||
"notes": "Auto-generated record",
|
||||
}
|
||||
# First just try to get an existing portfolio
|
||||
portfolio = Portfolio.objects.filter(organization_name=org_name).first()
|
||||
if portfolio:
|
||||
self.skipped_portfolios.add(portfolio)
|
||||
TerminalHelper.colorful_logger(
|
||||
logger.info,
|
||||
TerminalColors.YELLOW,
|
||||
f"Portfolio with organization name '{org_name}' already exists. Skipping create.",
|
||||
)
|
||||
return portfolio, False
|
||||
|
||||
if federal_agency.so_federal_agency.exists():
|
||||
portfolio_args["senior_official"] = federal_agency.so_federal_agency.first()
|
||||
|
||||
portfolio, created = Portfolio.objects.get_or_create(
|
||||
organization_name=portfolio_args.get("organization_name"), defaults=portfolio_args
|
||||
# Create new portfolio if it doesn't exist
|
||||
portfolio = Portfolio.objects.create(
|
||||
organization_name=org_name,
|
||||
federal_agency=federal_agency,
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
creator=User.get_default_user(),
|
||||
notes="Auto-generated record",
|
||||
senior_official=so,
|
||||
)
|
||||
|
||||
if created:
|
||||
message = f"Created portfolio '{portfolio}'"
|
||||
self.updated_portfolios.add(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)
|
||||
|
||||
if portfolio_args.get("senior_official"):
|
||||
message = f"Added senior official '{portfolio_args['senior_official']}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||
else:
|
||||
message = (
|
||||
f"No senior official added to portfolio '{portfolio}'. "
|
||||
"None was returned for the reverse relation `FederalAgency.so_federal_agency.first()`"
|
||||
)
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||
else:
|
||||
proceed = TerminalHelper.prompt_for_execution(
|
||||
system_exit_on_terminate=False,
|
||||
prompt_message=f"""
|
||||
The given portfolio '{federal_agency.agency}' already exists in our DB.
|
||||
If you cancel, the rest of the script will still execute but this record will not update.
|
||||
""",
|
||||
prompt_title="Do you wish to modify this record?",
|
||||
message = (
|
||||
f"No senior official added to portfolio '{org_name}'. "
|
||||
"None was returned for the reverse relation `FederalAgency.so_federal_agency.first()`"
|
||||
)
|
||||
if proceed:
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||
|
||||
# Don't override the creator and notes fields
|
||||
if portfolio.creator:
|
||||
portfolio_args.pop("creator")
|
||||
|
||||
if portfolio.notes:
|
||||
portfolio_args.pop("notes")
|
||||
|
||||
# Update everything else
|
||||
for key, value in portfolio_args.items():
|
||||
setattr(portfolio, key, value)
|
||||
|
||||
portfolio.save()
|
||||
message = f"Modified portfolio '{portfolio}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
if portfolio_args.get("senior_official"):
|
||||
message = f"Added/modified senior official '{portfolio_args['senior_official']}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
return portfolio
|
||||
return portfolio, True
|
||||
|
||||
def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||
"""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)
|
||||
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)
|
||||
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
|
||||
new_suborgs = []
|
||||
|
@ -175,29 +201,6 @@ class Command(BaseCommand):
|
|||
else:
|
||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, "No suborganizations added")
|
||||
|
||||
def _update_existing_suborganizations(self, portfolio, orgs_to_update):
|
||||
"""
|
||||
Update existing suborganizations with new portfolio.
|
||||
Prompts for user confirmation before proceeding.
|
||||
"""
|
||||
proceed = TerminalHelper.prompt_for_execution(
|
||||
system_exit_on_terminate=False,
|
||||
prompt_message=f"""Some suborganizations already exist in our DB.
|
||||
If you cancel, the rest of the script will still execute but these records will not update.
|
||||
|
||||
==Proposed Changes==
|
||||
The following suborgs will be updated: {[org.name for org in orgs_to_update]}
|
||||
""",
|
||||
prompt_title="Do you wish to modify existing suborganizations?",
|
||||
)
|
||||
if proceed:
|
||||
for org in orgs_to_update:
|
||||
org.portfolio = portfolio
|
||||
|
||||
Suborganization.objects.bulk_update(orgs_to_update, ["portfolio"])
|
||||
message = f"Updated {len(orgs_to_update)} suborganizations."
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||
"""
|
||||
Associate portfolio with domain requests for a federal agency.
|
||||
|
@ -208,12 +211,17 @@ class Command(BaseCommand):
|
|||
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||
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():
|
||||
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.
|
||||
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)
|
||||
return None
|
||||
|
@ -224,6 +232,7 @@ class Command(BaseCommand):
|
|||
domain_request.portfolio = portfolio
|
||||
if domain_request.organization_name in suborgs:
|
||||
domain_request.sub_organization = suborgs.get(domain_request.organization_name)
|
||||
self.updated_portfolios.add(portfolio)
|
||||
|
||||
DomainRequest.objects.bulk_update(domain_requests, ["portfolio", "sub_organization"])
|
||||
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.
|
||||
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():
|
||||
message = f"""
|
||||
Portfolios not added to domains: no valid records found.
|
||||
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||
Portfolio '{portfolio}' not added to domains: no valid records found.
|
||||
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)
|
||||
return None
|
||||
|
@ -251,5 +261,5 @@ class Command(BaseCommand):
|
|||
domain_info.sub_organization = suborgs.get(domain_info.organization_name)
|
||||
|
||||
DomainInformation.objects.bulk_update(domain_infos, ["portfolio", "sub_organization"])
|
||||
message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains"
|
||||
message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains."
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||
|
|
|
@ -295,7 +295,6 @@ class Command(BaseCommand):
|
|||
except Exception as err:
|
||||
logger.error(f"Could not load additional TransitionDomain data. {err}")
|
||||
raise err
|
||||
# TODO: handle this better...needs more logging
|
||||
|
||||
def handle( # noqa: C901
|
||||
self,
|
||||
|
|
|
@ -192,7 +192,7 @@ class PopulateScriptTemplate(ABC):
|
|||
class TerminalHelper:
|
||||
@staticmethod
|
||||
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
|
||||
all affected objects."""
|
||||
|
@ -203,8 +203,21 @@ class TerminalHelper:
|
|||
if log_header is None:
|
||||
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
|
||||
if debug:
|
||||
if debug or display_detailed_logs:
|
||||
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
|
||||
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.
|
||||
# Otherwise, don't print anything.
|
||||
TerminalHelper.print_conditional(
|
||||
debug,
|
||||
True,
|
||||
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('failed') if update_failed_count > 0 else ''}",
|
||||
|
@ -236,7 +249,7 @@ class TerminalHelper:
|
|||
f"""{TerminalColors.YELLOW}
|
||||
{log_header}
|
||||
Updated {update_success_count} entries
|
||||
----- SOME DATA WAS INVALID (NEEDS MANUAL PATCHING) -----
|
||||
{skipped_header}
|
||||
Skipped updating {update_skipped_count} entries
|
||||
{TerminalColors.ENDC}
|
||||
"""
|
||||
|
@ -368,7 +381,9 @@ class TerminalHelper:
|
|||
logger.info(print_statement)
|
||||
|
||||
@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.
|
||||
Prompts the user to inspect the given string
|
||||
and asks if they wish to proceed.
|
||||
|
@ -380,6 +395,9 @@ class TerminalHelper:
|
|||
if system_exit_on_terminate:
|
||||
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
|
||||
# and ask if they wish to proceed
|
||||
proceed_execution = TerminalHelper.query_yes_no_exit(
|
||||
|
@ -387,7 +405,7 @@ class TerminalHelper:
|
|||
=====================================================
|
||||
{prompt_title}
|
||||
=====================================================
|
||||
*** IMPORTANT: VERIFY THE FOLLOWING LOOKS CORRECT ***
|
||||
{verify_message}
|
||||
|
||||
{prompt_message}
|
||||
{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
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||
|
||||
from django.db import models
|
||||
|
|
|
@ -426,13 +426,14 @@ class DomainInformation(TimeStampedModel):
|
|||
else:
|
||||
return None
|
||||
|
||||
# ----- Portfolio Properties -----
|
||||
|
||||
@property
|
||||
def converted_organization_name(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.organization_name
|
||||
return self.organization_name
|
||||
|
||||
# ----- Portfolio Properties -----
|
||||
@property
|
||||
def converted_generic_org_type(self):
|
||||
if self.portfolio:
|
||||
|
@ -442,8 +443,8 @@ class DomainInformation(TimeStampedModel):
|
|||
@property
|
||||
def converted_federal_agency(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_agency
|
||||
return self.federal_agency
|
||||
return self.portfolio.federal_agency.agency
|
||||
return self.federal_agency.agency
|
||||
|
||||
@property
|
||||
def converted_federal_type(self):
|
||||
|
@ -454,20 +455,20 @@ class DomainInformation(TimeStampedModel):
|
|||
@property
|
||||
def converted_senior_official(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.senior_official
|
||||
return self.senior_official
|
||||
return self.portfolio.display_senior_official
|
||||
return self.display_senior_official
|
||||
|
||||
@property
|
||||
def converted_address_line1(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.address_line1
|
||||
return self.address_line1
|
||||
return self.portfolio.display_address_line1
|
||||
return self.display_address_line1
|
||||
|
||||
@property
|
||||
def converted_address_line2(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.address_line2
|
||||
return self.address_line2
|
||||
return self.portfolio.display_address_line2
|
||||
return self.display_address_line2
|
||||
|
||||
@property
|
||||
def converted_city(self):
|
||||
|
@ -478,17 +479,30 @@ class DomainInformation(TimeStampedModel):
|
|||
@property
|
||||
def converted_state_territory(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.state_territory
|
||||
return self.state_territory
|
||||
return self.portfolio.get_state_territory_display()
|
||||
return self.get_state_territory_display()
|
||||
|
||||
@property
|
||||
def converted_zipcode(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.zipcode
|
||||
return self.zipcode
|
||||
return self.portfolio.display_zipcode
|
||||
return self.display_zipcode
|
||||
|
||||
@property
|
||||
def converted_urbanization(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.urbanization
|
||||
return self.urbanization
|
||||
return self.portfolio.display_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")
|
||||
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")
|
||||
OTHER = ("other", "Other (no auto-email sent)")
|
||||
|
||||
|
@ -1437,6 +1437,18 @@ class DomainRequest(TimeStampedModel):
|
|||
return self.portfolio.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
|
||||
def converted_city(self):
|
||||
if self.portfolio:
|
||||
|
@ -1449,8 +1461,33 @@ class DomainRequest(TimeStampedModel):
|
|||
return self.portfolio.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
|
||||
def converted_senior_official(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.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()
|
||||
|
|
|
@ -29,12 +29,16 @@
|
|||
|
||||
<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 %}
|
||||
<ul class="usa-list">
|
||||
<li><a href="{{ url }}">Name servers</a></li>
|
||||
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
<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>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -37,12 +37,9 @@
|
|||
{% input_with_errors forms.0.zipcode %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="urbanization-field" style="display: none;">
|
||||
<div id="urbanization-field" class="display-none">
|
||||
{% input_with_errors forms.0.urbanization %}
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
{% endblock %}
|
||||
|
||||
<script src="{% static 'js/getgov.min.js' %}" defer></script>
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% 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 %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
{% load static field_helpers %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{% include "includes/required_fields.html" %}
|
||||
{% comment %} Empty - this step is not required {% endcomment %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<h2>Is there anything else you’d like us to know about your domain request?</h2>
|
||||
<fieldset class="usa-fieldset">
|
||||
<h2 class="margin-top-0 margin-bottom-0">Is there anything else you’d like us to know about your domain request?</h2>
|
||||
</legend>
|
||||
</fieldset>
|
||||
|
||||
<div class="margin-top-3" id="anything-else">
|
||||
<p><em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em></p>
|
||||
<div id="anything-else">
|
||||
<p><em>This question is optional.</em></p>
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.anything_else %}
|
||||
{% 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_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(
|
||||
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))
|
||||
|
|
|
@ -65,6 +65,7 @@ from .common import (
|
|||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib import messages
|
||||
|
||||
from unittest.mock import ANY, patch, Mock
|
||||
from django.forms import ValidationError
|
||||
|
@ -2452,6 +2453,33 @@ class TestTransferUser(WebTest):
|
|||
|
||||
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
|
||||
def test_transfer_user_transfers_domain_request_creator_and_investigator(self):
|
||||
"""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_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
|
||||
def test_transfer_user_transfers_verified_by_staff_requestor(self):
|
||||
"""Assert that verified by staff creator gets transferred"""
|
||||
|
|
|
@ -728,9 +728,9 @@ class TestDomainAdminWithClient(TestCase):
|
|||
response = self.client.get("/admin/registrar/domain/")
|
||||
# There are 4 template references to Federal (4) plus four references in the table
|
||||
# for our actual domain_request
|
||||
self.assertContains(response, "Federal", count=56)
|
||||
self.assertContains(response, "Federal", count=57)
|
||||
# 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
|
||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||
|
||||
|
|
|
@ -203,7 +203,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
domain_request.save()
|
||||
|
||||
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()
|
||||
|
||||
# Let's just change the action needed reason
|
||||
|
@ -230,7 +230,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"In review",
|
||||
"Rejected - Purpose requirements not met",
|
||||
"Action needed - Unclear organization eligibility",
|
||||
"Action needed - Already has domains",
|
||||
"Action needed - Already has a domain",
|
||||
"In review",
|
||||
"Submitted",
|
||||
"Started",
|
||||
|
@ -241,7 +241,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
assert_status_count(normalized_content, "Started", 1)
|
||||
assert_status_count(normalized_content, "Submitted", 1)
|
||||
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, "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")
|
||||
# There are 2 template references to Federal (4) and two in the results data
|
||||
# of the request
|
||||
self.assertContains(response, "Federal", count=51)
|
||||
self.assertContains(response, "Federal", count=55)
|
||||
# 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
|
||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||
|
||||
|
@ -685,9 +685,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=in_review, user=_creator)
|
||||
|
||||
# Test the email sent out for already_has_domains
|
||||
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains)
|
||||
# Test the email sent out for already_has_a_domain
|
||||
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_a_domain)
|
||||
|
||||
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)
|
||||
|
@ -1693,7 +1693,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"notes",
|
||||
"alternative_domains",
|
||||
]
|
||||
self.maxDiff = None
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_readonly_fields_for_analyst(self):
|
||||
|
@ -1702,7 +1701,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
request.user = self.staffuser
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
self.maxDiff = None
|
||||
expected_fields = [
|
||||
"portfolio_senior_official",
|
||||
"portfolio_organization_type",
|
||||
|
|
|
@ -1421,10 +1421,41 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
def setUp(self):
|
||||
self.mock_client = MockSESClient()
|
||||
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")
|
||||
|
||||
# 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(
|
||||
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):
|
||||
self.domain_request = completed_domain_request(
|
||||
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_request_2 = completed_domain_request(
|
||||
name="sock@igorville.org",
|
||||
name="icecreamforigorville.gov",
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
federal_agency=self.federal_agency,
|
||||
|
@ -1446,6 +1477,28 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
self.domain_request_2.approve()
|
||||
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):
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
|
@ -1456,18 +1509,16 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
User.objects.all().delete()
|
||||
|
||||
@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(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
|
||||
return_value=True,
|
||||
):
|
||||
call_command(
|
||||
"create_federal_portfolio", agency_name, parse_requests=parse_requests, parse_domains=parse_domains
|
||||
)
|
||||
call_command("create_federal_portfolio", **kwargs)
|
||||
|
||||
def test_create_or_modify_portfolio(self):
|
||||
"""Test portfolio creation and modification with suborg and senior official."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||
def test_create_single_portfolio(self):
|
||||
"""Test portfolio creation with suborg and senior official."""
|
||||
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True)
|
||||
|
||||
portfolio = Portfolio.objects.get(federal_agency=self.federal_agency)
|
||||
self.assertEqual(portfolio.organization_name, self.federal_agency.agency)
|
||||
|
@ -1483,9 +1534,125 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
# Test the 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):
|
||||
"""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.assertIsNotNone(self.domain_request.portfolio)
|
||||
|
@ -1494,7 +1661,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
|
||||
def test_handle_portfolio_domains(self):
|
||||
"""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.assertIsNotNone(self.domain_info.portfolio)
|
||||
|
@ -1503,7 +1670,7 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
|
||||
def test_handle_parse_both(self):
|
||||
"""Ensure correct parsing of both requests and domains."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True, parse_domains=True)
|
||||
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True, parse_domains=True)
|
||||
|
||||
self.domain_request.refresh_from_db()
|
||||
self.domain_info.refresh_from_db()
|
||||
|
@ -1511,12 +1678,26 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
self.assertIsNotNone(self.domain_info.portfolio)
|
||||
self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio)
|
||||
|
||||
def test_command_error_no_parse_options(self):
|
||||
"""Verify error when no parse options are provided."""
|
||||
def test_command_error_parse_options(self):
|
||||
"""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(
|
||||
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):
|
||||
"""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. "
|
||||
"The value you enter for `agency_name` must be prepopulated in the FederalAgency table before proceeding."
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, expected_message):
|
||||
self.run_create_federal_portfolio("Non-existent Agency", parse_requests=True)
|
||||
with self.assertRaisesRegex(CommandError, expected_message):
|
||||
self.run_create_federal_portfolio(agency_name="Non-existent Agency", parse_requests=True)
|
||||
|
||||
def test_update_existing_portfolio(self):
|
||||
"""Test updating an existing portfolio."""
|
||||
def test_does_not_update_existing_portfolio(self):
|
||||
"""Tests that an existing portfolio is not updated"""
|
||||
# Create an existing portfolio
|
||||
existing_portfolio = Portfolio.objects.create(
|
||||
federal_agency=self.federal_agency,
|
||||
|
@ -1538,12 +1719,15 @@ class TestCreateFederalPortfolio(TestCase):
|
|||
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()
|
||||
self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency)
|
||||
self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||
# SANITY CHECK: if the portfolio updates, it will change to 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
|
||||
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.creator, self.user)
|
||||
|
|
|
@ -63,7 +63,6 @@ class TestGroups(TestCase):
|
|||
|
||||
# Get the codenames of actual permissions associated with the group
|
||||
actual_permissions = [p.codename for p in cisa_analysts_group.permissions.all()]
|
||||
self.maxDiff = None
|
||||
|
||||
# Assert that the actual permissions match the expected permissions
|
||||
self.assertListEqual(actual_permissions, expected_permissions)
|
||||
|
|
|
@ -71,8 +71,8 @@ class CsvReportsTest(MockDbForSharedTests):
|
|||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
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("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(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"),
|
||||
]
|
||||
|
@ -93,8 +93,8 @@ class CsvReportsTest(MockDbForSharedTests):
|
|||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
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("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(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("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
|
||||
|
@ -251,32 +251,35 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO,"
|
||||
"SO email,Security contact email,Domain managers,Invited domain managers\n"
|
||||
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,,(blank),,,"
|
||||
"meoward@rocks.com,\n"
|
||||
"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"
|
||||
"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),,,"
|
||||
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,"
|
||||
"Organization name,City,State,SO,SO email,"
|
||||
"Security contact email,Domain managers,Invited domain managers\n"
|
||||
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
|
||||
"Portfolio 1 Federal Agency,,,, ,,(blank),"
|
||||
"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,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -312,20 +315,17 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# We expect only domains associated with the user
|
||||
expected_content = (
|
||||
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
|
||||
"City,State,SO,SO email,"
|
||||
"Security contact email,Domain managers,Invited domain managers\n"
|
||||
"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),"
|
||||
"City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
|
||||
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
|
||||
'"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,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -493,17 +493,17 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"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,World War I Centennial Commission,,,,(blank)\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||
"defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
|
||||
"cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
|
||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
|
||||
"ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
|
||||
"zdomain12.gov,Interstate,,,,,(blank)\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -533,16 +533,16 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"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,World War I Centennial Commission,,,,(blank)\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||
"defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
|
||||
"cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
|
||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
|
||||
"ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -587,13 +587,13 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,"
|
||||
"State,Status,Expiration date, Deleted\n"
|
||||
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
|
||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
|
||||
"zdomain12.govInterstateReady(blank)\n"
|
||||
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Ready,(blank)\n"
|
||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
||||
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
|
||||
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
||||
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
|
||||
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank),2024-04-02\n"
|
||||
"xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
||||
"sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
||||
"xdomain7.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# 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).
|
||||
She should show twice in this report but not in test_DomainManaged."""
|
||||
self.maxDiff = None
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
# Call the export functions
|
||||
|
@ -646,7 +645,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -683,7 +681,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -721,10 +718,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
# @less_console_noise_decorator
|
||||
def test_domain_request_data_full(self):
|
||||
"""Tests the full domain request report."""
|
||||
# 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)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
expected_content = (
|
||||
# Header
|
||||
"Domain request,Status,Domain type,Federal type,"
|
||||
"Federal agency,Organization name,Election office,City,State/territory,"
|
||||
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
|
||||
"Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
|
||||
"SO title/role,Request purpose,Request additional details,Other contacts,"
|
||||
"Domain request,Status,Domain type,Federal type,Federal agency,Organization name,Election office,"
|
||||
"City,State/territory,Region,Creator first name,Creator last name,Creator email,"
|
||||
"Creator approved domains count,Creator active requests count,Alternative domains,SO first name,"
|
||||
"SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
|
||||
"CISA regional representative,Current websites,Investigator\n"
|
||||
# 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"
|
||||
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
||||
"testy@town.com,"
|
||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||
'city3.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"cheeseville.gov, city1.gov,'
|
||||
'igorville.gov",Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,CISA-first-name '
|
||||
"CISA-last-name "
|
||||
'| There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, Testy Tester '
|
||||
'testy2@town.com"'
|
||||
',test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
||||
"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,Testy Tester "
|
||||
"testy2@town.com"
|
||||
",cisaRep@igorville.gov,city.com,\n"
|
||||
"city6.gov,Submitted,Federal,Executive,,Testorg,N/A,,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,Testy Tester "
|
||||
"testy2@town.com,"
|
||||
"city2.gov,In review,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
|
||||
"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,"
|
||||
'"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
|
||||
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
||||
'Testy Tester testy2@town.com",'
|
||||
'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
||||
"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,"
|
||||
"Testy Tester testy2@town.com,"
|
||||
"cisaRep@igorville.gov,city.com,\n"
|
||||
"city6.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
|
||||
"Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
|
||||
"cisaRep@igorville.gov,city.com,\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
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
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
self.maxDiff = None
|
||||
# Add portfolio to session
|
||||
request = GenericTestHelper._mock_user_request_for_factory(request)
|
||||
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,"
|
||||
"Member management,Domain management,Number of domains,Domains\n"
|
||||
# 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,"
|
||||
'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"
|
||||
"tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,None,False,0,\n"
|
||||
"icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,Manager,False,0,\n"
|
||||
"cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer,Viewer,False,0,\n"
|
||||
"nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Manager,False,0,\n"
|
||||
"nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Viewer,False,0,\n"
|
||||
"nonexistentmember_3@igorville.gov,False,help@get.gov,Unretrieved,Invited,Viewer,None,False,0,\n"
|
||||
"nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,"
|
||||
"Invited,Viewer Requester,Manager,False,0,\n"
|
||||
"nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer,Viewer,False,0,\n"
|
||||
"nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,"
|
||||
"None,Manager,False,0,\n"
|
||||
"nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,"
|
||||
"None,Viewer,False,0,\n"
|
||||
"nonexistentmember_3@igorville.gov,False,help@get.gov,Unretrieved,Invited,"
|
||||
"Viewer,None,False,0,\n"
|
||||
"nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,Invited,"
|
||||
"Viewer Requester,Manager,False,0,\n"
|
||||
"nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,"
|
||||
"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,
|
||||
# spaces and leading/trailing whitespace
|
||||
|
|
|
@ -414,8 +414,11 @@ class MemberExport(BaseExport):
|
|||
)
|
||||
.values(*shared_columns)
|
||||
)
|
||||
|
||||
return convert_queryset_to_dict(permissions.union(invitations), is_model=False)
|
||||
# Adding a order_by increases output predictability.
|
||||
# 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
|
||||
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 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
|
||||
def update_queryset(cls, queryset, **kwargs):
|
||||
"""
|
||||
|
@ -614,10 +726,10 @@ class DomainExport(BaseExport):
|
|||
if first_ready_on is None:
|
||||
first_ready_on = "(blank)"
|
||||
|
||||
# organization_type has generic_org_type AND is_election
|
||||
domain_org_type = model.get("organization_type")
|
||||
# organization_type has organization_type AND is_election
|
||||
domain_org_type = model.get("converted_generic_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)
|
||||
domain_type = human_readable_domain_org_type
|
||||
if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
|
||||
|
@ -640,12 +752,12 @@ class DomainExport(BaseExport):
|
|||
"First ready on": first_ready_on,
|
||||
"Expiration date": expiration_date,
|
||||
"Domain type": domain_type,
|
||||
"Agency": model.get("federal_agency__agency"),
|
||||
"Organization name": model.get("organization_name"),
|
||||
"City": model.get("city"),
|
||||
"State": model.get("state_territory"),
|
||||
"SO": model.get("so_name"),
|
||||
"SO email": model.get("senior_official__email"),
|
||||
"Agency": model.get("converted_federal_agency"),
|
||||
"Organization name": model.get("converted_organization_name"),
|
||||
"City": model.get("converted_city"),
|
||||
"State": model.get("converted_state_territory"),
|
||||
"SO": model.get("converted_so_name"),
|
||||
"SO email": model.get("converted_so_email"),
|
||||
"Security contact email": security_contact_email,
|
||||
"Created at": model.get("domain__created_at"),
|
||||
"Deleted": model.get("domain__deleted"),
|
||||
|
@ -654,8 +766,23 @@ class DomainExport(BaseExport):
|
|||
}
|
||||
|
||||
row = [FIELDS.get(column, "") for column in columns]
|
||||
|
||||
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
|
||||
def get_sliced_domains(cls, filter_condition):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
domains = DomainInformation.objects.all().filter(**filter_condition).distinct()
|
||||
domains_count = domains.count()
|
||||
federal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
|
||||
interstate = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).count()
|
||||
state_or_territory = (
|
||||
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
|
||||
domain_informations = DomainInformation.objects.all().filter(**filter_condition).distinct()
|
||||
domains_count = domain_informations.count()
|
||||
federal = (
|
||||
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.FEDERAL)
|
||||
.distinct()
|
||||
.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 = (
|
||||
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 = (
|
||||
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 [
|
||||
domains_count,
|
||||
|
@ -706,6 +861,7 @@ class DomainDataType(DomainExport):
|
|||
"""
|
||||
Overrides the columns for CSV export specific to DomainExport.
|
||||
"""
|
||||
|
||||
return [
|
||||
"Domain name",
|
||||
"Status",
|
||||
|
@ -723,6 +879,13 @@ class DomainDataType(DomainExport):
|
|||
"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
|
||||
def get_sort_fields(cls):
|
||||
"""
|
||||
|
@ -730,9 +893,9 @@ class DomainDataType(DomainExport):
|
|||
"""
|
||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||
return [
|
||||
"organization_type",
|
||||
Coalesce("federal_type", Value("ZZZZZ")),
|
||||
"federal_agency",
|
||||
"converted_generic_org_type",
|
||||
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
||||
"converted_federal_agency",
|
||||
"domain__name",
|
||||
]
|
||||
|
||||
|
@ -773,20 +936,6 @@ class DomainDataType(DomainExport):
|
|||
"""
|
||||
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
|
||||
def get_related_table_fields(cls):
|
||||
"""
|
||||
|
@ -892,7 +1041,7 @@ class DomainRequestsDataType:
|
|||
cls.safe_get(getattr(request, "region_field", None)),
|
||||
request.status,
|
||||
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, "additional_details", None)),
|
||||
cls.safe_get(getattr(request, "creator_approved_domains_count", None)),
|
||||
|
@ -943,6 +1092,13 @@ class DomainDataFull(DomainExport):
|
|||
"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
|
||||
def get_sort_fields(cls):
|
||||
"""
|
||||
|
@ -950,9 +1106,9 @@ class DomainDataFull(DomainExport):
|
|||
"""
|
||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||
return [
|
||||
"organization_type",
|
||||
Coalesce("federal_type", Value("ZZZZZ")),
|
||||
"federal_agency",
|
||||
"converted_generic_org_type",
|
||||
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
||||
"converted_federal_agency",
|
||||
"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
|
||||
def get_related_table_fields(cls):
|
||||
"""
|
||||
|
@ -1037,6 +1179,13 @@ class DomainDataFederal(DomainExport):
|
|||
"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
|
||||
def get_sort_fields(cls):
|
||||
"""
|
||||
|
@ -1044,9 +1193,9 @@ class DomainDataFederal(DomainExport):
|
|||
"""
|
||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||
return [
|
||||
"organization_type",
|
||||
Coalesce("federal_type", Value("ZZZZZ")),
|
||||
"federal_agency",
|
||||
"converted_generic_org_type",
|
||||
Coalesce("converted_federal_type", Value("ZZZZZ")),
|
||||
"converted_federal_agency",
|
||||
"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
|
||||
def get_related_table_fields(cls):
|
||||
"""
|
||||
|
@ -1476,24 +1611,180 @@ class DomainRequestExport(BaseExport):
|
|||
# Return the model class that this export handles
|
||||
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
|
||||
def get_sliced_requests(cls, filter_condition):
|
||||
"""Get filtered requests counts sliced by org type and election office."""
|
||||
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
|
||||
requests_count = requests.count()
|
||||
federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
|
||||
interstate = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).distinct().count()
|
||||
state_or_territory = (
|
||||
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
|
||||
federal = (
|
||||
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.FEDERAL)
|
||||
.distinct()
|
||||
.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 = (
|
||||
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 = (
|
||||
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()
|
||||
|
||||
|
@ -1517,11 +1808,11 @@ class DomainRequestExport(BaseExport):
|
|||
"""
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Handle the status field. Defaults to the wrong format.
|
||||
|
@ -1569,19 +1860,19 @@ class DomainRequestExport(BaseExport):
|
|||
"Other contacts": model.get("all_other_contacts"),
|
||||
"Current websites": model.get("all_current_websites"),
|
||||
# Untouched FK fields - passed into the request dict.
|
||||
"Federal agency": model.get("federal_agency__agency"),
|
||||
"SO first name": model.get("senior_official__first_name"),
|
||||
"SO last name": model.get("senior_official__last_name"),
|
||||
"SO email": model.get("senior_official__email"),
|
||||
"SO title/role": model.get("senior_official__title"),
|
||||
"Federal agency": model.get("converted_federal_agency"),
|
||||
"SO first name": model.get("converted_senior_official_first_name"),
|
||||
"SO last name": model.get("converted_senior_official_last_name"),
|
||||
"SO email": model.get("converted_so_email"),
|
||||
"SO title/role": model.get("converted_senior_official_title"),
|
||||
"Creator first name": model.get("creator__first_name"),
|
||||
"Creator last name": model.get("creator__last_name"),
|
||||
"Creator email": model.get("creator__email"),
|
||||
"Investigator": model.get("investigator__email"),
|
||||
# Untouched fields
|
||||
"Organization name": model.get("organization_name"),
|
||||
"City": model.get("city"),
|
||||
"State/territory": model.get("state_territory"),
|
||||
"Organization name": model.get("converted_organization_name"),
|
||||
"City": model.get("converted_city"),
|
||||
"State/territory": model.get("converted_state_territory"),
|
||||
"Request purpose": model.get("purpose"),
|
||||
"CISA regional representative": model.get("cisa_representative_email"),
|
||||
"Last submitted date": model.get("last_submitted_date"),
|
||||
|
@ -1724,24 +2015,34 @@ class DomainRequestDataFull(DomainRequestExport):
|
|||
"""
|
||||
Get a dict of computed fields.
|
||||
"""
|
||||
return {
|
||||
"creator_approved_domains_count": cls.get_creator_approved_domains_count_query(),
|
||||
"creator_active_requests_count": cls.get_creator_active_requests_count_query(),
|
||||
"all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True),
|
||||
"all_alternative_domains": StringAgg("alternative_domains__website", delimiter=delimiter, distinct=True),
|
||||
# Coerce the other contacts object to "{first_name} {last_name} {email}"
|
||||
"all_other_contacts": StringAgg(
|
||||
Concat(
|
||||
"other_contacts__first_name",
|
||||
Value(" "),
|
||||
"other_contacts__last_name",
|
||||
Value(" "),
|
||||
"other_contacts__email",
|
||||
# Get computed fields from the parent class
|
||||
computed_fields = super().get_computed_fields()
|
||||
|
||||
# Add additional computed fields
|
||||
computed_fields.update(
|
||||
{
|
||||
"creator_approved_domains_count": cls.get_creator_approved_domains_count_query(),
|
||||
"creator_active_requests_count": cls.get_creator_active_requests_count_query(),
|
||||
"all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True),
|
||||
"all_alternative_domains": StringAgg(
|
||||
"alternative_domains__website", delimiter=delimiter, distinct=True
|
||||
),
|
||||
delimiter=delimiter,
|
||||
distinct=True,
|
||||
),
|
||||
}
|
||||
# Coerce the other contacts object to "{first_name} {last_name} {email}"
|
||||
"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
|
||||
def get_related_table_fields(cls):
|
||||
|
|
|
@ -13,6 +13,7 @@ from .domain import (
|
|||
DomainAddUserView,
|
||||
DomainInvitationCancelView,
|
||||
DomainDeleteUserView,
|
||||
PrototypeDomainDNSRecordView,
|
||||
)
|
||||
from .user_profile import UserProfileView, FinishProfileSetupView
|
||||
from .health import *
|
||||
|
|
|
@ -7,7 +7,7 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionCancelView).
|
|||
|
||||
from datetime import date
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db import IntegrityError
|
||||
|
@ -64,6 +64,7 @@ from epplibwrapper import (
|
|||
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
from .utility import DomainPermissionView, DomainInvitationPermissionCancelView
|
||||
from django import forms
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -454,6 +455,216 @@ class DomainDNSView(DomainBaseView):
|
|||
"""DNS Information View."""
|
||||
|
||||
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):
|
||||
|
|
|
@ -53,7 +53,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
URL_NAMESPACE = "domain-request"
|
||||
# name for accessing /domain-request/<id>/edit
|
||||
EDIT_URL_NAME = "edit-domain-request"
|
||||
NEW_URL_NAME = "/request/start/"
|
||||
NEW_URL_NAME = "start"
|
||||
FINISHED_URL_NAME = "finished"
|
||||
|
||||
# region: Titles
|
||||
# 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
|
||||
# is first in the list of steps.
|
||||
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.
|
||||
# Creating a request will be handled in the post method for the
|
||||
# intro page.
|
||||
|
@ -614,7 +615,7 @@ class RequestingEntity(DomainRequestWizard):
|
|||
class PortfolioAdditionalDetails(DomainRequestWizard):
|
||||
template_name = "portfolio_domain_request_additional_details.html"
|
||||
|
||||
forms = [forms.AnythingElseForm]
|
||||
forms = [forms.PortfolioAnythingElseForm]
|
||||
|
||||
|
||||
# Non-portfolio pages
|
||||
|
|
|
@ -626,34 +626,3 @@ class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
|||
if permission_exists:
|
||||
messages.warning(self.request, "User is already a member of this portfolio.")
|
||||
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():
|
||||
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
|
||||
setattr(obj, field_name, current_user)
|
||||
obj.save()
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
10038 OUTOFSCOPE http://app:8080/permissions
|
||||
10038 OUTOFSCOPE http://app:8080/suborganization/
|
||||
10038 OUTOFSCOPE http://app:8080/transfer/
|
||||
10038 OUTOFSCOPE http://app:8080/prototype-dns
|
||||
# This URL always returns 404, so include it as well.
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue