Merge branch 'main' of https://github.com/cisagov/manage.get.gov into es/2913-domain-request-screenreader

This commit is contained in:
Erin Song 2024-12-09 11:37:16 -08:00
commit f7cc588780
No known key found for this signature in database
21 changed files with 1128 additions and 303 deletions

View file

@ -378,3 +378,18 @@ Then, copy the variables under the section labled `s3`.
## Request Flow FSM Diagram ## Request Flow FSM Diagram
The [.gov Domain Request & Domain Status Digram](https://miro.com/app/board/uXjVMuqbLOk=/?moveToWidget=3458764594819017396&cot=14) visualizes the domain request flow and resulting domain objects. The [.gov Domain Request & Domain Status Digram](https://miro.com/app/board/uXjVMuqbLOk=/?moveToWidget=3458764594819017396&cot=14) visualizes the domain request flow and resulting domain objects.
## Testing the prototype add DNS record feature (delete this after we are done testing!)
We are currently testing using cloudflare to add DNS records. Specifically, an A record. To use this, you will need to enable the
`prototype_dns_flag` waffle flag and navigate to `igorville.gov`, `dns.gov`, or `domainops.gov`. Click manage, then click DNS. From there, click the `Prototype DNS record creator` button.
Before we can send data to cloudflare, you will need these values in your .env file:
```
REGISTRY_TENANT_KEY = {tenant key}
REGISTRY_SERVICE_EMAIL = {An email address}
REGISTRY_TENANT_NAME = {Name of the bucket, i.e. "CISA" }
```
You can obtain these by following the steps outlined in the [dns hosting discovery doc](https://docs.google.com/document/d/1Yq5d2M3MgM2vPhUBZ0k5wOmCQst4vND9-2qEZ55-h-Y/edit?tab=t.0), BUT it is far easier to just get these from someone else. Reach out to Zander for this information if you do not have it.
Alternatively, if you are testing on a sandbox, you will need to add those to getgov-credentials.

View file

@ -59,6 +59,9 @@ services:
- AWS_S3_BUCKET_NAME - AWS_S3_BUCKET_NAME
# File encryption credentials # File encryption credentials
- SECRET_ENCRYPT_METADATA - SECRET_ENCRYPT_METADATA
- REGISTRY_TENANT_KEY
- REGISTRY_SERVICE_EMAIL
- REGISTRY_TENANT_NAME
stdin_open: true stdin_open: true
tty: true tty: true
ports: ports:

View file

@ -3,7 +3,14 @@ import logging
import copy import copy
from typing import Optional from typing import Optional
from django import forms from django import forms
from django.db.models import Value, CharField, Q from django.db.models import (
Case,
CharField,
F,
Q,
Value,
When,
)
from django.db.models.functions import Concat, Coalesce from django.db.models.functions import Concat, Coalesce
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from registrar.models.federal_agency import FederalAgency from registrar.models.federal_agency import FederalAgency
@ -1467,21 +1474,57 @@ class DomainInformationResource(resources.ModelResource):
class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"""Customize domain information admin class.""" """Customize domain information admin class."""
class GenericOrgFilter(admin.SimpleListFilter):
"""Custom Generic Organization filter that accomodates portfolio feature.
If we have a portfolio, use the portfolio's organization. If not, use the
organization in the Domain Information object."""
title = "generic organization"
parameter_name = "converted_generic_orgs"
def lookups(self, request, model_admin):
converted_generic_orgs = set()
# Populate the set with tuples of (value, display value)
for domain_info in DomainInformation.objects.all():
converted_generic_org = domain_info.converted_generic_org_type # Actual value
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
if converted_generic_org:
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
# Sort the set by display value
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
# Filter queryset
def queryset(self, request, queryset):
if self.value(): # Check if a generic org is selected in the filter
return queryset.filter(
Q(portfolio__organization_type=self.value())
| Q(portfolio__isnull=True, generic_org_type=self.value())
)
return queryset
resource_classes = [DomainInformationResource] resource_classes = [DomainInformationResource]
form = DomainInformationAdminForm form = DomainInformationAdminForm
# Customize column header text
@admin.display(description=_("Generic Org Type"))
def converted_generic_org_type(self, obj):
return obj.converted_generic_org_type_display
# Columns # Columns
list_display = [ list_display = [
"domain", "domain",
"generic_org_type", "converted_generic_org_type",
"created_at", "created_at",
] ]
orderable_fk_fields = [("domain", "name")] orderable_fk_fields = [("domain", "name")]
# Filters # Filters
list_filter = ["generic_org_type"] list_filter = [GenericOrgFilter]
# Search # Search
search_fields = [ search_fields = [
@ -1661,24 +1704,23 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
converted_generic_orgs = set() converted_generic_orgs = set()
# Populate the set with tuples of (value, display value)
for domain_request in DomainRequest.objects.all(): for domain_request in DomainRequest.objects.all():
converted_generic_org = domain_request.converted_generic_org_type converted_generic_org = domain_request.converted_generic_org_type # Actual value
if converted_generic_org: converted_generic_org_display = domain_request.converted_generic_org_type_display # Display value
converted_generic_orgs.add(converted_generic_org)
return sorted((org, org) for org in converted_generic_orgs) if converted_generic_org:
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
# Sort the set by display value
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
# Filter queryset # Filter queryset
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value(): # Check if a generic org is selected in the filter if self.value(): # Check if a generic org is selected in the filter
return queryset.filter( return queryset.filter(
# Filter based on the generic org value returned by converted_generic_org_type Q(portfolio__organization_type=self.value())
id__in=[ | Q(portfolio__isnull=True, generic_org_type=self.value())
domain_request.id
for domain_request in queryset
if domain_request.converted_generic_org_type
and domain_request.converted_generic_org_type == self.value()
]
) )
return queryset return queryset
@ -1693,24 +1735,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
converted_federal_types = set() converted_federal_types = set()
# Populate the set with tuples of (value, display value)
for domain_request in DomainRequest.objects.all(): for domain_request in DomainRequest.objects.all():
converted_federal_type = domain_request.converted_federal_type converted_federal_type = domain_request.converted_federal_type # Actual value
if converted_federal_type: converted_federal_type_display = domain_request.converted_federal_type_display # Display value
converted_federal_types.add(converted_federal_type)
return sorted((type, type) for type in converted_federal_types) if converted_federal_type:
converted_federal_types.add(
(converted_federal_type, converted_federal_type_display) # Value, Display
)
# Sort the set by display value
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
# Filter queryset # Filter queryset
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value(): # Check if federal Type is selected in the filter if self.value(): # Check if a federal type is selected in the filter
return queryset.filter( return queryset.filter(
# Filter based on the federal type returned by converted_federal_type Q(portfolio__federal_agency__federal_type=self.value())
id__in=[ | Q(portfolio__isnull=True, federal_type=self.value())
domain_request.id
for domain_request in queryset
if domain_request.converted_federal_type
and domain_request.converted_federal_type == self.value()
]
) )
return queryset return queryset
@ -1776,7 +1819,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
@admin.display(description=_("Generic Org Type")) @admin.display(description=_("Generic Org Type"))
def converted_generic_org_type(self, obj): def converted_generic_org_type(self, obj):
return obj.converted_generic_org_type return obj.converted_generic_org_type_display
@admin.display(description=_("Organization Name")) @admin.display(description=_("Organization Name"))
def converted_organization_name(self, obj): def converted_organization_name(self, obj):
@ -1788,7 +1831,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
@admin.display(description=_("Federal Type")) @admin.display(description=_("Federal Type"))
def converted_federal_type(self, obj): def converted_federal_type(self, obj):
return obj.converted_federal_type return obj.converted_federal_type_display
@admin.display(description=_("City")) @admin.display(description=_("City"))
def converted_city(self, obj): def converted_city(self, obj):
@ -2679,6 +2722,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
resource_classes = [DomainResource] resource_classes = [DomainResource]
# ------- FILTERS
class ElectionOfficeFilter(admin.SimpleListFilter): class ElectionOfficeFilter(admin.SimpleListFilter):
"""Define a custom filter for is_election_board""" """Define a custom filter for is_election_board"""
@ -2697,18 +2741,135 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
if self.value() == "0": if self.value() == "0":
return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None)) return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None))
class GenericOrgFilter(admin.SimpleListFilter):
"""Custom Generic Organization filter that accomodates portfolio feature.
If we have a portfolio, use the portfolio's organization. If not, use the
organization in the Domain Information object."""
title = "generic organization"
parameter_name = "converted_generic_orgs"
def lookups(self, request, model_admin):
converted_generic_orgs = set()
# Populate the set with tuples of (value, display value)
for domain_info in DomainInformation.objects.all():
converted_generic_org = domain_info.converted_generic_org_type # Actual value
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
if converted_generic_org:
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
# Sort the set by display value
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
# Filter queryset
def queryset(self, request, queryset):
if self.value(): # Check if a generic org is selected in the filter
return queryset.filter(
Q(domain_info__portfolio__organization_type=self.value())
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
)
return queryset
class FederalTypeFilter(admin.SimpleListFilter):
"""Custom Federal Type filter that accomodates portfolio feature.
If we have a portfolio, use the portfolio's federal type. If not, use the
federal type in the Domain Information object."""
title = "federal type"
parameter_name = "converted_federal_types"
def lookups(self, request, model_admin):
converted_federal_types = set()
# Populate the set with tuples of (value, display value)
for domain_info in DomainInformation.objects.all():
converted_federal_type = domain_info.converted_federal_type # Actual value
converted_federal_type_display = domain_info.converted_federal_type_display # Display value
if converted_federal_type:
converted_federal_types.add(
(converted_federal_type, converted_federal_type_display) # Value, Display
)
# Sort the set by display value
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
# Filter queryset
def queryset(self, request, queryset):
if self.value(): # Check if a federal type is selected in the filter
return queryset.filter(
Q(domain_info__portfolio__federal_agency__federal_type=self.value())
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
)
return queryset
def get_annotated_queryset(self, queryset):
return queryset.annotate(
converted_generic_org_type=Case(
# When portfolio is present, use its value instead
When(domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__organization_type")),
# Otherwise, return the natively assigned value
default=F("domain_info__generic_org_type"),
),
converted_federal_agency=Case(
# When portfolio is present, use its value instead
When(
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
then=F("domain_info__portfolio__federal_agency__agency"),
),
# Otherwise, return the natively assigned value
default=F("domain_info__federal_agency__agency"),
),
converted_federal_type=Case(
# When portfolio is present, use its value instead
When(
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
then=F("domain_info__portfolio__federal_agency__federal_type"),
),
# Otherwise, return the natively assigned value
default=F("domain_info__federal_agency__federal_type"),
),
converted_organization_name=Case(
# When portfolio is present, use its value instead
When(domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__organization_name")),
# Otherwise, return the natively assigned value
default=F("domain_info__organization_name"),
),
converted_city=Case(
# When portfolio is present, use its value instead
When(domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__city")),
# Otherwise, return the natively assigned value
default=F("domain_info__city"),
),
converted_state_territory=Case(
# When portfolio is present, use its value instead
When(domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__state_territory")),
# Otherwise, return the natively assigned value
default=F("domain_info__state_territory"),
),
)
# Filters
list_filter = [GenericOrgFilter, FederalTypeFilter, ElectionOfficeFilter, "state"]
# ------- END FILTERS
# Inlines
inlines = [DomainInformationInline] inlines = [DomainInformationInline]
# Columns # Columns
list_display = [ list_display = [
"name", "name",
"generic_org_type", "converted_generic_org_type",
"federal_type", "converted_federal_type",
"federal_agency", "converted_federal_agency",
"organization_name", "converted_organization_name",
"custom_election_board", "custom_election_board",
"city", "converted_city",
"state_territory", "converted_state_territory",
"state", "state",
"expiration_date", "expiration_date",
"created_at", "created_at",
@ -2723,28 +2884,81 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
), ),
) )
# ------- Domain Information Fields
# --- Generic Org Type
# Use converted value in the table
@admin.display(description=_("Generic Org Type"))
def converted_generic_org_type(self, obj):
return obj.domain_info.converted_generic_org_type_display
converted_generic_org_type.admin_order_field = "converted_generic_org_type" # type: ignore
# Use native value for the change form
def generic_org_type(self, obj): def generic_org_type(self, obj):
return obj.domain_info.get_generic_org_type_display() return obj.domain_info.get_generic_org_type_display()
generic_org_type.admin_order_field = "domain_info__generic_org_type" # type: ignore # --- Federal Agency
@admin.display(description=_("Federal Agency"))
def converted_federal_agency(self, obj):
return obj.domain_info.converted_federal_agency
converted_federal_agency.admin_order_field = "converted_federal_agency" # type: ignore
# Use native value for the change form
def federal_agency(self, obj): def federal_agency(self, obj):
if obj.domain_info: if obj.domain_info:
return obj.domain_info.federal_agency return obj.domain_info.federal_agency
else: else:
return None return None
federal_agency.admin_order_field = "domain_info__federal_agency" # type: ignore # --- Federal Type
# Use converted value in the table
@admin.display(description=_("Federal Type"))
def converted_federal_type(self, obj):
return obj.domain_info.converted_federal_type_display
converted_federal_type.admin_order_field = "converted_federal_type" # type: ignore
# Use native value for the change form
def federal_type(self, obj): def federal_type(self, obj):
return obj.domain_info.federal_type if obj.domain_info else None return obj.domain_info.federal_type if obj.domain_info else None
federal_type.admin_order_field = "domain_info__federal_type" # type: ignore # --- Organization Name
# Use converted value in the table
@admin.display(description=_("Organization Name"))
def converted_organization_name(self, obj):
return obj.domain_info.converted_organization_name
converted_organization_name.admin_order_field = "converted_organization_name" # type: ignore
# Use native value for the change form
def organization_name(self, obj): def organization_name(self, obj):
return obj.domain_info.organization_name if obj.domain_info else None return obj.domain_info.organization_name if obj.domain_info else None
organization_name.admin_order_field = "domain_info__organization_name" # type: ignore # --- City
# Use converted value in the table
@admin.display(description=_("City"))
def converted_city(self, obj):
return obj.domain_info.converted_city
converted_city.admin_order_field = "converted_city" # type: ignore
# Use native value for the change form
def city(self, obj):
return obj.domain_info.city if obj.domain_info else None
# --- State
# Use converted value in the table
@admin.display(description=_("State / territory"))
def converted_state_territory(self, obj):
return obj.domain_info.converted_state_territory
converted_state_territory.admin_order_field = "converted_state_territory" # type: ignore
# Use native value for the change form
def state_territory(self, obj):
return obj.domain_info.state_territory if obj.domain_info else None
def dnssecdata(self, obj): def dnssecdata(self, obj):
return "Yes" if obj.dnssecdata else "No" return "Yes" if obj.dnssecdata else "No"
@ -2777,23 +2991,14 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore custom_election_board.short_description = "Election office" # type: ignore
def city(self, obj): # Search
return obj.domain_info.city if obj.domain_info else None
city.admin_order_field = "domain_info__city" # type: ignore
@admin.display(description=_("State / territory"))
def state_territory(self, obj):
return obj.domain_info.state_territory if obj.domain_info else None
state_territory.admin_order_field = "domain_info__state_territory" # type: ignore
# Filters
list_filter = ["domain_info__generic_org_type", "domain_info__federal_type", ElectionOfficeFilter, "state"]
search_fields = ["name"] search_fields = ["name"]
search_help_text = "Search by domain name." search_help_text = "Search by domain name."
# Change Form
change_form_template = "django/admin/domain_change_form.html" change_form_template = "django/admin/domain_change_form.html"
# Readonly Fields
readonly_fields = ( readonly_fields = (
"state", "state",
"expiration_date", "expiration_date",
@ -3058,7 +3263,8 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
"""Custom get_queryset to filter by portfolio if portfolio is in the """Custom get_queryset to filter by portfolio if portfolio is in the
request params.""" request params."""
qs = super().get_queryset(request) initial_qs = super().get_queryset(request)
qs = self.get_annotated_queryset(initial_qs)
# Check if a 'portfolio' parameter is passed in the request # Check if a 'portfolio' parameter is passed in the request
portfolio_id = request.GET.get("portfolio") portfolio_id = request.GET.get("portfolio")
if portfolio_id: if portfolio_id:
@ -3579,6 +3785,14 @@ class WaffleFlagAdmin(FlagAdmin):
model = models.WaffleFlag model = models.WaffleFlag
fields = "__all__" fields = "__all__"
# Hack to get the dns_prototype_flag to auto populate when you navigate to
# the waffle flag page.
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["dns_prototype_flag"] = flag_is_active_for_user(request.user, "dns_prototype_flag")
return super().changelist_view(request, extra_context=extra_context)
class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
list_display = ["name", "portfolio"] list_display = ["name", "portfolio"]

View file

@ -86,6 +86,11 @@ secret_registry_key = b64decode(secret("REGISTRY_KEY", ""))
secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "") secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "")
secret_registry_hostname = secret("REGISTRY_HOSTNAME") secret_registry_hostname = secret("REGISTRY_HOSTNAME")
# PROTOTYPE: Used for DNS hosting
secret_registry_tenant_key = secret("REGISTRY_TENANT_KEY", None)
secret_registry_tenant_name = secret("REGISTRY_TENANT_NAME", None)
secret_registry_service_email = secret("REGISTRY_SERVICE_EMAIL", None)
# region: Basic Django Config-----------------------------------------------### # region: Basic Django Config-----------------------------------------------###
# Build paths inside the project like this: BASE_DIR / "subdir". # Build paths inside the project like this: BASE_DIR / "subdir".
@ -685,6 +690,9 @@ SECRET_REGISTRY_CERT = secret_registry_cert
SECRET_REGISTRY_KEY = secret_registry_key SECRET_REGISTRY_KEY = secret_registry_key
SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase
SECRET_REGISTRY_HOSTNAME = secret_registry_hostname SECRET_REGISTRY_HOSTNAME = secret_registry_hostname
SECRET_REGISTRY_TENANT_KEY = secret_registry_tenant_key
SECRET_REGISTRY_TENANT_NAME = secret_registry_tenant_name
SECRET_REGISTRY_SERVICE_EMAIL = secret_registry_service_email
# endregion # endregion
# region: Security and Privacy----------------------------------------------### # region: Security and Privacy----------------------------------------------###

View file

@ -255,11 +255,6 @@ urlpatterns = [
ExportDataTypeRequests.as_view(), ExportDataTypeRequests.as_view(),
name="export_data_type_requests", name="export_data_type_requests",
), ),
path(
"reports/export_data_type_requests/",
ExportDataTypeRequests.as_view(),
name="export_data_type_requests",
),
path( path(
"domain-request/<int:id>/edit/", "domain-request/<int:id>/edit/",
views.DomainRequestWizard.as_view(), views.DomainRequestWizard.as_view(),
@ -298,6 +293,7 @@ urlpatterns = [
name="todo", name="todo",
), ),
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"), path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
path("domain/<int:pk>/prototype-dns", views.PrototypeDomainDNSRecordView.as_view(), name="prototype-domain-dns"),
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"), path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
path( path(
"domain/<int:pk>/dns", "domain/<int:pk>/dns",

View file

@ -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", "Doesnt meet naming requirements"),
("other", "Other (no auto-email sent)"),
],
null=True,
),
),
]

View file

@ -4,7 +4,6 @@ import ipaddress
import re import re
from datetime import date from datetime import date
from typing import Optional from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
from django.db import models from django.db import models

View file

@ -426,13 +426,14 @@ class DomainInformation(TimeStampedModel):
else: else:
return None return None
# ----- Portfolio Properties -----
@property @property
def converted_organization_name(self): def converted_organization_name(self):
if self.portfolio: if self.portfolio:
return self.portfolio.organization_name return self.portfolio.organization_name
return self.organization_name return self.organization_name
# ----- Portfolio Properties -----
@property @property
def converted_generic_org_type(self): def converted_generic_org_type(self):
if self.portfolio: if self.portfolio:
@ -442,8 +443,8 @@ class DomainInformation(TimeStampedModel):
@property @property
def converted_federal_agency(self): def converted_federal_agency(self):
if self.portfolio: if self.portfolio:
return self.portfolio.federal_agency return self.portfolio.federal_agency.agency
return self.federal_agency return self.federal_agency.agency
@property @property
def converted_federal_type(self): def converted_federal_type(self):
@ -454,20 +455,20 @@ class DomainInformation(TimeStampedModel):
@property @property
def converted_senior_official(self): def converted_senior_official(self):
if self.portfolio: if self.portfolio:
return self.portfolio.senior_official return self.portfolio.display_senior_official
return self.senior_official return self.display_senior_official
@property @property
def converted_address_line1(self): def converted_address_line1(self):
if self.portfolio: if self.portfolio:
return self.portfolio.address_line1 return self.portfolio.display_address_line1
return self.address_line1 return self.display_address_line1
@property @property
def converted_address_line2(self): def converted_address_line2(self):
if self.portfolio: if self.portfolio:
return self.portfolio.address_line2 return self.portfolio.display_address_line2
return self.address_line2 return self.display_address_line2
@property @property
def converted_city(self): def converted_city(self):
@ -478,17 +479,30 @@ class DomainInformation(TimeStampedModel):
@property @property
def converted_state_territory(self): def converted_state_territory(self):
if self.portfolio: if self.portfolio:
return self.portfolio.state_territory return self.portfolio.get_state_territory_display()
return self.state_territory return self.get_state_territory_display()
@property @property
def converted_zipcode(self): def converted_zipcode(self):
if self.portfolio: if self.portfolio:
return self.portfolio.zipcode return self.portfolio.display_zipcode
return self.zipcode return self.display_zipcode
@property @property
def converted_urbanization(self): def converted_urbanization(self):
if self.portfolio: if self.portfolio:
return self.portfolio.urbanization return self.portfolio.display_urbanization
return self.urbanization return self.display_urbanization
# ----- Portfolio Properties (display values)-----
@property
def converted_generic_org_type_display(self):
if self.portfolio:
return self.portfolio.get_organization_type_display()
return self.get_generic_org_type_display()
@property
def converted_federal_type_display(self):
if self.portfolio:
return self.portfolio.federal_agency.get_federal_type_display()
return self.get_federal_type_display()

View file

@ -280,7 +280,7 @@ class DomainRequest(TimeStampedModel):
ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility") ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility")
QUESTIONABLE_SENIOR_OFFICIAL = ("questionable_senior_official", "Questionable senior official") QUESTIONABLE_SENIOR_OFFICIAL = ("questionable_senior_official", "Questionable senior official")
ALREADY_HAS_DOMAINS = ("already_has_domains", "Already has domains") ALREADY_HAS_A_DOMAIN = ("already_has_a_domain", "Already has a domain")
BAD_NAME = ("bad_name", "Doesnt meet naming requirements") BAD_NAME = ("bad_name", "Doesnt meet naming requirements")
OTHER = ("other", "Other (no auto-email sent)") OTHER = ("other", "Other (no auto-email sent)")
@ -1437,6 +1437,18 @@ class DomainRequest(TimeStampedModel):
return self.portfolio.federal_type return self.portfolio.federal_type
return self.federal_type return self.federal_type
@property
def converted_address_line1(self):
if self.portfolio:
return self.portfolio.address_line1
return self.address_line1
@property
def converted_address_line2(self):
if self.portfolio:
return self.portfolio.address_line2
return self.address_line2
@property @property
def converted_city(self): def converted_city(self):
if self.portfolio: if self.portfolio:
@ -1449,8 +1461,33 @@ class DomainRequest(TimeStampedModel):
return self.portfolio.state_territory return self.portfolio.state_territory
return self.state_territory return self.state_territory
@property
def converted_urbanization(self):
if self.portfolio:
return self.portfolio.urbanization
return self.urbanization
@property
def converted_zipcode(self):
if self.portfolio:
return self.portfolio.zipcode
return self.zipcode
@property @property
def converted_senior_official(self): def converted_senior_official(self):
if self.portfolio: if self.portfolio:
return self.portfolio.senior_official return self.portfolio.senior_official
return self.senior_official return self.senior_official
# ----- Portfolio Properties (display values)-----
@property
def converted_generic_org_type_display(self):
if self.portfolio:
return self.portfolio.get_organization_type_display()
return self.get_generic_org_type_display()
@property
def converted_federal_type_display(self):
if self.portfolio:
return self.portfolio.federal_agency.get_federal_type_display()
return self.get_federal_type_display()

View file

@ -28,6 +28,7 @@
<p>The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to a DNS hosting service and provide us with your name server information.</p> <p>The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to a DNS hosting service and provide us with your name server information.</p>
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p> <p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
{% url 'domain-dns-nameservers' pk=domain.id as url %} {% url 'domain-dns-nameservers' pk=domain.id as url %}
<ul class="usa-list"> <ul class="usa-list">
@ -35,6 +36,9 @@
{% url 'domain-dns-dnssec' pk=domain.id as url %} {% url 'domain-dns-dnssec' pk=domain.id as url %}
<li><a href="{{ url }}">DNSSEC</a></li> <li><a href="{{ url }}">DNSSEC</a></li>
{% if dns_prototype_flag and is_valid_domain %}
<li><a href="{% url 'prototype-domain-dns' pk=domain.id %}">Prototype DNS record creator</a></li>
{% endif %}
</ul> </ul>
{% endblock %} {# domain_content #} {% endblock %} {# domain_content #}

View 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 #}

View file

@ -563,9 +563,12 @@ class MockDb(TestCase):
cls.federal_agency_1, _ = FederalAgency.objects.get_or_create(agency="World War I Centennial Commission") cls.federal_agency_1, _ = FederalAgency.objects.get_or_create(agency="World War I Centennial Commission")
cls.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home") cls.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home")
cls.federal_agency_3, _ = FederalAgency.objects.get_or_create(
agency="Portfolio 1 Federal Agency", federal_type="executive"
)
cls.portfolio_1, _ = Portfolio.objects.get_or_create( cls.portfolio_1, _ = Portfolio.objects.get_or_create(
creator=cls.custom_superuser, federal_agency=cls.federal_agency_1 creator=cls.custom_superuser, federal_agency=cls.federal_agency_3, organization_type="federal"
) )
current_date = get_time_aware_date(datetime(2024, 4, 2)) current_date = get_time_aware_date(datetime(2024, 4, 2))

View file

@ -728,9 +728,9 @@ class TestDomainAdminWithClient(TestCase):
response = self.client.get("/admin/registrar/domain/") response = self.client.get("/admin/registrar/domain/")
# There are 4 template references to Federal (4) plus four references in the table # There are 4 template references to Federal (4) plus four references in the table
# for our actual domain_request # for our actual domain_request
self.assertContains(response, "Federal", count=56) self.assertContains(response, "Federal", count=57)
# This may be a bit more robust # This may be a bit more robust
self.assertContains(response, '<td class="field-generic_org_type">Federal</td>', count=1) self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist # Now let's make sure the long description does not exist
self.assertNotContains(response, "Federal: an agency of the U.S. government") self.assertNotContains(response, "Federal: an agency of the U.S. government")

View file

@ -203,7 +203,7 @@ class TestDomainRequestAdmin(MockEppLib):
domain_request.save() domain_request.save()
domain_request.action_needed() domain_request.action_needed()
domain_request.action_needed_reason = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS domain_request.action_needed_reason = DomainRequest.ActionNeededReasons.ALREADY_HAS_A_DOMAIN
domain_request.save() domain_request.save()
# Let's just change the action needed reason # Let's just change the action needed reason
@ -230,7 +230,7 @@ class TestDomainRequestAdmin(MockEppLib):
"In review", "In review",
"Rejected - Purpose requirements not met", "Rejected - Purpose requirements not met",
"Action needed - Unclear organization eligibility", "Action needed - Unclear organization eligibility",
"Action needed - Already has domains", "Action needed - Already has a domain",
"In review", "In review",
"Submitted", "Submitted",
"Started", "Started",
@ -241,7 +241,7 @@ class TestDomainRequestAdmin(MockEppLib):
assert_status_count(normalized_content, "Started", 1) assert_status_count(normalized_content, "Started", 1)
assert_status_count(normalized_content, "Submitted", 1) assert_status_count(normalized_content, "Submitted", 1)
assert_status_count(normalized_content, "In review", 2) assert_status_count(normalized_content, "In review", 2)
assert_status_count(normalized_content, "Action needed - Already has domains", 1) assert_status_count(normalized_content, "Action needed - Already has a domain", 1)
assert_status_count(normalized_content, "Action needed - Unclear organization eligibility", 1) assert_status_count(normalized_content, "Action needed - Unclear organization eligibility", 1)
assert_status_count(normalized_content, "Rejected - Purpose requirements not met", 1) assert_status_count(normalized_content, "Rejected - Purpose requirements not met", 1)
@ -576,9 +576,9 @@ class TestDomainRequestAdmin(MockEppLib):
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal") response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
# There are 2 template references to Federal (4) and two in the results data # There are 2 template references to Federal (4) and two in the results data
# of the request # of the request
self.assertContains(response, "Federal", count=51) self.assertContains(response, "Federal", count=55)
# This may be a bit more robust # This may be a bit more robust
self.assertContains(response, '<td class="field-converted_generic_org_type">federal</td>', count=1) self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist # Now let's make sure the long description does not exist
self.assertNotContains(response, "Federal: an agency of the U.S. government") self.assertNotContains(response, "Federal: an agency of the U.S. government")
@ -685,9 +685,9 @@ class TestDomainRequestAdmin(MockEppLib):
# Create a sample domain request # Create a sample domain request
domain_request = completed_domain_request(status=in_review, user=_creator) domain_request = completed_domain_request(status=in_review, user=_creator)
# Test the email sent out for already_has_domains # Test the email sent out for already_has_a_domain
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS already_has_a_domain = DomainRequest.ActionNeededReasons.ALREADY_HAS_A_DOMAIN
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains) self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_a_domain)
self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL) self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@ -1693,7 +1693,6 @@ class TestDomainRequestAdmin(MockEppLib):
"notes", "notes",
"alternative_domains", "alternative_domains",
] ]
self.maxDiff = None
self.assertEqual(readonly_fields, expected_fields) self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_analyst(self): def test_readonly_fields_for_analyst(self):
@ -1702,7 +1701,6 @@ class TestDomainRequestAdmin(MockEppLib):
request.user = self.staffuser request.user = self.staffuser
readonly_fields = self.admin.get_readonly_fields(request) readonly_fields = self.admin.get_readonly_fields(request)
self.maxDiff = None
expected_fields = [ expected_fields = [
"portfolio_senior_official", "portfolio_senior_official",
"portfolio_organization_type", "portfolio_organization_type",

View file

@ -63,7 +63,6 @@ class TestGroups(TestCase):
# Get the codenames of actual permissions associated with the group # Get the codenames of actual permissions associated with the group
actual_permissions = [p.codename for p in cisa_analysts_group.permissions.all()] actual_permissions = [p.codename for p in cisa_analysts_group.permissions.all()]
self.maxDiff = None
# Assert that the actual permissions match the expected permissions # Assert that the actual permissions match the expected permissions
self.assertListEqual(actual_permissions, expected_permissions) self.assertListEqual(actual_permissions, expected_permissions)

View file

@ -71,8 +71,8 @@ class CsvReportsTest(MockDbForSharedTests):
fake_open = mock_open() fake_open = mock_open()
expected_file_content = [ expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"), call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"), call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"), call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
] ]
@ -93,8 +93,8 @@ class CsvReportsTest(MockDbForSharedTests):
fake_open = mock_open() fake_open = mock_open()
expected_file_content = [ expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"), call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"), call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"), call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"), call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
@ -251,32 +251,35 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# We expect READY domains, # We expect READY domains,
# sorted alphabetially by domain name # sorted alphabetially by domain name
expected_content = ( expected_content = (
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO," "Domain name,Status,First ready on,Expiration date,Domain type,Agency,"
"SO email,Security contact email,Domain managers,Invited domain managers\n" "Organization name,City,State,SO,SO email,"
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,,(blank),,," "Security contact email,Domain managers,Invited domain managers\n"
"meoward@rocks.com,\n" "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,," "Portfolio 1 Federal Agency,,,, ,,(blank),"
',,,(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
"woofwardthethird@rocks.com\n"
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,"
"squeaker@rocks.com\n"
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"ddomain3.gov,On hold,(blank),2023-11-15,Federal,Armed Forces Retirement Home,,,,,,"
"security@mail.gov,,\n"
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,,(blank),,,"
"meoward@rocks.com,squeaker@rocks.com\n" "meoward@rocks.com,squeaker@rocks.com\n"
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,,(blank),,,meoward@rocks.com,\n" "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
"Portfolio 1 Federal Agency,,,, ,,(blank),"
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
"World War I Centennial Commission,,,, ,,(blank),"
"meoward@rocks.com,\n"
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,"
"squeaker@rocks.com\n"
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
"bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
"bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
"ddomain3.gov,On hold,(blank),2023-11-15,Federal,"
"Armed Forces Retirement Home,,,, ,,security@mail.gov,,\n"
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,(blank),meoward@rocks.com,\n"
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator @less_console_noise_decorator
@ -312,20 +315,17 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# We expect only domains associated with the user # We expect only domains associated with the user
expected_content = ( expected_content = (
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name," "Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
"City,State,SO,SO email," "City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
"Security contact email,Domain managers,Invited domain managers\n" "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,"
'(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
"woofwardthethird@rocks.com\n"
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,(blank),"
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n' '"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator @less_console_noise_decorator
@ -493,17 +493,17 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# sorted alphabetially by domain name # sorted alphabetially by domain name
expected_content = ( expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n" "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n" "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n" "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n" "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
"zdomain12.gov,Interstate,,,,,(blank)\n" "zdomain12.gov,Interstate,,,,,(blank)\n"
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator @less_console_noise_decorator
@ -533,16 +533,16 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# sorted alphabetially by domain name # sorted alphabetially by domain name
expected_content = ( expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n" "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n" "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n" "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n" "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator @less_console_noise_decorator
@ -587,13 +587,13 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
expected_content = ( expected_content = (
"Domain name,Domain type,Agency,Organization name,City," "Domain name,Domain type,Agency,Organization name,City,"
"State,Status,Expiration date, Deleted\n" "State,Status,Expiration date, Deleted\n"
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n" "cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Ready,(blank)\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n" "adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n" "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
"zdomain12.govInterstateReady(blank)\n" "zdomain12.gov,Interstate,Ready,(blank)\n"
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n" "zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank),2024-04-02\n" "sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
"xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n" "xdomain7.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
@ -611,7 +611,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers). squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
She should show twice in this report but not in test_DomainManaged.""" She should show twice in this report but not in test_DomainManaged."""
self.maxDiff = None
# Create a CSV file in memory # Create a CSV file in memory
csv_file = StringIO() csv_file = StringIO()
# Call the export functions # Call the export functions
@ -646,7 +645,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator @less_console_noise_decorator
@ -683,7 +681,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator @less_console_noise_decorator
@ -721,10 +718,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator # @less_console_noise_decorator
def test_domain_request_data_full(self): def test_domain_request_data_full(self):
"""Tests the full domain request report.""" """Tests the full domain request report."""
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data # Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
@ -766,35 +762,34 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
csv_file.seek(0) csv_file.seek(0)
# Read the content into a variable # Read the content into a variable
csv_content = csv_file.read() csv_content = csv_file.read()
expected_content = ( expected_content = (
# Header # Header
"Domain request,Status,Domain type,Federal type," "Domain request,Status,Domain type,Federal type,Federal agency,Organization name,Election office,"
"Federal agency,Organization name,Election office,City,State/territory," "City,State/territory,Region,Creator first name,Creator last name,Creator email,"
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count," "Creator approved domains count,Creator active requests count,Alternative domains,SO first name,"
"Creator active requests count,Alternative domains,SO first name,SO last name,SO email," "SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
"SO title/role,Request purpose,Request additional details,Other contacts,"
"CISA regional representative,Current websites,Investigator\n" "CISA regional representative,Current websites,Investigator\n"
# Content # Content
"city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com," "city5.gov,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n" "Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester," "city2.gov,In review,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
"testy@town.com," "Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n" "city3.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,"
'city3.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"cheeseville.gov, city1.gov,' '"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
'igorville.gov",Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,CISA-first-name ' 'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
"CISA-last-name " 'Testy Tester testy2@town.com",'
'| There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, Testy Tester ' 'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
'testy2@town.com"' "city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,"
',test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n' "Tester,testy@town.com,"
"city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com," "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester " "Testy Tester testy2@town.com,"
"testy2@town.com" "cisaRep@igorville.gov,city.com,\n"
",cisaRep@igorville.gov,city.com,\n" "city6.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
"city6.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com," "Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
"testy2@town.com,"
"cisaRep@igorville.gov,city.com,\n" "cisaRep@igorville.gov,city.com,\n"
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
@ -862,7 +857,6 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
# Create a request and add the user to the request # Create a request and add the user to the request
request = self.factory.get("/") request = self.factory.get("/")
request.user = self.user request.user = self.user
self.maxDiff = None
# Add portfolio to session # Add portfolio to session
request = GenericTestHelper._mock_user_request_for_factory(request) request = GenericTestHelper._mock_user_request_for_factory(request)
request.session["portfolio"] = self.portfolio_1 request.session["portfolio"] = self.portfolio_1

View file

@ -525,6 +525,115 @@ class DomainExport(BaseExport):
# Return the model class that this export handles # Return the model class that this export handles
return DomainInformation return DomainInformation
@classmethod
def get_computed_fields(cls, **kwargs):
"""
Get a dict of computed fields.
"""
# NOTE: These computed fields imitate @Property functions in the Domain model and Portfolio model where needed.
# This is for performance purposes. Since we are working with dictionary values and not
# model objects as we export data, trying to reinstate model objects in order to grab @property
# values negatively impacts performance. Therefore, we will follow best practice and use annotations
return {
"converted_generic_org_type": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
# Otherwise, return the natively assigned value
default=F("generic_org_type"),
output_field=CharField(),
),
"converted_federal_agency": Case(
# When portfolio is present, use its value instead
When(
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
then=F("portfolio__federal_agency__agency"),
),
# Otherwise, return the natively assigned value
default=F("federal_agency__agency"),
output_field=CharField(),
),
"converted_federal_type": Case(
# When portfolio is present, use its value instead
# NOTE: this is an @Property funciton in portfolio.
When(
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
then=F("portfolio__federal_agency__federal_type"),
),
# Otherwise, return the natively assigned value
default=F("federal_type"),
output_field=CharField(),
),
"converted_organization_name": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__organization_name")),
# Otherwise, return the natively assigned value
default=F("organization_name"),
output_field=CharField(),
),
"converted_city": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__city")),
# Otherwise, return the natively assigned value
default=F("city"),
output_field=CharField(),
),
"converted_state_territory": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__state_territory")),
# Otherwise, return the natively assigned value
default=F("state_territory"),
output_field=CharField(),
),
"converted_so_email": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
# Otherwise, return the natively assigned senior official
default=F("senior_official__email"),
output_field=CharField(),
),
"converted_senior_official_last_name": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__senior_official__last_name")),
# Otherwise, return the natively assigned senior official
default=F("senior_official__last_name"),
output_field=CharField(),
),
"converted_senior_official_first_name": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__senior_official__first_name")),
# Otherwise, return the natively assigned senior official
default=F("senior_official__first_name"),
output_field=CharField(),
),
"converted_senior_official_title": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__senior_official__title")),
# Otherwise, return the natively assigned senior official
default=F("senior_official__title"),
output_field=CharField(),
),
"converted_so_name": Case(
# When portfolio is present, use that senior official instead
When(
Q(portfolio__isnull=False) & Q(portfolio__senior_official__isnull=False),
then=Concat(
Coalesce(F("portfolio__senior_official__first_name"), Value("")),
Value(" "),
Coalesce(F("portfolio__senior_official__last_name"), Value("")),
output_field=CharField(),
),
),
# Otherwise, return the natively assigned senior official
default=Concat(
Coalesce(F("senior_official__first_name"), Value("")),
Value(" "),
Coalesce(F("senior_official__last_name"), Value("")),
output_field=CharField(),
),
output_field=CharField(),
),
}
@classmethod @classmethod
def update_queryset(cls, queryset, **kwargs): def update_queryset(cls, queryset, **kwargs):
""" """
@ -614,10 +723,10 @@ class DomainExport(BaseExport):
if first_ready_on is None: if first_ready_on is None:
first_ready_on = "(blank)" first_ready_on = "(blank)"
# organization_type has generic_org_type AND is_election # organization_type has organization_type AND is_election
domain_org_type = model.get("organization_type") domain_org_type = model.get("converted_generic_org_type")
human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type) human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
domain_federal_type = model.get("federal_type") domain_federal_type = model.get("converted_federal_type")
human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type) human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
domain_type = human_readable_domain_org_type domain_type = human_readable_domain_org_type
if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL: if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
@ -640,12 +749,12 @@ class DomainExport(BaseExport):
"First ready on": first_ready_on, "First ready on": first_ready_on,
"Expiration date": expiration_date, "Expiration date": expiration_date,
"Domain type": domain_type, "Domain type": domain_type,
"Agency": model.get("federal_agency__agency"), "Agency": model.get("converted_federal_agency"),
"Organization name": model.get("organization_name"), "Organization name": model.get("converted_organization_name"),
"City": model.get("city"), "City": model.get("converted_city"),
"State": model.get("state_territory"), "State": model.get("converted_state_territory"),
"SO": model.get("so_name"), "SO": model.get("converted_so_name"),
"SO email": model.get("senior_official__email"), "SO email": model.get("converted_so_email"),
"Security contact email": security_contact_email, "Security contact email": security_contact_email,
"Created at": model.get("domain__created_at"), "Created at": model.get("domain__created_at"),
"Deleted": model.get("domain__deleted"), "Deleted": model.get("domain__deleted"),
@ -654,8 +763,23 @@ class DomainExport(BaseExport):
} }
row = [FIELDS.get(column, "") for column in columns] row = [FIELDS.get(column, "") for column in columns]
return row return row
def get_filtered_domain_infos_by_org(domain_infos_to_filter, org_to_filter_by):
"""Returns a list of Domain Requests that has been filtered by the given organization value."""
annotated_queryset = domain_infos_to_filter.annotate(
converted_generic_org_type=Case(
# Recreate the logic of the converted_generic_org_type property
# here in annotations
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
default=F("generic_org_type"),
output_field=CharField(),
)
)
return annotated_queryset.filter(converted_generic_org_type=org_to_filter_by)
@classmethod @classmethod
def get_sliced_domains(cls, filter_condition): def get_sliced_domains(cls, filter_condition):
"""Get filtered domains counts sliced by org type and election office. """Get filtered domains counts sliced by org type and election office.
@ -663,23 +787,51 @@ class DomainExport(BaseExport):
when a domain has more that one manager. when a domain has more that one manager.
""" """
domains = DomainInformation.objects.all().filter(**filter_condition).distinct() domain_informations = DomainInformation.objects.all().filter(**filter_condition).distinct()
domains_count = domains.count() domains_count = domain_informations.count()
federal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count() federal = (
interstate = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).count() cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.FEDERAL)
state_or_territory = ( .distinct()
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count() .count()
)
interstate = cls.get_filtered_domain_infos_by_org(
domain_informations, DomainRequest.OrganizationChoices.INTERSTATE
).count()
state_or_territory = (
cls.get_filtered_domain_infos_by_org(
domain_informations, DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
)
.distinct()
.count()
)
tribal = (
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.TRIBAL)
.distinct()
.count()
)
county = (
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.COUNTY)
.distinct()
.count()
)
city = (
cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.CITY)
.distinct()
.count()
) )
tribal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
county = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
city = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
special_district = ( special_district = (
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count() cls.get_filtered_domain_infos_by_org(
domain_informations, DomainRequest.OrganizationChoices.SPECIAL_DISTRICT
)
.distinct()
.count()
) )
school_district = ( school_district = (
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count() cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.SCHOOL_DISTRICT)
.distinct()
.count()
) )
election_board = domains.filter(is_election_board=True).distinct().count() election_board = domain_informations.filter(is_election_board=True).distinct().count()
return [ return [
domains_count, domains_count,
@ -706,6 +858,7 @@ class DomainDataType(DomainExport):
""" """
Overrides the columns for CSV export specific to DomainExport. Overrides the columns for CSV export specific to DomainExport.
""" """
return [ return [
"Domain name", "Domain name",
"Status", "Status",
@ -723,6 +876,13 @@ class DomainDataType(DomainExport):
"Invited domain managers", "Invited domain managers",
] ]
@classmethod
def get_annotations_for_sort(cls):
"""
Get a dict of annotations to make available for sorting.
"""
return cls.get_computed_fields()
@classmethod @classmethod
def get_sort_fields(cls): def get_sort_fields(cls):
""" """
@ -730,9 +890,9 @@ class DomainDataType(DomainExport):
""" """
# Coalesce is used to replace federal_type of None with ZZZZZ # Coalesce is used to replace federal_type of None with ZZZZZ
return [ return [
"organization_type", "converted_generic_org_type",
Coalesce("federal_type", Value("ZZZZZ")), Coalesce("converted_federal_type", Value("ZZZZZ")),
"federal_agency", "converted_federal_agency",
"domain__name", "domain__name",
] ]
@ -773,20 +933,6 @@ class DomainDataType(DomainExport):
""" """
return ["domain__permissions"] return ["domain__permissions"]
@classmethod
def get_computed_fields(cls, delimiter=", ", **kwargs):
"""
Get a dict of computed fields.
"""
return {
"so_name": Concat(
Coalesce(F("senior_official__first_name"), Value("")),
Value(" "),
Coalesce(F("senior_official__last_name"), Value("")),
output_field=CharField(),
),
}
@classmethod @classmethod
def get_related_table_fields(cls): def get_related_table_fields(cls):
""" """
@ -892,7 +1038,7 @@ class DomainRequestsDataType:
cls.safe_get(getattr(request, "region_field", None)), cls.safe_get(getattr(request, "region_field", None)),
request.status, request.status,
cls.safe_get(getattr(request, "election_office", None)), cls.safe_get(getattr(request, "election_office", None)),
request.federal_type, request.converted_federal_type,
cls.safe_get(getattr(request, "domain_type", None)), cls.safe_get(getattr(request, "domain_type", None)),
cls.safe_get(getattr(request, "additional_details", None)), cls.safe_get(getattr(request, "additional_details", None)),
cls.safe_get(getattr(request, "creator_approved_domains_count", None)), cls.safe_get(getattr(request, "creator_approved_domains_count", None)),
@ -943,6 +1089,13 @@ class DomainDataFull(DomainExport):
"Security contact email", "Security contact email",
] ]
@classmethod
def get_annotations_for_sort(cls, delimiter=", "):
"""
Get a dict of annotations to make available for sorting.
"""
return cls.get_computed_fields()
@classmethod @classmethod
def get_sort_fields(cls): def get_sort_fields(cls):
""" """
@ -950,9 +1103,9 @@ class DomainDataFull(DomainExport):
""" """
# Coalesce is used to replace federal_type of None with ZZZZZ # Coalesce is used to replace federal_type of None with ZZZZZ
return [ return [
"organization_type", "converted_generic_org_type",
Coalesce("federal_type", Value("ZZZZZ")), Coalesce("converted_federal_type", Value("ZZZZZ")),
"federal_agency", "converted_federal_agency",
"domain__name", "domain__name",
] ]
@ -990,20 +1143,6 @@ class DomainDataFull(DomainExport):
], ],
) )
@classmethod
def get_computed_fields(cls, delimiter=", ", **kwargs):
"""
Get a dict of computed fields.
"""
return {
"so_name": Concat(
Coalesce(F("senior_official__first_name"), Value("")),
Value(" "),
Coalesce(F("senior_official__last_name"), Value("")),
output_field=CharField(),
),
}
@classmethod @classmethod
def get_related_table_fields(cls): def get_related_table_fields(cls):
""" """
@ -1037,6 +1176,13 @@ class DomainDataFederal(DomainExport):
"Security contact email", "Security contact email",
] ]
@classmethod
def get_annotations_for_sort(cls, delimiter=", "):
"""
Get a dict of annotations to make available for sorting.
"""
return cls.get_computed_fields()
@classmethod @classmethod
def get_sort_fields(cls): def get_sort_fields(cls):
""" """
@ -1044,9 +1190,9 @@ class DomainDataFederal(DomainExport):
""" """
# Coalesce is used to replace federal_type of None with ZZZZZ # Coalesce is used to replace federal_type of None with ZZZZZ
return [ return [
"organization_type", "converted_generic_org_type",
Coalesce("federal_type", Value("ZZZZZ")), Coalesce("converted_federal_type", Value("ZZZZZ")),
"federal_agency", "converted_federal_agency",
"domain__name", "domain__name",
] ]
@ -1085,20 +1231,6 @@ class DomainDataFederal(DomainExport):
], ],
) )
@classmethod
def get_computed_fields(cls, delimiter=", ", **kwargs):
"""
Get a dict of computed fields.
"""
return {
"so_name": Concat(
Coalesce(F("senior_official__first_name"), Value("")),
Value(" "),
Coalesce(F("senior_official__last_name"), Value("")),
output_field=CharField(),
),
}
@classmethod @classmethod
def get_related_table_fields(cls): def get_related_table_fields(cls):
""" """
@ -1476,24 +1608,180 @@ class DomainRequestExport(BaseExport):
# Return the model class that this export handles # Return the model class that this export handles
return DomainRequest return DomainRequest
def get_filtered_domain_requests_by_org(domain_requests_to_filter, org_to_filter_by):
"""Returns a list of Domain Requests that has been filtered by the given organization value"""
annotated_queryset = domain_requests_to_filter.annotate(
converted_generic_org_type=Case(
# Recreate the logic of the converted_generic_org_type property
# here in annotations
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
default=F("generic_org_type"),
output_field=CharField(),
)
)
return annotated_queryset.filter(converted_generic_org_type=org_to_filter_by)
# return domain_requests_to_filter.filter(
# # Filter based on the generic org value returned by converted_generic_org_type
# id__in=[
# domainRequest.id
# for domainRequest in domain_requests_to_filter
# if domainRequest.converted_generic_org_type
# and domainRequest.converted_generic_org_type == org_to_filter_by
# ]
# )
@classmethod
def get_computed_fields(cls, delimiter=", ", **kwargs):
"""
Get a dict of computed fields.
"""
# NOTE: These computed fields imitate @Property functions in the Domain model and Portfolio model where needed.
# This is for performance purposes. Since we are working with dictionary values and not
# model objects as we export data, trying to reinstate model objects in order to grab @property
# values negatively impacts performance. Therefore, we will follow best practice and use annotations
return {
"converted_generic_org_type": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
# Otherwise, return the natively assigned value
default=F("generic_org_type"),
output_field=CharField(),
),
"converted_federal_agency": Case(
# When portfolio is present, use its value instead
When(
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
then=F("portfolio__federal_agency__agency"),
),
# Otherwise, return the natively assigned value
default=F("federal_agency__agency"),
output_field=CharField(),
),
"converted_federal_type": Case(
# When portfolio is present, use its value instead
# NOTE: this is an @Property funciton in portfolio.
When(
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
then=F("portfolio__federal_agency__federal_type"),
),
# Otherwise, return the natively assigned value
default=F("federal_type"),
output_field=CharField(),
),
"converted_organization_name": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__organization_name")),
# Otherwise, return the natively assigned value
default=F("organization_name"),
output_field=CharField(),
),
"converted_city": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__city")),
# Otherwise, return the natively assigned value
default=F("city"),
output_field=CharField(),
),
"converted_state_territory": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__state_territory")),
# Otherwise, return the natively assigned value
default=F("state_territory"),
output_field=CharField(),
),
"converted_so_email": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
# Otherwise, return the natively assigned senior official
default=F("senior_official__email"),
output_field=CharField(),
),
"converted_senior_official_last_name": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__senior_official__last_name")),
# Otherwise, return the natively assigned senior official
default=F("senior_official__last_name"),
output_field=CharField(),
),
"converted_senior_official_first_name": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__senior_official__first_name")),
# Otherwise, return the natively assigned senior official
default=F("senior_official__first_name"),
output_field=CharField(),
),
"converted_senior_official_title": Case(
# When portfolio is present, use its value instead
When(portfolio__isnull=False, then=F("portfolio__senior_official__title")),
# Otherwise, return the natively assigned senior official
default=F("senior_official__title"),
output_field=CharField(),
),
"converted_so_name": Case(
# When portfolio is present, use that senior official instead
When(
Q(portfolio__isnull=False) & Q(portfolio__senior_official__isnull=False),
then=Concat(
Coalesce(F("portfolio__senior_official__first_name"), Value("")),
Value(" "),
Coalesce(F("portfolio__senior_official__last_name"), Value("")),
output_field=CharField(),
),
),
# Otherwise, return the natively assigned senior official
default=Concat(
Coalesce(F("senior_official__first_name"), Value("")),
Value(" "),
Coalesce(F("senior_official__last_name"), Value("")),
output_field=CharField(),
),
output_field=CharField(),
),
}
@classmethod @classmethod
def get_sliced_requests(cls, filter_condition): def get_sliced_requests(cls, filter_condition):
"""Get filtered requests counts sliced by org type and election office.""" """Get filtered requests counts sliced by org type and election office."""
requests = DomainRequest.objects.all().filter(**filter_condition).distinct() requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
requests_count = requests.count() requests_count = requests.count()
federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count() federal = (
interstate = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).distinct().count() cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.FEDERAL)
state_or_territory = ( .distinct()
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count() .count()
)
interstate = (
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.INTERSTATE)
.distinct()
.count()
)
state_or_territory = (
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.STATE_OR_TERRITORY)
.distinct()
.count()
)
tribal = (
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.TRIBAL)
.distinct()
.count()
)
county = (
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.COUNTY)
.distinct()
.count()
)
city = (
cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.CITY).distinct().count()
) )
tribal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
county = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
city = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
special_district = ( special_district = (
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count() cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.SPECIAL_DISTRICT)
.distinct()
.count()
) )
school_district = ( school_district = (
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count() cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.SCHOOL_DISTRICT)
.distinct()
.count()
) )
election_board = requests.filter(is_election_board=True).distinct().count() election_board = requests.filter(is_election_board=True).distinct().count()
@ -1517,11 +1805,11 @@ class DomainRequestExport(BaseExport):
""" """
# Handle the federal_type field. Defaults to the wrong format. # Handle the federal_type field. Defaults to the wrong format.
federal_type = model.get("federal_type") federal_type = model.get("converted_federal_type")
human_readable_federal_type = BranchChoices.get_branch_label(federal_type) if federal_type else None human_readable_federal_type = BranchChoices.get_branch_label(federal_type) if federal_type else None
# Handle the org_type field # Handle the org_type field
org_type = model.get("generic_org_type") or model.get("organization_type") org_type = model.get("converted_generic_org_type")
human_readable_org_type = DomainRequest.OrganizationChoices.get_org_label(org_type) if org_type else None human_readable_org_type = DomainRequest.OrganizationChoices.get_org_label(org_type) if org_type else None
# Handle the status field. Defaults to the wrong format. # Handle the status field. Defaults to the wrong format.
@ -1569,19 +1857,19 @@ class DomainRequestExport(BaseExport):
"Other contacts": model.get("all_other_contacts"), "Other contacts": model.get("all_other_contacts"),
"Current websites": model.get("all_current_websites"), "Current websites": model.get("all_current_websites"),
# Untouched FK fields - passed into the request dict. # Untouched FK fields - passed into the request dict.
"Federal agency": model.get("federal_agency__agency"), "Federal agency": model.get("converted_federal_agency"),
"SO first name": model.get("senior_official__first_name"), "SO first name": model.get("converted_senior_official_first_name"),
"SO last name": model.get("senior_official__last_name"), "SO last name": model.get("converted_senior_official_last_name"),
"SO email": model.get("senior_official__email"), "SO email": model.get("converted_so_email"),
"SO title/role": model.get("senior_official__title"), "SO title/role": model.get("converted_senior_official_title"),
"Creator first name": model.get("creator__first_name"), "Creator first name": model.get("creator__first_name"),
"Creator last name": model.get("creator__last_name"), "Creator last name": model.get("creator__last_name"),
"Creator email": model.get("creator__email"), "Creator email": model.get("creator__email"),
"Investigator": model.get("investigator__email"), "Investigator": model.get("investigator__email"),
# Untouched fields # Untouched fields
"Organization name": model.get("organization_name"), "Organization name": model.get("converted_organization_name"),
"City": model.get("city"), "City": model.get("converted_city"),
"State/territory": model.get("state_territory"), "State/territory": model.get("converted_state_territory"),
"Request purpose": model.get("purpose"), "Request purpose": model.get("purpose"),
"CISA regional representative": model.get("cisa_representative_email"), "CISA regional representative": model.get("cisa_representative_email"),
"Last submitted date": model.get("last_submitted_date"), "Last submitted date": model.get("last_submitted_date"),
@ -1724,24 +2012,34 @@ class DomainRequestDataFull(DomainRequestExport):
""" """
Get a dict of computed fields. Get a dict of computed fields.
""" """
return { # Get computed fields from the parent class
"creator_approved_domains_count": cls.get_creator_approved_domains_count_query(), computed_fields = super().get_computed_fields()
"creator_active_requests_count": cls.get_creator_active_requests_count_query(),
"all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True), # Add additional computed fields
"all_alternative_domains": StringAgg("alternative_domains__website", delimiter=delimiter, distinct=True), computed_fields.update(
# Coerce the other contacts object to "{first_name} {last_name} {email}" {
"all_other_contacts": StringAgg( "creator_approved_domains_count": cls.get_creator_approved_domains_count_query(),
Concat( "creator_active_requests_count": cls.get_creator_active_requests_count_query(),
"other_contacts__first_name", "all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True),
Value(" "), "all_alternative_domains": StringAgg(
"other_contacts__last_name", "alternative_domains__website", delimiter=delimiter, distinct=True
Value(" "),
"other_contacts__email",
), ),
delimiter=delimiter, # Coerce the other contacts object to "{first_name} {last_name} {email}"
distinct=True, "all_other_contacts": StringAgg(
), Concat(
} "other_contacts__first_name",
Value(" "),
"other_contacts__last_name",
Value(" "),
"other_contacts__email",
),
delimiter=delimiter,
distinct=True,
),
}
)
return computed_fields
@classmethod @classmethod
def get_related_table_fields(cls): def get_related_table_fields(cls):

View file

@ -13,6 +13,7 @@ from .domain import (
DomainAddUserView, DomainAddUserView,
DomainInvitationCancelView, DomainInvitationCancelView,
DomainDeleteUserView, DomainDeleteUserView,
PrototypeDomainDNSRecordView,
) )
from .user_profile import UserProfileView, FinishProfileSetupView from .user_profile import UserProfileView, FinishProfileSetupView
from .health import * from .health import *

View file

@ -7,7 +7,7 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionCancelView).
from datetime import date from datetime import date
import logging import logging
import requests
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError from django.db import IntegrityError
@ -64,6 +64,7 @@ from epplibwrapper import (
from ..utility.email import send_templated_email, EmailSendingError from ..utility.email import send_templated_email, EmailSendingError
from .utility import DomainPermissionView, DomainInvitationPermissionCancelView from .utility import DomainPermissionView, DomainInvitationPermissionCancelView
from django import forms
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -454,6 +455,216 @@ class DomainDNSView(DomainBaseView):
"""DNS Information View.""" """DNS Information View."""
template_name = "domain_dns.html" template_name = "domain_dns.html"
valid_domains = ["igorville.gov", "domainops.gov", "dns.gov"]
def get_context_data(self, **kwargs):
"""Adds custom context."""
context = super().get_context_data(**kwargs)
context["dns_prototype_flag"] = flag_is_active_for_user(self.request.user, "dns_prototype_flag")
context["is_valid_domain"] = self.object.name in self.valid_domains
return context
class PrototypeDomainDNSRecordForm(forms.Form):
"""Form for adding DNS records in prototype."""
name = forms.CharField(label="DNS record name (A record)", required=True, help_text="DNS record name")
content = forms.GenericIPAddressField(
label="IPv4 Address",
required=True,
protocol="IPv4",
)
ttl = forms.ChoiceField(
label="TTL",
choices=[
(1, "Automatic"),
(60, "1 minute"),
(300, "5 minutes"),
(1800, "30 minutes"),
(3600, "1 hour"),
(7200, "2 hours"),
(18000, "5 hours"),
(43200, "12 hours"),
(86400, "1 day"),
],
initial=1,
)
class PrototypeDomainDNSRecordView(DomainFormBaseView):
template_name = "prototype_domain_dns.html"
form_class = PrototypeDomainDNSRecordForm
valid_domains = ["igorville.gov", "domainops.gov", "dns.gov"]
def has_permission(self):
has_permission = super().has_permission()
if not has_permission:
return False
flag_enabled = flag_is_active_for_user(self.request.user, "dns_prototype_flag")
if not flag_enabled:
return False
self.object = self.get_object()
if self.object.name not in self.valid_domains:
return False
return True
def get_success_url(self):
return reverse("prototype-domain-dns", kwargs={"pk": self.object.pk})
def find_by_name(self, items, name):
"""Find an item by name in a list of dictionaries."""
return next((item.get("id") for item in items if item.get("name") == name), None)
def post(self, request, *args, **kwargs):
"""Handle form submission."""
self.object = self.get_object()
form = self.get_form()
errors = []
if form.is_valid():
try:
if settings.IS_PRODUCTION and self.object.name != "igorville.gov":
raise Exception(f"create dns record was called for domain {self.name}")
if not settings.IS_PRODUCTION and self.object.name not in self.valid_domains:
raise Exception(
f"Can only create DNS records for: {self.valid_domains}."
" Create one in a test environment if it doesn't already exist."
)
base_url = "https://api.cloudflare.com/client/v4"
headers = {
"X-Auth-Email": settings.SECRET_REGISTRY_SERVICE_EMAIL,
"X-Auth-Key": settings.SECRET_REGISTRY_TENANT_KEY,
"Content-Type": "application/json",
}
params = {"tenant_name": settings.SECRET_REGISTRY_TENANT_NAME}
# 1. Get tenant details
tenant_response = requests.get(f"{base_url}/user/tenants", headers=headers, params=params, timeout=5)
tenant_response_json = tenant_response.json()
logger.info(f"Found tenant: {tenant_response_json}")
tenant_id = tenant_response_json["result"][0]["tenant_tag"]
errors = tenant_response_json.get("errors", [])
tenant_response.raise_for_status()
# 2. Create or get a account under tenant
# Check to see if the account already exists. Filters accounts by tenant_id / account_name.
account_name = f"account-{self.object.name}"
params = {"tenant_id": tenant_id, "name": account_name}
account_response = requests.get(f"{base_url}/accounts", headers=headers, params=params, timeout=5)
account_response_json = account_response.json()
logger.debug(f"account get: {account_response_json}")
errors = account_response_json.get("errors", [])
account_response.raise_for_status()
# See if we already made an account.
# This maybe doesn't need to be a for loop (1 record or 0) but alas, here we are
accounts = account_response_json.get("result", [])
account_id = self.find_by_name(accounts, account_name)
# If we didn't, create one
if not account_id:
account_response = requests.post(
f"{base_url}/accounts",
headers=headers,
json={"name": account_name, "type": "enterprise", "unit": {"id": tenant_id}},
timeout=5,
)
account_response_json = account_response.json()
logger.info(f"Created account: {account_response_json}")
account_id = account_response_json["result"]["id"]
errors = account_response_json.get("errors", [])
account_response.raise_for_status()
# 3. Create or get a zone under account
# Try to find an existing zone first by searching on the current id
zone_name = self.object.name
params = {"account.id": account_id, "name": zone_name}
zone_response = requests.get(f"{base_url}/zones", headers=headers, params=params, timeout=5)
zone_response_json = zone_response.json()
logger.debug(f"get zone: {zone_response_json}")
errors = zone_response_json.get("errors", [])
zone_response.raise_for_status()
# Get the zone id
zones = zone_response_json.get("result", [])
zone_id = self.find_by_name(zones, zone_name)
# Create one if it doesn't presently exist
if not zone_id:
zone_response = requests.post(
f"{base_url}/zones",
headers=headers,
json={"name": zone_name, "account": {"id": account_id}, "type": "full"},
timeout=5,
)
zone_response_json = zone_response.json()
logger.info(f"Created zone: {zone_response_json}")
zone_id = zone_response_json.get("result", {}).get("id")
errors = zone_response_json.get("errors", [])
zone_response.raise_for_status()
# 4. Add or get a zone subscription
# See if one already exists
subscription_response = requests.get(
f"{base_url}/zones/{zone_id}/subscription", headers=headers, timeout=5
)
subscription_response_json = subscription_response.json()
logger.debug(f"get subscription: {subscription_response_json}")
# Create a subscription if one doesn't exist already.
# If it doesn't, we get this error message (code 1207):
# Add a core subscription first and try again. The zone does not have an active core subscription.
# Note that status code and error code are different here.
if subscription_response.status_code == 404:
subscription_response = requests.post(
f"{base_url}/zones/{zone_id}/subscription",
headers=headers,
json={"rate_plan": {"id": "PARTNERS_ENT"}, "frequency": "annual"},
timeout=5,
)
subscription_response.raise_for_status()
subscription_response_json = subscription_response.json()
logger.info(f"Created subscription: {subscription_response_json}")
else:
subscription_response.raise_for_status()
# # 5. Create DNS record
# # Format the DNS record according to Cloudflare's API requirements
dns_response = requests.post(
f"{base_url}/zones/{zone_id}/dns_records",
headers=headers,
json={
"type": "A",
"name": form.cleaned_data["name"],
"content": form.cleaned_data["content"],
"ttl": int(form.cleaned_data["ttl"]),
"comment": "Test record (will need clean up)",
},
timeout=5,
)
dns_response_json = dns_response.json()
logger.info(f"Created DNS record: {dns_response_json}")
errors = dns_response_json.get("errors", [])
dns_response.raise_for_status()
dns_name = dns_response_json["result"]["name"]
messages.success(request, f"DNS A record '{dns_name}' created successfully.")
except Exception as err:
logger.error(f"Error creating DNS A record for {self.object.name}: {err}")
messages.error(request, f"An error occurred: {err}")
finally:
if errors:
messages.error(request, f"Request errors: {errors}")
return super().post(request)
class DomainNameserversView(DomainFormBaseView): class DomainNameserversView(DomainFormBaseView):

View file

@ -626,34 +626,3 @@ class NewMemberView(PortfolioMembersPermissionView, FormMixin):
if permission_exists: if permission_exists:
messages.warning(self.request, "User is already a member of this portfolio.") messages.warning(self.request, "User is already a member of this portfolio.")
return redirect(self.get_success_url()) return redirect(self.get_success_url())
# look up a user with that email
try:
requested_user = User.objects.get(email=requested_email)
except User.DoesNotExist:
# no matching user, go make an invitation
return self._make_invitation(requested_email, requestor)
else:
# If user already exists, check to see if they are part of the portfolio already
# If they are already part of the portfolio, raise an error. Otherwise, send an invite.
existing_user = UserPortfolioPermission.objects.get(user=requested_user, portfolio=self.object)
if existing_user:
messages.warning(self.request, "User is already a member of this portfolio.")
else:
try:
self._send_portfolio_invitation_email(requested_email, requestor, add_success=False)
except EmailSendingError:
logger.warn(
"Could not send email invitation (EmailSendingError)",
self.object,
exc_info=True,
)
messages.warning(self.request, "Could not send email invitation.")
except Exception:
logger.warn(
"Could not send email invitation (Other Exception)",
self.object,
exc_info=True,
)
messages.warning(self.request, "Could not send email invitation.")
return redirect(self.get_success_url())