diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 5db51b539..0521a71be 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -1,4 +1,3 @@
-from collections import Counter
import csv
import logging
from datetime import datetime
@@ -8,7 +7,7 @@ from registrar.models.domain_request import DomainRequest
from registrar.models.domain_information import DomainInformation
from django.utils import timezone
from django.core.paginator import Paginator
-from django.db.models import F, Value, CharField
+from django.db.models import F, Value, CharField, Q, Count
from django.db.models.functions import Concat, Coalesce
from registrar.models.public_contact import PublicContact
@@ -54,7 +53,7 @@ def get_domain_infos(filter_condition, sort_fields):
return domain_infos_cleaned
-def parse_domain_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False):
+def parse_domain_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False, invites_with_invited_status=None):
"""Given a set of columns, generate a new row from cleaned column data"""
# Domain should never be none when parsing this information
@@ -108,7 +107,7 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
# Get lists of emails for active and invited domain managers
dm_active_emails = [dm.user.email for dm in domain.permissions.all()]
dm_invited_emails = [
- invite.email for invite in domain.invitations.filter(status=DomainInvitation.DomainInvitationStatus.INVITED)
+ invite.email for invite in invites_with_invited_status.filter(domain=domain)
]
# Set up the "matching headers" + row field data for email and status
@@ -149,32 +148,25 @@ def _get_security_emails(sec_contact_ids):
def update_columns_with_domain_managers(
- domain_info, update_columns, columns, max_dm_active, max_dm_invited, max_dm_total
+ domain_info,invites_with_invited_status, update_columns, columns, max_dm_total
):
"""Helper function that works with 'global' variables set in write_domains_csv
Accepts:
domain_info -> Domains to parse
update_columns -> A control to make sure we only run the columns test and update when needed
columns -> The header cells in the csv that's under construction
- max_dm_active -> Starts at 0 and gets updated and passed again through this method
- max_dm_invited -> Starts at 0 and gets updated and passed again through this method
max_dm_total -> Starts at 0 and gets updated and passed again through this method
Returns:
Updated update_columns, columns, max_dm_active, max_dm_invited, max_dm_total"""
dm_active = domain_info.domain.permissions.count()
- dm_invited = domain_info.domain.invitations.filter(status=DomainInvitation.DomainInvitationStatus.INVITED).count()
+ dm_invited = invites_with_invited_status.filter(domain=domain_info.domain).count()
- if dm_active > max_dm_active:
- max_dm_active = max(dm_active, max_dm_active)
- update_columns = True
-
- if dm_invited > max_dm_invited:
- max_dm_invited = max(dm_invited, max_dm_invited)
+ if dm_active + dm_invited > max_dm_total:
+ max_dm_total = dm_active + dm_invited
update_columns = True
if update_columns:
- max_dm_total = max_dm_active + max_dm_invited
for i in range(1, max_dm_total + 1):
column_name = f"Domain manager {i}"
column2_name = f"DM{i} status"
@@ -183,7 +175,7 @@ def update_columns_with_domain_managers(
columns.append(column2_name)
update_columns = False
- return update_columns, columns, max_dm_active, max_dm_invited, max_dm_total
+ return update_columns, columns, max_dm_total
def write_domains_csv(
@@ -213,10 +205,18 @@ def write_domains_csv(
# We get the number of domain managers (DMs) an the domain
# that has the most DMs so we can set the header row appropriately
- max_dm_active = 0
- max_dm_invited = 0
+
max_dm_total = 0
update_columns = False
+ invites_with_invited_status=None
+
+ if get_domain_managers:
+ invites_with_invited_status = DomainInvitation.objects.filter(status=DomainInvitation.DomainInvitationStatus.INVITED).prefetch_related("domain")
+
+ # zander = DomainInformation.objects.filter(**filter_condition).annotate(invitations_count=Count('invitation', filter=Q(invitation__status='invited'))).values_list('domain_name', 'invitations_count')
+ # logger.info(f'zander {zander}')
+ # zander_dict = dict(zander)
+ # logger.info(f'zander_dict {zander_dict}')
# This var will live outside of the nested for loops to aggregate
# the data from those loops
@@ -229,14 +229,14 @@ def write_domains_csv(
# Get max number of domain managers
if get_domain_managers:
- update_columns, columns, max_dm_active, max_dm_invited, max_dm_total = (
+ update_columns, columns, max_dm_total = (
update_columns_with_domain_managers(
- domain_info, update_columns, columns, max_dm_active, max_dm_invited, max_dm_total
+ domain_info,invites_with_invited_status, update_columns, columns, max_dm_total
)
)
try:
- row = parse_domain_row(columns, domain_info, security_emails_dict, get_domain_managers)
+ row = parse_domain_row(columns, domain_info, security_emails_dict, get_domain_managers, invites_with_invited_status)
rows.append(row)
except ValueError:
# This should not happen. If it does, just skip this row.
@@ -518,58 +518,23 @@ def get_sliced_domains(filter_condition, distinct=False):
when a domain has more that one manager.
"""
- # Round trip 1: Get distinct domain names based on filter condition
- domains_count = DomainInformation.objects.filter(**filter_condition).distinct().count()
-
- # Round trip 2: Get counts for other slices
- # This will require either 8 filterd and distinct DB round trips,
- # or 2 DB round trips plus iteration on domain_permissions for each domain
- if distinct:
- generic_org_types_query = DomainInformation.objects.filter(**filter_condition).values_list(
- "domain_id", "generic_org_type"
- )
- # Initialize Counter to store counts for each generic_org_type
- generic_org_type_counts = Counter()
-
- # Keep track of domains already counted
- domains_counted = set()
-
- # Iterate over distinct domains
- for domain_id, generic_org_type in generic_org_types_query:
- # Check if the domain has already been counted
- if domain_id in domains_counted:
- continue
-
- # Get all permissions for the current domain
- domain_permissions = DomainInformation.objects.filter(domain_id=domain_id, **filter_condition).values_list(
- "domain__permissions", flat=True
- )
-
- # Check if the domain has multiple permissions
- if len(domain_permissions) > 0:
- # Mark the domain as counted
- domains_counted.add(domain_id)
-
- # Increment the count for the corresponding generic_org_type
- generic_org_type_counts[generic_org_type] += 1
- else:
- generic_org_types_query = DomainInformation.objects.filter(**filter_condition).values_list(
- "generic_org_type", flat=True
- )
- generic_org_type_counts = Counter(generic_org_types_query)
-
- # Extract counts for each generic_org_type
- federal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
- interstate = generic_org_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
- state_or_territory = generic_org_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
- tribal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0)
- county = generic_org_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0)
- city = generic_org_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0)
- special_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0)
- school_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0)
-
- # Round trip 3
- election_board = DomainInformation.objects.filter(is_election_board=True, **filter_condition).distinct().count()
+ domains = DomainInformation.objects.all().filter(**filter_condition).distinct()
+ domains_count = domains.count()
+ federal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
+ interstate = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).count()
+ state_or_territory = (
+ domains.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
+ )
+ tribal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
+ county = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
+ city = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
+ special_district = (
+ domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count()
+ )
+ school_district = (
+ domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count()
+ )
+ election_board = domains.filter(is_election_board=True).distinct().count()
return [
domains_count,
@@ -588,26 +553,23 @@ def get_sliced_domains(filter_condition, distinct=False):
def get_sliced_requests(filter_condition):
"""Get filtered requests counts sliced by org type and election office."""
- # Round trip 1: Get distinct requests based on filter condition
- requests_count = DomainRequest.objects.filter(**filter_condition).distinct().count()
-
- # Round trip 2: Get counts for other slices
- generic_org_types_query = DomainRequest.objects.filter(**filter_condition).values_list(
- "generic_org_type", flat=True
+ requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
+ requests_count = requests.count()
+ federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
+ interstate = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).distinct().count()
+ state_or_territory = (
+ requests.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
)
- generic_org_type_counts = Counter(generic_org_types_query)
-
- federal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
- interstate = generic_org_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
- state_or_territory = generic_org_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
- tribal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0)
- county = generic_org_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0)
- city = generic_org_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0)
- special_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0)
- school_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0)
-
- # Round trip 3
- election_board = DomainRequest.objects.filter(is_election_board=True, **filter_condition).distinct().count()
+ tribal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
+ county = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
+ city = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
+ special_district = (
+ requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count()
+ )
+ school_district = (
+ requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count()
+ )
+ election_board = requests.filter(is_election_board=True).distinct().count()
return [
requests_count,
diff --git a/src/registrar/views/admin_views.py b/src/registrar/views/admin_views.py
index e33dea407..eba8423ed 100644
--- a/src/registrar/views/admin_views.py
+++ b/src/registrar/views/admin_views.py
@@ -41,29 +41,29 @@ class AnalyticsView(View):
start_date_formatted = csv_export.format_start_date(start_date)
end_date_formatted = csv_export.format_end_date(end_date)
- # filter_managed_domains_start_date = {
- # "domain__permissions__isnull": False,
- # "domain__first_ready__lte": start_date_formatted,
- # }
- # filter_managed_domains_end_date = {
- # "domain__permissions__isnull": False,
- # "domain__first_ready__lte": end_date_formatted,
- # }
- # managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date, True)
- # managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date, True)
+ filter_managed_domains_start_date = {
+ "domain__permissions__isnull": False,
+ "domain__first_ready__lte": start_date_formatted,
+ }
+ filter_managed_domains_end_date = {
+ "domain__permissions__isnull": False,
+ "domain__first_ready__lte": end_date_formatted,
+ }
+ managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date, True)
+ managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date, True)
- # filter_unmanaged_domains_start_date = {
- # "domain__permissions__isnull": True,
- # "domain__first_ready__lte": start_date_formatted,
- # }
- # filter_unmanaged_domains_end_date = {
- # "domain__permissions__isnull": True,
- # "domain__first_ready__lte": end_date_formatted,
- # }
- # unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(
- # filter_unmanaged_domains_start_date, True
- # )
- # unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date, True)
+ filter_unmanaged_domains_start_date = {
+ "domain__permissions__isnull": True,
+ "domain__first_ready__lte": start_date_formatted,
+ }
+ filter_unmanaged_domains_end_date = {
+ "domain__permissions__isnull": True,
+ "domain__first_ready__lte": end_date_formatted,
+ }
+ unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(
+ filter_unmanaged_domains_start_date, True
+ )
+ unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date, True)
filter_ready_domains_start_date = {
"domain__state__in": [models.Domain.State.READY],
@@ -120,10 +120,10 @@ class AnalyticsView(View):
last_30_days_applications=last_30_days_applications.count(),
last_30_days_approved_applications=last_30_days_approved_applications.count(),
average_application_approval_time_last_30_days=avg_approval_time_display,
- # managed_domains_sliced_at_start_date=managed_domains_sliced_at_start_date,
- # unmanaged_domains_sliced_at_start_date=unmanaged_domains_sliced_at_start_date,
- # managed_domains_sliced_at_end_date=managed_domains_sliced_at_end_date,
- # unmanaged_domains_sliced_at_end_date=unmanaged_domains_sliced_at_end_date,
+ managed_domains_sliced_at_start_date=managed_domains_sliced_at_start_date,
+ unmanaged_domains_sliced_at_start_date=unmanaged_domains_sliced_at_start_date,
+ managed_domains_sliced_at_end_date=managed_domains_sliced_at_end_date,
+ unmanaged_domains_sliced_at_end_date=unmanaged_domains_sliced_at_end_date,
ready_domains_sliced_at_start_date=ready_domains_sliced_at_start_date,
deleted_domains_sliced_at_start_date=deleted_domains_sliced_at_start_date,
ready_domains_sliced_at_end_date=ready_domains_sliced_at_end_date,
From 58b8e4649dfb61ed24c5f193d4f1bf9538b131bc Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 10:12:25 -0600
Subject: [PATCH 21/80] Refactor
---
src/registrar/admin.py | 3 +-
...ninformation_organization_type_and_more.py | 83 +++++++++++++
.../0081_domainrequest_organization_type.py | 38 ------
src/registrar/models/domain_information.py | 18 ++-
src/registrar/models/domain_request.py | 57 +++++++--
src/registrar/signals.py | 109 ++++++++++++------
6 files changed, 224 insertions(+), 84 deletions(-)
create mode 100644 src/registrar/migrations/0081_domaininformation_organization_type_and_more.py
delete mode 100644 src/registrar/migrations/0081_domainrequest_organization_type.py
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index d179d5549..f2204e543 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -883,8 +883,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
"Type of organization",
{
"fields": [
- "generic_org_type",
- "is_election_board",
+ "organization_type",
"federal_type",
"federal_agency",
"tribe_name",
diff --git a/src/registrar/migrations/0081_domaininformation_organization_type_and_more.py b/src/registrar/migrations/0081_domaininformation_organization_type_and_more.py
new file mode 100644
index 000000000..8b3818b16
--- /dev/null
+++ b/src/registrar/migrations/0081_domaininformation_organization_type_and_more.py
@@ -0,0 +1,83 @@
+# Generated by Django 4.2.10 on 2024-04-01 15:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("registrar", "0080_create_groups_v09"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="domaininformation",
+ name="organization_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("federal", "Federal"),
+ ("interstate", "Interstate"),
+ ("state_or_territory", "State or territory"),
+ ("tribal", "Tribal"),
+ ("county", "County"),
+ ("city", "City"),
+ ("special_district", "Special district"),
+ ("school_district", "School district"),
+ ("state_or_territory_election", "State or territory - Election"),
+ ("tribal_election", "Tribal - Election"),
+ ("county_election", "County - Election"),
+ ("city_election", "City - Election"),
+ ("special_district_election", "Special district - Election"),
+ ],
+ help_text="Type of organization - Election office",
+ max_length=255,
+ null=True,
+ ),
+ ),
+ migrations.AddField(
+ model_name="domainrequest",
+ name="organization_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("federal", "Federal"),
+ ("interstate", "Interstate"),
+ ("state_or_territory", "State or territory"),
+ ("tribal", "Tribal"),
+ ("county", "County"),
+ ("city", "City"),
+ ("special_district", "Special district"),
+ ("school_district", "School district"),
+ ("state_or_territory_election", "State or territory - Election"),
+ ("tribal_election", "Tribal - Election"),
+ ("county_election", "County - Election"),
+ ("city_election", "City - Election"),
+ ("special_district_election", "Special district - Election"),
+ ],
+ help_text="Type of organization - Election office",
+ max_length=255,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="generic_org_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("federal", "Federal"),
+ ("interstate", "Interstate"),
+ ("state_or_territory", "State or territory"),
+ ("tribal", "Tribal"),
+ ("county", "County"),
+ ("city", "City"),
+ ("special_district", "Special district"),
+ ("school_district", "School district"),
+ ],
+ help_text="Type of organization",
+ max_length=255,
+ null=True,
+ ),
+ ),
+ ]
diff --git a/src/registrar/migrations/0081_domainrequest_organization_type.py b/src/registrar/migrations/0081_domainrequest_organization_type.py
deleted file mode 100644
index 1d2185fbb..000000000
--- a/src/registrar/migrations/0081_domainrequest_organization_type.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Generated by Django 4.2.10 on 2024-03-29 15:58
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("registrar", "0080_create_groups_v09"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="domainrequest",
- name="organization_type",
- field=models.CharField(
- blank=True,
- choices=[
- ("federal", "Federal"),
- ("interstate", "Interstate"),
- ("state_or_territory", "State or territory"),
- ("tribal", "Tribal"),
- ("county", "County"),
- ("city", "City"),
- ("special_district", "Special district"),
- ("school_district", "School district"),
- ("state_or_territory_election", "State or territory - Election"),
- ("tribal_election", "Tribal - Election"),
- ("county_election", "County - Election"),
- ("city_election", "City - Election"),
- ("special_district_election", "Special district - Election"),
- ],
- help_text="Type of organization - Election office",
- max_length=255,
- null=True,
- ),
- ),
- ]
diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py
index b5755a3c9..f41e7d5c6 100644
--- a/src/registrar/models/domain_information.py
+++ b/src/registrar/models/domain_information.py
@@ -54,7 +54,23 @@ class DomainInformation(TimeStampedModel):
choices=OrganizationChoices.choices,
null=True,
blank=True,
- help_text="Type of Organization",
+ help_text="Type of organization",
+ )
+
+ # TODO - Ticket #1911: stub this data from DomainRequest
+ is_election_board = models.BooleanField(
+ null=True,
+ blank=True,
+ help_text="Is your organization an election office?",
+ )
+
+ # TODO - Ticket #1911: stub this data from DomainRequest
+ organization_type = models.CharField(
+ max_length=255,
+ choices=DomainRequest.OrgChoicesElectionOffice.choices,
+ null=True,
+ blank=True,
+ help_text="Type of organization - Election office",
)
federally_recognized_tribe = models.BooleanField(
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 2b08bf1d0..0293fd124 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -101,7 +101,7 @@ class DomainRequest(TimeStampedModel):
"""
Primary organization choices:
For use in the request experience
- Keys need to match OrganizationChoicesElectionOffice and OrganizationChoicesVerbose
+ Keys need to match OrgChoicesElectionOffice and OrganizationChoicesVerbose
"""
FEDERAL = "federal", "Federal"
@@ -113,7 +113,7 @@ class DomainRequest(TimeStampedModel):
SPECIAL_DISTRICT = "special_district", "Special district"
SCHOOL_DISTRICT = "school_district", "School district"
- class OrganizationChoicesElectionOffice(models.TextChoices):
+ class OrgChoicesElectionOffice(models.TextChoices):
"""
Primary organization choices for Django admin:
Keys need to match OrganizationChoices and OrganizationChoicesVerbose.
@@ -142,6 +142,44 @@ class DomainRequest(TimeStampedModel):
CITY_ELECTION = "city_election", "City - Election"
SPECIAL_DISTRICT_ELECTION = "special_district_election", "Special district - Election"
+ @classmethod
+ def get_org_election_to_org_generic(cls):
+ """
+ Creates and returns a dictionary mapping from election-specific organization
+ choice enums to their corresponding general organization choice enums.
+
+ If no such mapping exists, it is simple excluded from the map.
+ """
+ # This can be mapped automatically but its harder to read.
+ # For clarity reasons, we manually define this.
+ org_election_map = {
+ cls.STATE_OR_TERRITORY_ELECTION: cls.STATE_OR_TERRITORY,
+ cls.TRIBAL_ELECTION: cls.TRIBAL,
+ cls.COUNTY_ELECTION: cls.COUNTY,
+ cls.CITY_ELECTION: cls.CITY,
+ cls.SPECIAL_DISTRICT_ELECTION: cls.SPECIAL_DISTRICT,
+ }
+ return org_election_map
+
+ @classmethod
+ def get_org_generic_to_org_election(cls):
+ """
+ Creates and returns a dictionary mapping from general organization
+ choice enums to their corresponding election-specific organization enums.
+
+ If no such mapping exists, it is simple excluded from the map.
+ """
+ # This can be mapped automatically but its harder to read.
+ # For clarity reasons, we manually define this.
+ org_election_map = {
+ cls.STATE_OR_TERRITORY: cls.STATE_OR_TERRITORY_ELECTION,
+ cls.TRIBAL: cls.TRIBAL_ELECTION,
+ cls.COUNTY: cls.COUNTY_ELECTION,
+ cls.CITY: cls.CITY_ELECTION,
+ cls.SPECIAL_DISTRICT: cls.SPECIAL_DISTRICT_ELECTION,
+ }
+ return org_election_map
+
class OrganizationChoicesVerbose(models.TextChoices):
"""
Tertiary organization choices
@@ -435,9 +473,16 @@ class DomainRequest(TimeStampedModel):
help_text="Type of organization",
)
+ is_election_board = models.BooleanField(
+ null=True,
+ blank=True,
+ help_text="Is your organization an election office?",
+ )
+
+ # TODO - Ticket #1911: stub this data from DomainRequest
organization_type = models.CharField(
max_length=255,
- choices=OrganizationChoicesElectionOffice.choices,
+ choices=OrgChoicesElectionOffice.choices,
null=True,
blank=True,
help_text="Type of organization - Election office",
@@ -474,12 +519,6 @@ class DomainRequest(TimeStampedModel):
help_text="Federal government branch",
)
- is_election_board = models.BooleanField(
- null=True,
- blank=True,
- help_text="Is your organization an election office?",
- )
-
organization_name = models.CharField(
null=True,
blank=True,
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index c7dc8821d..02f13a57b 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -3,15 +3,16 @@ import logging
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
-from .models import User, Contact, DomainRequest
+from .models import User, Contact, DomainRequest, DomainInformation
logger = logging.getLogger(__name__)
@receiver(pre_save, sender=DomainRequest)
+@receiver(pre_save, sender=DomainInformation)
def create_or_update_organization_type(sender, instance, **kwargs):
- """The organization_type field on DomainRequest is consituted from the
+ """The organization_type field on DomainRequest and DomainInformation is consituted from the
generic_org_type and is_election_board fields. To keep the organization_type
field up to date, we need to update it before save based off of those field
values.
@@ -21,50 +22,67 @@ def create_or_update_organization_type(sender, instance, **kwargs):
organization_type is set to a corresponding election variant. Otherwise, it directly
mirrors the generic_org_type value.
"""
- if not isinstance(instance, DomainRequest):
+ if not isinstance(instance, DomainRequest) and not isinstance(instance, DomainInformation):
# I don't see how this could possibly happen - but its still a good check to have.
# Lets force a fail condition rather than wait for one to happen, if this occurs.
- raise ValueError("Type mismatch. The instance was not DomainRequest.")
+ raise ValueError("Type mismatch. The instance was not DomainRequest or DomainInformation.")
# == Init variables == #
- # We can't grab the election variant if it is in federal, interstate, or school_district.
- # The "election variant" is just the org name, with " - Election" appended to the end.
- # For example, "School district - Election".
- invalid_types = [
- DomainRequest.OrganizationChoices.FEDERAL,
- DomainRequest.OrganizationChoices.INTERSTATE,
- DomainRequest.OrganizationChoices.SCHOOL_DISTRICT,
- ]
-
- # TODO - maybe we need a check here for .filter then .get
is_new_instance = instance.id is None
+ election_org_choices = DomainRequest.OrgChoicesElectionOffice
+
+ # For any given organization type, return the "_election" variant.
+ # For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
+ generic_org_to_org_map = election_org_choices.get_org_generic_to_org_election()
+
+ # For any given "_election" variant, return the base org type.
+ # For example: STATE_OR_TERRITORY_ELECTION => STATE_OR_TERRITORY
+ election_org_to_generic_org_map = election_org_choices.get_org_election_to_org_generic()
# A new record is added with organization_type not defined.
# This happens from the regular domain request flow.
if is_new_instance:
# == Check for invalid conditions before proceeding == #
+ # Since organization type is linked with generic_org_type and election board,
+ # we have to update one or the other, not both.
if instance.organization_type and instance.generic_org_type:
- # Since organization type is linked with generic_org_type and election board,
- # we have to update one or the other, not both.
- raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
+ organization_type = str(instance.organization_type)
+ generic_org_type = str(instance.generic_org_type)
+
+ # We can only proceed if all values match (fixtures, load_from_da).
+ # Otherwise, we're overwriting data so lets forbid this.
+ if (
+ "_election" in organization_type != instance.is_election_board or
+ organization_type != generic_org_type
+ ):
+ message = (
+ "Cannot add organization_type and generic_org_type simultaneously "
+ "when generic_org_type, is_election_board, and organization_type values do not match."
+ )
+ raise ValueError(message)
elif not instance.organization_type and not instance.generic_org_type:
- # Do values to update - do nothing
+ # No values to update - do nothing
return None
# == Program flow will halt here if there is no reason to update == #
# == Update the linked values == #
- # Find out which field needs updating
organization_type_needs_update = instance.organization_type is None
generic_org_type_needs_update = instance.generic_org_type is None
- # Update that field
+ # If a field is none, it indicates (per prior checks) that the
+ # related field (generic org type <-> org type) has data and we should update according to that.
if organization_type_needs_update:
- _update_org_type_from_generic_org_and_election(instance, invalid_types)
+ _update_org_type_from_generic_org_and_election(instance)
elif generic_org_type_needs_update:
_update_generic_org_and_election_from_org_type(instance)
+ else:
+ # This indicates that all data already matches,
+ # so we should just do nothing because there is nothing to update.
+ pass
else:
-
+
+ # == Init variables == #
# Instance is already in the database, fetch its current state
current_instance = DomainRequest.objects.get(id=instance.id)
@@ -77,6 +95,7 @@ def create_or_update_organization_type(sender, instance, **kwargs):
if organization_type_changed and (generic_org_type_changed or is_election_board_changed):
# Since organization type is linked with generic_org_type and election board,
# we have to update one or the other, not both.
+ # This will not happen in normal flow as it is not possible otherwise.
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
# Do values to update - do nothing
@@ -90,28 +109,50 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# Update that field
if organization_type_needs_update:
- _update_org_type_from_generic_org_and_election(instance, invalid_types)
+ _update_org_type_from_generic_org_and_election(instance)
elif generic_org_type_needs_update:
_update_generic_org_and_election_from_org_type(instance)
-def _update_org_type_from_generic_org_and_election(instance, invalid_types):
- # TODO handle if generic_org_type is None
- if instance.generic_org_type not in invalid_types and instance.is_election_board:
- instance.organization_type = f"{instance.generic_org_type}_election"
+def _update_org_type_from_generic_org_and_election(instance):
+ """Given a field values for generic_org_type and is_election_board, update the
+ organization_type field."""
+
+ # We convert to a string because the enum types are different.
+ generic_org_type = str(instance.generic_org_type)
+
+ # For any given organization type, return the "_election" variant.
+ # For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
+ election_org_choices = DomainRequest.OrgChoicesElectionOffice
+ org_map = election_org_choices.get_org_generic_to_org_election()
+
+ # This essentially means: instance.generic_org_type not in invalid_types
+ if generic_org_type in org_map and instance.is_election_board:
+ instance.organization_type = org_map[generic_org_type]
else:
- instance.organization_type = str(instance.generic_org_type)
+ instance.organization_type = generic_org_type
def _update_generic_org_and_election_from_org_type(instance):
- """Given a value for organization_type, update the
- generic_org_type and is_election_board values."""
- # TODO find a better solution than this
+ """Given the field value for organization_type, update the
+ generic_org_type and is_election_board field."""
+
+ # We convert to a string because the enum types are different
+ # between OrgChoicesElectionOffice and OrganizationChoices.
+ # But their names are the same (for the most part).
current_org_type = str(instance.organization_type)
- if "_election" in current_org_type:
- instance.generic_org_type = current_org_type.split("_election")[0]
+
+ # For any given organization type, return the generic variant.
+ # For example: STATE_OR_TERRITORY_ELECTION => STATE_OR_TERRITORY
+ election_org_choices = DomainRequest.OrgChoicesElectionOffice
+ org_map = election_org_choices.get_org_election_to_org_generic()
+
+ # This essentially means: "_election" in current_org_type
+ if current_org_type in org_map:
+ new_org = org_map[current_org_type]
+ instance.generic_org_type = new_org
instance.is_election_board = True
else:
- instance.organization_type = str(instance.generic_org_type)
+ instance.generic_org_type = current_org_type
instance.is_election_board = False
@receiver(post_save, sender=User)
From 198977bb6ea92846107583cd496e6cc49f30009e Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 12:41:59 -0600
Subject: [PATCH 22/80] Test cases
---
src/registrar/signals.py | 88 +++++++++++------
src/registrar/tests/common.py | 9 +-
src/registrar/tests/test_signals.py | 148 ++++++++++++++++++++++++++++
3 files changed, 213 insertions(+), 32 deletions(-)
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index 02f13a57b..3a90c6a5c 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -30,7 +30,7 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# == Init variables == #
is_new_instance = instance.id is None
election_org_choices = DomainRequest.OrgChoicesElectionOffice
-
+
# For any given organization type, return the "_election" variant.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_to_org_map = election_org_choices.get_org_generic_to_org_election()
@@ -48,19 +48,29 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# we have to update one or the other, not both.
if instance.organization_type and instance.generic_org_type:
organization_type = str(instance.organization_type)
+ # Strip "_election" if it exists
+ mapped_org_type = election_org_to_generic_org_map.get(organization_type)
generic_org_type = str(instance.generic_org_type)
+ should_proceed = True
# We can only proceed if all values match (fixtures, load_from_da).
# Otherwise, we're overwriting data so lets forbid this.
- if (
- "_election" in organization_type != instance.is_election_board or
- organization_type != generic_org_type
- ):
+ is_election_type = "_election" in organization_type
+ can_have_election_board = organization_type in generic_org_to_org_map
+ if is_election_type != instance.is_election_board and can_have_election_board:
+ # This means that there is a mismatch between the booleans
+ # (i.e. FEDERAL is not equal to is_election_board = True)
+ should_proceed = False
+ elif mapped_org_type is not None and generic_org_type != mapped_org_type:
+ # This means that there is as mismatch between the org types
+ should_proceed = False
+
+ if not should_proceed:
message = (
"Cannot add organization_type and generic_org_type simultaneously "
"when generic_org_type, is_election_board, and organization_type values do not match."
)
- raise ValueError(message)
+ raise ValueError(message)
elif not instance.organization_type and not instance.generic_org_type:
# No values to update - do nothing
return None
@@ -73,15 +83,17 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# If a field is none, it indicates (per prior checks) that the
# related field (generic org type <-> org type) has data and we should update according to that.
if organization_type_needs_update:
- _update_org_type_from_generic_org_and_election(instance)
+ _update_org_type_from_generic_org_and_election(instance, generic_org_to_org_map)
elif generic_org_type_needs_update:
- _update_generic_org_and_election_from_org_type(instance)
+ _update_generic_org_and_election_from_org_type(
+ instance, election_org_to_generic_org_map, generic_org_to_org_map
+ )
else:
# This indicates that all data already matches,
# so we should just do nothing because there is nothing to update.
pass
else:
-
+
# == Init variables == #
# Instance is already in the database, fetch its current state
current_instance = DomainRequest.objects.get(id=instance.id)
@@ -109,30 +121,41 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# Update that field
if organization_type_needs_update:
- _update_org_type_from_generic_org_and_election(instance)
+ _update_org_type_from_generic_org_and_election(instance, generic_org_to_org_map)
elif generic_org_type_needs_update:
- _update_generic_org_and_election_from_org_type(instance)
+ _update_generic_org_and_election_from_org_type(
+ instance, election_org_to_generic_org_map, generic_org_to_org_map
+ )
-def _update_org_type_from_generic_org_and_election(instance):
+
+def _update_org_type_from_generic_org_and_election(instance, org_map):
"""Given a field values for generic_org_type and is_election_board, update the
organization_type field."""
# We convert to a string because the enum types are different.
generic_org_type = str(instance.generic_org_type)
- # For any given organization type, return the "_election" variant.
- # For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
- election_org_choices = DomainRequest.OrgChoicesElectionOffice
- org_map = election_org_choices.get_org_generic_to_org_election()
-
- # This essentially means: instance.generic_org_type not in invalid_types
- if generic_org_type in org_map and instance.is_election_board:
- instance.organization_type = org_map[generic_org_type]
- else:
+ # If the election board is none, then it tells us that it is an invalid field.
+ # Such as federal, interstate, or school_district.
+ if instance.is_election_board is None and generic_org_type not in org_map:
instance.organization_type = generic_org_type
+ return instance
+ elif instance.is_election_board is None and generic_org_type in org_map:
+ # This can only happen with manual data tinkering, which causes these to be out of sync.
+ instance.is_election_board = False
+ logger.warning("create_or_update_organization_type() -> is_election_board is out of sync. Updating value.")
+
+ if generic_org_type in org_map:
+ # Swap to the election type if it is an election board. Otherwise, stick to the normal one.
+ instance.organization_type = org_map[generic_org_type] if instance.is_election_board else generic_org_type
+ elif generic_org_type not in org_map:
+ # Election board should be reset to None if the record
+ # can't have one. For example, federal.
+ instance.organization_type = generic_org_type
+ instance.is_election_board = None
-def _update_generic_org_and_election_from_org_type(instance):
+def _update_generic_org_and_election_from_org_type(instance, election_org_map, generic_org_map):
"""Given the field value for organization_type, update the
generic_org_type and is_election_board field."""
@@ -141,19 +164,22 @@ def _update_generic_org_and_election_from_org_type(instance):
# But their names are the same (for the most part).
current_org_type = str(instance.organization_type)
- # For any given organization type, return the generic variant.
- # For example: STATE_OR_TERRITORY_ELECTION => STATE_OR_TERRITORY
- election_org_choices = DomainRequest.OrgChoicesElectionOffice
- org_map = election_org_choices.get_org_election_to_org_generic()
-
- # This essentially means: "_election" in current_org_type
- if current_org_type in org_map:
- new_org = org_map[current_org_type]
+ # This essentially means: "_election" in current_org_type.
+ if current_org_type in election_org_map:
+ new_org = election_org_map[current_org_type]
instance.generic_org_type = new_org
instance.is_election_board = True
else:
instance.generic_org_type = current_org_type
- instance.is_election_board = False
+
+ # This basically checks if the given org type
+ # can even have an election board in the first place.
+ # For instance, federal cannot so is_election_board = None
+ if current_org_type in generic_org_map:
+ instance.is_election_board = False
+ else:
+ instance.is_election_board = None
+
@receiver(post_save, sender=User)
def handle_profile(sender, instance, **kwargs):
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 9ecc6af67..1a4120106 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -782,6 +782,9 @@ def completed_domain_request(
submitter=False,
name="city.gov",
investigator=None,
+ generic_org_type="federal",
+ is_election_board=False,
+ organization_type=None,
):
"""A completed domain request."""
if not user:
@@ -819,7 +822,8 @@ def completed_domain_request(
is_staff=True,
)
domain_request_kwargs = dict(
- generic_org_type="federal",
+ generic_org_type=generic_org_type,
+ is_election_board=is_election_board,
federal_type="executive",
purpose="Purpose of the site",
is_policy_acknowledged=True,
@@ -840,6 +844,9 @@ def completed_domain_request(
if has_anything_else:
domain_request_kwargs["anything_else"] = "There is more"
+ if organization_type:
+ domain_request_kwargs["organization_type"] = organization_type
+
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
if has_other_contacts:
diff --git a/src/registrar/tests/test_signals.py b/src/registrar/tests/test_signals.py
index 4e2cbc83b..a6f8adb44 100644
--- a/src/registrar/tests/test_signals.py
+++ b/src/registrar/tests/test_signals.py
@@ -2,6 +2,8 @@ from django.test import TestCase
from django.contrib.auth import get_user_model
from registrar.models import Contact
+from registrar.models.domain_request import DomainRequest
+from registrar.tests.common import completed_domain_request
class TestUserPostSave(TestCase):
@@ -99,3 +101,149 @@ class TestUserPostSave(TestCase):
self.assertEqual(actual.last_name, self.last_name)
self.assertEqual(actual.email, self.email)
self.assertEqual(actual.phone, self.phone)
+
+
+class TestDomainRequestSignals(TestCase):
+ """Tests hooked signals on the DomainRequest object"""
+
+ def tearDown(self):
+ DomainRequest.objects.all().delete()
+ super().tearDown()
+
+ def test_create_or_update_organization_type_new_instance(self):
+ """Test create_or_update_organization_type when creating a new instance"""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=True,
+ )
+
+ self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
+
+ def test_create_or_update_organization_type_new_instance_federal_does_nothing(self):
+ """Test if create_or_update_organization_type does nothing when creating a new instance for federal"""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
+ is_election_board=True,
+ )
+ self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL)
+
+ def test_create_or_update_organization_type_existing_instance_updates_election_board(self):
+ """Test create_or_update_organization_type for an existing instance."""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=False,
+ )
+ domain_request.is_election_board = True
+ domain_request.save()
+
+ self.assertEqual(domain_request.is_election_board, True)
+ self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
+
+ # Try reverting the election board value
+ domain_request.is_election_board = False
+ domain_request.save()
+
+ self.assertEqual(domain_request.is_election_board, False)
+ self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
+
+ # Try reverting setting an invalid value for election board (should revert to False)
+ domain_request.is_election_board = None
+ domain_request.save()
+
+ self.assertEqual(domain_request.is_election_board, False)
+ self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
+
+ def test_create_or_update_organization_type_existing_instance_updates_generic_org_type(self):
+ """Test create_or_update_organization_type when modifying generic_org_type on an existing instance."""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=True,
+ )
+
+ domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
+ domain_request.save()
+
+ # Election board should be None because interstate cannot have an election board.
+ self.assertEqual(domain_request.is_election_board, None)
+ self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.INTERSTATE)
+
+ # Try changing the org Type to something that CAN have an election board.
+ domain_request_tribal = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="startedTribal.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
+ is_election_board=True,
+ )
+ self.assertEqual(
+ domain_request_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
+ )
+
+ # Change the org type
+ domain_request_tribal.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
+ domain_request_tribal.save()
+
+ self.assertEqual(domain_request_tribal.is_election_board, True)
+ self.assertEqual(
+ domain_request_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.STATE_OR_TERRITORY_ELECTION
+ )
+
+ def test_create_or_update_organization_type_no_update(self):
+ """Test create_or_update_organization_type when there are no values to update."""
+
+ # Test for when both generic_org_type and organization_type is declared,
+ # and are both non-election board
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=False,
+ )
+ domain_request.save()
+ self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
+ self.assertEqual(domain_request.is_election_board, False)
+ self.assertEqual(domain_request.generic_org_type, DomainRequest.OrganizationChoices.CITY)
+
+ # Test for when both generic_org_type and organization_type is declared,
+ # and are both election board
+ domain_request_election = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="startedElection.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=True,
+ organization_type=DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION,
+ )
+
+ self.assertEqual(
+ domain_request_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
+ )
+ self.assertEqual(domain_request_election.is_election_board, True)
+ self.assertEqual(domain_request_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
+
+ # Modify an unrelated existing value for both, and ensure that everything is still consistent
+ domain_request.city = "Fudge"
+ domain_request_election.city = "Caramel"
+ domain_request.save()
+ domain_request_election.save()
+
+ self.assertEqual(domain_request.city, "Fudge")
+ self.assertEqual(domain_request_election.city, "Caramel")
+
+ # Test for non-election
+ self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
+ self.assertEqual(domain_request.is_election_board, False)
+ self.assertEqual(domain_request.generic_org_type, DomainRequest.OrganizationChoices.CITY)
+
+ # Test for election
+ self.assertEqual(
+ domain_request_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
+ )
+ self.assertEqual(domain_request_election.is_election_board, True)
+ self.assertEqual(domain_request_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
From 941512c70412004ed2f95e8d5640a1469cae2b15 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 13:47:27 -0600
Subject: [PATCH 23/80] Linting
---
src/registrar/models/domain.py | 1 -
src/registrar/models/domain_request.py | 3 +-
src/registrar/signals.py | 81 +++++++++++++-------------
3 files changed, 44 insertions(+), 41 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index b3d5b19ce..079fce3bc 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -198,7 +198,6 @@ class Domain(TimeStampedModel, DomainHelper):
is called in the validate function on the request/domain page
throws- RegistryError or InvalidDomainError"""
- return True
if not cls.string_could_be_domain(domain):
logger.warning("Not a valid domain: %s" % str(domain))
# throw invalid domain error so that it can be caught in
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 0293fd124..fc2864fe4 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -124,6 +124,7 @@ class DomainRequest(TimeStampedModel):
When adding the election variant, you must append "_election" to the end of the string.
"""
+
# We can't inherit OrganizationChoices due to models.TextChoices being an enum.
# We can redefine these values instead.
FEDERAL = "federal", "Federal"
@@ -160,7 +161,7 @@ class DomainRequest(TimeStampedModel):
cls.SPECIAL_DISTRICT_ELECTION: cls.SPECIAL_DISTRICT,
}
return org_election_map
-
+
@classmethod
def get_org_generic_to_org_election(cls):
"""
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index 3a90c6a5c..22a04b39c 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -22,13 +22,8 @@ def create_or_update_organization_type(sender, instance, **kwargs):
organization_type is set to a corresponding election variant. Otherwise, it directly
mirrors the generic_org_type value.
"""
- if not isinstance(instance, DomainRequest) and not isinstance(instance, DomainInformation):
- # I don't see how this could possibly happen - but its still a good check to have.
- # Lets force a fail condition rather than wait for one to happen, if this occurs.
- raise ValueError("Type mismatch. The instance was not DomainRequest or DomainInformation.")
# == Init variables == #
- is_new_instance = instance.id is None
election_org_choices = DomainRequest.OrgChoicesElectionOffice
# For any given organization type, return the "_election" variant.
@@ -41,38 +36,13 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# A new record is added with organization_type not defined.
# This happens from the regular domain request flow.
+ is_new_instance = instance.id is None
+
if is_new_instance:
# == Check for invalid conditions before proceeding == #
- # Since organization type is linked with generic_org_type and election board,
- # we have to update one or the other, not both.
- if instance.organization_type and instance.generic_org_type:
- organization_type = str(instance.organization_type)
- # Strip "_election" if it exists
- mapped_org_type = election_org_to_generic_org_map.get(organization_type)
- generic_org_type = str(instance.generic_org_type)
- should_proceed = True
-
- # We can only proceed if all values match (fixtures, load_from_da).
- # Otherwise, we're overwriting data so lets forbid this.
- is_election_type = "_election" in organization_type
- can_have_election_board = organization_type in generic_org_to_org_map
- if is_election_type != instance.is_election_board and can_have_election_board:
- # This means that there is a mismatch between the booleans
- # (i.e. FEDERAL is not equal to is_election_board = True)
- should_proceed = False
- elif mapped_org_type is not None and generic_org_type != mapped_org_type:
- # This means that there is as mismatch between the org types
- should_proceed = False
-
- if not should_proceed:
- message = (
- "Cannot add organization_type and generic_org_type simultaneously "
- "when generic_org_type, is_election_board, and organization_type values do not match."
- )
- raise ValueError(message)
- elif not instance.organization_type and not instance.generic_org_type:
- # No values to update - do nothing
+ should_proceed = _validate_new_instance(instance, election_org_to_generic_org_map, generic_org_to_org_map)
+ if not should_proceed:
return None
# == Program flow will halt here if there is no reason to update == #
@@ -88,10 +58,6 @@ def create_or_update_organization_type(sender, instance, **kwargs):
_update_generic_org_and_election_from_org_type(
instance, election_org_to_generic_org_map, generic_org_to_org_map
)
- else:
- # This indicates that all data already matches,
- # so we should just do nothing because there is nothing to update.
- pass
else:
# == Init variables == #
@@ -148,7 +114,7 @@ def _update_org_type_from_generic_org_and_election(instance, org_map):
if generic_org_type in org_map:
# Swap to the election type if it is an election board. Otherwise, stick to the normal one.
instance.organization_type = org_map[generic_org_type] if instance.is_election_board else generic_org_type
- elif generic_org_type not in org_map:
+ else:
# Election board should be reset to None if the record
# can't have one. For example, federal.
instance.organization_type = generic_org_type
@@ -181,6 +147,43 @@ def _update_generic_org_and_election_from_org_type(instance, election_org_map, g
instance.is_election_board = None
+def _validate_new_instance(instance, election_org_to_generic_org_map, generic_org_to_org_map):
+ """
+ Validates whether a new instance of DomainRequest or DomainInformation can proceed with the update
+ based on the consistency between organization_type, generic_org_type, and is_election_board.
+ """
+
+ # We conditionally accept both of these values to exist simultaneously, as long as
+ # those values do not intefere with eachother.
+ # Because this condition can only be triggered through a dev (no user flow),
+ # we throw an error if an invalid state is found here.
+ if instance.organization_type and instance.generic_org_type:
+ generic_org_type = str(instance.generic_org_type)
+ organization_type = str(instance.organization_type)
+
+ # Strip "_election" if it exists
+ mapped_org_type = election_org_to_generic_org_map.get(organization_type)
+
+ # Do tests on the org update for election board changes.
+ is_election_type = "_election" in organization_type
+ can_have_election_board = organization_type in generic_org_to_org_map
+
+ election_board_mismatch = is_election_type != instance.is_election_board and can_have_election_board
+ org_type_mismatch = mapped_org_type is not None and generic_org_type != mapped_org_type
+ if election_board_mismatch or org_type_mismatch:
+ message = (
+ "Cannot add organization_type and generic_org_type simultaneously "
+ "when generic_org_type, is_election_board, and organization_type values do not match."
+ )
+ raise ValueError(message)
+
+ should_proceed = True
+ return should_proceed
+ else:
+ should_proceed = not instance.organization_type and not instance.generic_org_type
+ return should_proceed
+
+
@receiver(post_save, sender=User)
def handle_profile(sender, instance, **kwargs):
"""Method for when a User is saved.
From 6c22cf7169f098af8bcfbaac7d240a6567c9b522 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 13:56:27 -0600
Subject: [PATCH 24/80] Fix unit tests
---
src/registrar/assets/sass/_theme/_admin.scss | 28 ++++++--------------
src/registrar/tests/test_admin.py | 4 +--
2 files changed, 10 insertions(+), 22 deletions(-)
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index d1e000033..ebd0d1b5b 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -390,53 +390,41 @@ address.margin-top-neg-1__detail-list {
margin-top: 5px !important;
}
// Mimic the normal label size
- dt {
- font-size: 0.8125rem;
- color: var(--body-quiet-color);
- }
-
- address {
+ address, dt {
font-size: 0.8125rem;
color: var(--body-quiet-color);
}
}
+td button.usa-button__clipboard-link, address.dja-address-contact-list {
+ font-size: 0.8125rem !important;
+}
+
address.dja-address-contact-list {
- font-size: 0.8125rem;
color: var(--body-quiet-color);
button.usa-button__clipboard-link {
font-size: 0.8125rem !important;
}
}
-td button.usa-button__clipboard-link {
- font-size: 0.8125rem !important;
-}
-
// Mimic the normal label size
@media (max-width: 1024px){
- .dja-detail-list dt {
- font-size: 0.875rem;
- color: var(--body-quiet-color);
- }
- .dja-detail-list address {
+ .dja-detail-list dt, .dja-detail-list address {
font-size: 0.875rem;
color: var(--body-quiet-color);
}
- address button.usa-button__clipboard-link {
+ address button.usa-button__clipboard-link, td button.usa-button__clipboard-link {
font-size: 0.875rem !important;
}
- td button.usa-button__clipboard-link {
- font-size: 0.875rem !important;
- }
}
.errors span.select2-selection {
border: 1px solid var(--error-fg) !important;
}
+// Make the clipboard button "float" inside of the input box
.admin-icon-group {
position: relative;
display: flex;
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index bbbf385d9..1f7ba7c71 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1306,7 +1306,7 @@ class TestDomainRequestAdmin(MockEppLib):
("title", "Treat inspector"),
("email", "meoward.jones@igorville.gov"),
("phone", "(555) 123 12345"),
- ("email_copy_button_input", f'
"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
@@ -2035,7 +2035,7 @@ class TestDomainInformationAdmin(TestCase):
("title", "Treat inspector"),
("email", "meoward.jones@igorville.gov"),
("phone", "(555) 123 12345"),
- ("email_copy_button_input", f'
"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
From 022eb9bbaf398ad46e5d45bff9beec49b416b652 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 14:05:09 -0600
Subject: [PATCH 25/80] Fix bug
---
src/registrar/signals.py | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index 22a04b39c..a6fabe873 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -151,6 +151,8 @@ def _validate_new_instance(instance, election_org_to_generic_org_map, generic_or
"""
Validates whether a new instance of DomainRequest or DomainInformation can proceed with the update
based on the consistency between organization_type, generic_org_type, and is_election_board.
+
+ Returns a boolean determining if execution should proceed or not.
"""
# We conditionally accept both of these values to exist simultaneously, as long as
@@ -168,8 +170,8 @@ def _validate_new_instance(instance, election_org_to_generic_org_map, generic_or
is_election_type = "_election" in organization_type
can_have_election_board = organization_type in generic_org_to_org_map
- election_board_mismatch = is_election_type != instance.is_election_board and can_have_election_board
- org_type_mismatch = mapped_org_type is not None and generic_org_type != mapped_org_type
+ election_board_mismatch = (is_election_type != instance.is_election_board) and can_have_election_board
+ org_type_mismatch = mapped_org_type is not None and (generic_org_type != mapped_org_type)
if election_board_mismatch or org_type_mismatch:
message = (
"Cannot add organization_type and generic_org_type simultaneously "
@@ -177,11 +179,11 @@ def _validate_new_instance(instance, election_org_to_generic_org_map, generic_or
)
raise ValueError(message)
- should_proceed = True
- return should_proceed
+ return True
+ elif not instance.organization_type and not instance.generic_org_type:
+ return False
else:
- should_proceed = not instance.organization_type and not instance.generic_org_type
- return should_proceed
+ return True
@receiver(post_save, sender=User)
From 892607bd658b780a29d08d015acfd90738173e07 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 14:06:36 -0600
Subject: [PATCH 26/80] Update test_admin.py
---
src/registrar/tests/test_admin.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 1f7ba7c71..c285c8f37 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1306,7 +1306,6 @@ class TestDomainRequestAdmin(MockEppLib):
("title", "Treat inspector"),
("email", "meoward.jones@igorville.gov"),
("phone", "(555) 123 12345"),
- ("email_copy_button_input", f"{expected_email}"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
@@ -2035,7 +2034,6 @@ class TestDomainInformationAdmin(TestCase):
("title", "Treat inspector"),
("email", "meoward.jones@igorville.gov"),
("phone", "(555) 123 12345"),
- ("email_copy_button_input", f"{expected_email}"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
From 843158b3ef2de255017618fda8930c60004b92a1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 14:08:59 -0600
Subject: [PATCH 27/80] Update src/registrar/signals.py
---
src/registrar/signals.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index a6fabe873..4e2020731 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -76,7 +76,7 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# This will not happen in normal flow as it is not possible otherwise.
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
- # Do values to update - do nothing
+ # No values to update - do nothing
return None
# == Program flow will halt here if there is no reason to update == #
From 47b7fb8f492976d6c1337f41396bf6eba72348d1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 14:11:28 -0600
Subject: [PATCH 28/80] Remove bad tests
No longer applicable here
---
src/registrar/tests/test_admin.py | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index c285c8f37..759fd8b9e 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1319,7 +1319,6 @@ class TestDomainRequestAdmin(MockEppLib):
("title", "Admin Tester"),
("email", "mayor@igorville.gov"),
("phone", "(555) 555 5556"),
- ("email_copy_button_input", f'
"),
("email", "testy@town.com"),
("phone", "(555) 555 5555"),
- ("email_copy_button_input", f'
Date: Mon, 1 Apr 2024 14:13:48 -0600
Subject: [PATCH 29/80] Revert "Remove bad tests"
This reverts commit 47b7fb8f492976d6c1337f41396bf6eba72348d1.
---
src/registrar/tests/test_admin.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 759fd8b9e..c285c8f37 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1319,6 +1319,7 @@ class TestDomainRequestAdmin(MockEppLib):
("title", "Admin Tester"),
("email", "mayor@igorville.gov"),
("phone", "(555) 555 5556"),
+ ("email_copy_button_input", f'
"),
("email", "testy@town.com"),
("phone", "(555) 555 5555"),
+ ("email_copy_button_input", f'
Date: Mon, 1 Apr 2024 14:21:35 -0600
Subject: [PATCH 30/80] New unit tests
---
src/registrar/tests/test_admin.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index c285c8f37..3999acc4e 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1319,7 +1319,6 @@ class TestDomainRequestAdmin(MockEppLib):
("title", "Admin Tester"),
("email", "mayor@igorville.gov"),
("phone", "(555) 555 5556"),
- ("email_copy_button_input", f'
"),
- ("email", "testy@town.com"),
- ("phone", "(555) 555 5555"),
- ("email_copy_button_input", f'
Date: Mon, 1 Apr 2024 15:38:53 -0600
Subject: [PATCH 31/80] Add DomainInformatoin tests
---
src/registrar/admin.py | 1 +
src/registrar/signals.py | 10 +-
src/registrar/tests/common.py | 8 +-
src/registrar/tests/test_admin.py | 3 +-
src/registrar/tests/test_reports.py | 4 +-
src/registrar/tests/test_signals.py | 164 +++++++++++++++++++++++++++-
6 files changed, 179 insertions(+), 11 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index f2204e543..7c88c34a0 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -883,6 +883,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
"Type of organization",
{
"fields": [
+ "is_election_board",
"organization_type",
"federal_type",
"federal_agency",
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index 4e2020731..5cf035eb9 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -62,7 +62,15 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# == Init variables == #
# Instance is already in the database, fetch its current state
- current_instance = DomainRequest.objects.get(id=instance.id)
+ if isinstance(instance, DomainRequest):
+ current_instance = DomainRequest.objects.get(id=instance.id)
+ elif isinstance(instance, DomainInformation):
+ current_instance = DomainInformation.objects.get(id=instance.id)
+ else:
+ # This should never occur. But it never hurts to have this check anyway.
+ raise ValueError(
+ "create_or_update_organization_type() -> instance was not DomainRequest or DomainInformation"
+ )
# Check the new and old values
generic_org_type_changed = instance.generic_org_type != current_instance.generic_org_type
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 1a4120106..9681b8cb7 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -585,7 +585,7 @@ class MockDb(TestCase):
generic_org_type="federal",
federal_agency="World War I Centennial Commission",
federal_type="executive",
- is_election_board=True,
+ is_election_board=False,
)
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain_2, generic_org_type="interstate", is_election_board=True
@@ -595,14 +595,14 @@ class MockDb(TestCase):
domain=self.domain_3,
generic_org_type="federal",
federal_agency="Armed Forces Retirement Home",
- is_election_board=True,
+ is_election_board=False,
)
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_4,
generic_org_type="federal",
federal_agency="Armed Forces Retirement Home",
- is_election_board=True,
+ is_election_board=False,
)
self.domain_information_5, _ = DomainInformation.objects.get_or_create(
creator=self.user,
@@ -652,7 +652,7 @@ class MockDb(TestCase):
generic_org_type="federal",
federal_agency="World War I Centennial Commission",
federal_type="executive",
- is_election_board=True,
+ is_election_board=False,
)
self.domain_information_12, _ = DomainInformation.objects.get_or_create(
creator=self.user,
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 7c0c81db4..46b5e104a 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1453,12 +1453,13 @@ class TestDomainRequestAdmin(MockEppLib):
"creator",
"investigator",
"generic_org_type",
+ "is_election_board",
+ "organization_type",
"federally_recognized_tribe",
"state_recognized_tribe",
"tribe_name",
"federal_agency",
"federal_type",
- "is_election_board",
"organization_name",
"address_line1",
"address_line2",
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 5bd594a15..d3eec946d 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -687,12 +687,12 @@ class HelperFunctions(MockDb):
}
# Test with distinct
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition, True)
- expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 2]
+ expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
# Test without distinct
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
- expected_content = [3, 4, 1, 0, 0, 0, 0, 0, 0, 2]
+ expected_content = [3, 4, 1, 0, 0, 0, 0, 0, 0, 0]
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
def test_get_sliced_requests(self):
diff --git a/src/registrar/tests/test_signals.py b/src/registrar/tests/test_signals.py
index a6f8adb44..e950f39fb 100644
--- a/src/registrar/tests/test_signals.py
+++ b/src/registrar/tests/test_signals.py
@@ -1,8 +1,6 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
-
-from registrar.models import Contact
-from registrar.models.domain_request import DomainRequest
+from registrar.models import Contact, DomainRequest, Domain, DomainInformation
from registrar.tests.common import completed_domain_request
@@ -130,6 +128,7 @@ class TestDomainRequestSignals(TestCase):
is_election_board=True,
)
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL)
+ self.assertEqual(domain_request.is_election_board, None)
def test_create_or_update_organization_type_existing_instance_updates_election_board(self):
"""Test create_or_update_organization_type for an existing instance."""
@@ -247,3 +246,162 @@ class TestDomainRequestSignals(TestCase):
)
self.assertEqual(domain_request_election.is_election_board, True)
self.assertEqual(domain_request_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
+
+
+class TestDomainInformationSignals(TestCase):
+ """Tests hooked signals on the DomainRequest object"""
+
+ def tearDown(self):
+ DomainInformation.objects.all().delete()
+ DomainRequest.objects.all().delete()
+ Domain.objects.all().delete()
+ super().tearDown()
+
+ def test_create_or_update_organization_type_new_instance(self):
+ """Test create_or_update_organization_type when creating a new instance"""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=True,
+ )
+
+ domain_information = DomainInformation.create_from_da(domain_request)
+ self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
+
+ def test_create_or_update_organization_type_new_instance_federal_does_nothing(self):
+ """Test if create_or_update_organization_type does nothing when creating a new instance for federal"""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
+ is_election_board=True,
+ )
+
+ domain_information = DomainInformation.create_from_da(domain_request)
+ self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL)
+ self.assertEqual(domain_information.is_election_board, None)
+
+ def test_create_or_update_organization_type_existing_instance_updates_election_board(self):
+ """Test create_or_update_organization_type for an existing instance."""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=False,
+ )
+ domain_information = DomainInformation.create_from_da(domain_request)
+ domain_information.is_election_board = True
+ domain_information.save()
+
+ self.assertEqual(domain_information.is_election_board, True)
+ self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
+
+ # Try reverting the election board value
+ domain_information.is_election_board = False
+ domain_information.save()
+ domain_information.refresh_from_db()
+
+ self.assertEqual(domain_information.is_election_board, False)
+ self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
+
+ # Try reverting setting an invalid value for election board (should revert to False)
+ domain_information.is_election_board = None
+ domain_information.save()
+
+ self.assertEqual(domain_information.is_election_board, False)
+ self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
+
+ def test_create_or_update_organization_type_existing_instance_updates_generic_org_type(self):
+ """Test create_or_update_organization_type when modifying generic_org_type on an existing instance."""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=True,
+ )
+ domain_information = DomainInformation.create_from_da(domain_request)
+
+ domain_information.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
+ domain_information.save()
+
+ # Election board should be None because interstate cannot have an election board.
+ self.assertEqual(domain_information.is_election_board, None)
+ self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.INTERSTATE)
+
+ # Try changing the org Type to something that CAN have an election board.
+ domain_request_tribal = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="startedTribal.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
+ is_election_board=True,
+ )
+ domain_information_tribal = DomainInformation.create_from_da(domain_request_tribal)
+ self.assertEqual(
+ domain_information_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
+ )
+
+ # Change the org type
+ domain_information_tribal.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
+ domain_information_tribal.save()
+
+ self.assertEqual(domain_information_tribal.is_election_board, True)
+ self.assertEqual(
+ domain_information_tribal.organization_type,
+ DomainRequest.OrgChoicesElectionOffice.STATE_OR_TERRITORY_ELECTION,
+ )
+
+ def test_create_or_update_organization_type_no_update(self):
+ """Test create_or_update_organization_type when there are no values to update."""
+
+ # Test for when both generic_org_type and organization_type is declared,
+ # and are both non-election board
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="started.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=False,
+ )
+ domain_information = DomainInformation.create_from_da(domain_request)
+ domain_information.save()
+ self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
+ self.assertEqual(domain_information.is_election_board, False)
+ self.assertEqual(domain_information.generic_org_type, DomainRequest.OrganizationChoices.CITY)
+
+ # Test for when both generic_org_type and organization_type is declared,
+ # and are both election board
+ domain_request_election = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="startedElection.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ is_election_board=True,
+ organization_type=DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION,
+ )
+ domain_information_election = DomainInformation.create_from_da(domain_request_election)
+
+ self.assertEqual(
+ domain_information_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
+ )
+ self.assertEqual(domain_information_election.is_election_board, True)
+ self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
+
+ # Modify an unrelated existing value for both, and ensure that everything is still consistent
+ domain_information.city = "Fudge"
+ domain_information_election.city = "Caramel"
+ domain_information.save()
+ domain_information_election.save()
+
+ self.assertEqual(domain_information.city, "Fudge")
+ self.assertEqual(domain_information_election.city, "Caramel")
+
+ # Test for non-election
+ self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
+ self.assertEqual(domain_information.is_election_board, False)
+ self.assertEqual(domain_information.generic_org_type, DomainRequest.OrganizationChoices.CITY)
+
+ # Test for election
+ self.assertEqual(
+ domain_information_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
+ )
+ self.assertEqual(domain_information_election.is_election_board, True)
+ self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
From 92f1a0dab4d551e1117713d3e79a1775cbcaef09 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 1 Apr 2024 15:47:15 -0600
Subject: [PATCH 32/80] Update test_admin.py
---
src/registrar/tests/test_admin.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 3999acc4e..b73d9bce1 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1324,7 +1324,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, "Testy2 Tester2")
# == Check for the authorizing_official == #
- expected_email = "testy@town.com"
+
expected_ao_fields = [
# Field, expected value
("title", "Chief Tester"),
@@ -1338,7 +1338,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, "Testy Tester", count=5)
# == Test the other_employees field == #
- expected_email = "testy@town.com"
+
expected_other_employees_fields = [
# Field, expected value
("title", "Another Tester"),
@@ -2052,7 +2052,7 @@ class TestDomainInformationAdmin(TestCase):
self.assertContains(response, "Testy2 Tester2")
# == Check for the authorizing_official == #
- expected_email = "testy@town.com"
+
expected_ao_fields = [
# Field, expected value
("title", "Chief Tester"),
@@ -2066,7 +2066,7 @@ class TestDomainInformationAdmin(TestCase):
self.assertContains(response, "Testy Tester", count=5)
# == Test the other_employees field == #
- expected_email = "testy@town.com"
+
expected_other_employees_fields = [
# Field, expected value
("title", "Another Tester"),
From 45c047dc8a5cf32a888d620dde33036b55b40df9 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Mon, 1 Apr 2024 21:35:29 -0400
Subject: [PATCH 33/80] optimize by removing filters from inside iterations
---
src/registrar/utility/csv_export.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 0521a71be..d4485e11f 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -107,7 +107,7 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
# Get lists of emails for active and invited domain managers
dm_active_emails = [dm.user.email for dm in domain.permissions.all()]
dm_invited_emails = [
- invite.email for invite in invites_with_invited_status.filter(domain=domain)
+ invite.email for invite in invites_with_invited_status if invite.domain_id == domain_info.domain_id
]
# Set up the "matching headers" + row field data for email and status
@@ -160,7 +160,7 @@ def update_columns_with_domain_managers(
Updated update_columns, columns, max_dm_active, max_dm_invited, max_dm_total"""
dm_active = domain_info.domain.permissions.count()
- dm_invited = invites_with_invited_status.filter(domain=domain_info.domain).count()
+ dm_invited = sum(1 for invite in invites_with_invited_status if invite.domain_id == domain_info.domain_id)
if dm_active + dm_invited > max_dm_total:
max_dm_total = dm_active + dm_invited
From dba2dfb6c2034668fc658f461262ba4f5a6c1c89 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Mon, 1 Apr 2024 21:46:06 -0400
Subject: [PATCH 34/80] Revert last experiment
---
src/registrar/utility/csv_export.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index d4485e11f..0521a71be 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -107,7 +107,7 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
# Get lists of emails for active and invited domain managers
dm_active_emails = [dm.user.email for dm in domain.permissions.all()]
dm_invited_emails = [
- invite.email for invite in invites_with_invited_status if invite.domain_id == domain_info.domain_id
+ invite.email for invite in invites_with_invited_status.filter(domain=domain)
]
# Set up the "matching headers" + row field data for email and status
@@ -160,7 +160,7 @@ def update_columns_with_domain_managers(
Updated update_columns, columns, max_dm_active, max_dm_invited, max_dm_total"""
dm_active = domain_info.domain.permissions.count()
- dm_invited = sum(1 for invite in invites_with_invited_status if invite.domain_id == domain_info.domain_id)
+ dm_invited = invites_with_invited_status.filter(domain=domain_info.domain).count()
if dm_active + dm_invited > max_dm_total:
max_dm_total = dm_active + dm_invited
From 79daf4c65176060d62fe82af2c3811bb9f0148ab Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 2 Apr 2024 09:11:05 -0600
Subject: [PATCH 35/80] Update test
---
src/registrar/signals.py | 2 +-
src/registrar/tests/test_reports.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index 5cf035eb9..301459f93 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -93,7 +93,7 @@ def create_or_update_organization_type(sender, instance, **kwargs):
organization_type_needs_update = generic_org_type_changed or is_election_board_changed
generic_org_type_needs_update = organization_type_changed
- # Update that field
+ # Update the field
if organization_type_needs_update:
_update_org_type_from_generic_org_and_election(instance, generic_org_to_org_map)
elif generic_org_type_needs_update:
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index d3eec946d..459ccde0f 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -562,7 +562,7 @@ class ExportDataTest(MockDb, MockEppLib):
"MANAGED DOMAINS COUNTS AT END DATE\n"
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
"Special district,School district,Election office\n"
- "3,2,1,0,0,0,0,0,0,2\n"
+ "3,2,1,0,0,0,0,0,0,0\n"
"\n"
"Domain name,Domain type,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
"cdomain11.govFederal-Executivemeoward@rocks.com\n"
From 410440ee709b26e086f3c85b80c04a6ba1c5bb63 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Tue, 2 Apr 2024 12:39:29 -0400
Subject: [PATCH 36/80] Optimize by using prebuilt dicts
---
src/registrar/utility/csv_export.py | 139 ++++++++++++++++++----------
1 file changed, 88 insertions(+), 51 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 0521a71be..d8638d0d9 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -11,6 +11,8 @@ from django.db.models import F, Value, CharField, Q, Count
from django.db.models.functions import Concat, Coalesce
from registrar.models.public_contact import PublicContact
+from registrar.models.user_domain_role import UserDomainRole
+from registrar.models.utility.generic_helper import Timer
from registrar.utility.enums import DefaultEmail
logger = logging.getLogger(__name__)
@@ -33,7 +35,6 @@ def get_domain_infos(filter_condition, sort_fields):
"""
domain_infos = (
DomainInformation.objects.select_related("domain", "authorizing_official")
- .prefetch_related("domain__permissions", "domain__invitations")
.filter(**filter_condition)
.order_by(*sort_fields)
.distinct()
@@ -53,7 +54,7 @@ def get_domain_infos(filter_condition, sort_fields):
return domain_infos_cleaned
-def parse_domain_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False, invites_with_invited_status=None):
+def parse_domain_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False, domain_invitation_emails=None, domain_permissions_emails=None):
"""Given a set of columns, generate a new row from cleaned column data"""
# Domain should never be none when parsing this information
@@ -105,10 +106,9 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
if get_domain_managers:
# Get lists of emails for active and invited domain managers
- dm_active_emails = [dm.user.email for dm in domain.permissions.all()]
- dm_invited_emails = [
- invite.email for invite in invites_with_invited_status.filter(domain=domain)
- ]
+
+ dm_active_emails = domain_permissions_emails.get(domain_info.domain.name, [])
+ dm_invited_emails = domain_invitation_emails.get(domain_info.domain.name, [])
# Set up the "matching headers" + row field data for email and status
i = 0 # Declare i outside of the loop to avoid a reference before assignment in the second loop
@@ -148,7 +148,7 @@ def _get_security_emails(sec_contact_ids):
def update_columns_with_domain_managers(
- domain_info,invites_with_invited_status, update_columns, columns, max_dm_total
+ domain_info,domain_invitation_emails, domain_permissions_emails, update_columns, columns, max_dm_total
):
"""Helper function that works with 'global' variables set in write_domains_csv
Accepts:
@@ -159,8 +159,25 @@ def update_columns_with_domain_managers(
Returns:
Updated update_columns, columns, max_dm_active, max_dm_invited, max_dm_total"""
- dm_active = domain_info.domain.permissions.count()
- dm_invited = invites_with_invited_status.filter(domain=domain_info.domain).count()
+ dm_active = 0
+ dm_invited = 0
+ try:
+ # logger.info(f'domain_invitation_emails {domain_invitation_emails[domain_info.domain.name]}')
+
+ # Get the list of invitation emails for the domain name if it exists, otherwise, return an empty list
+ invitation_emails = domain_invitation_emails.get(domain_info.domain.name, [])
+ # Count the number of invitation emails
+ dm_invited = len(invitation_emails)
+ except KeyError:
+ pass
+
+ try:
+ active_emails = domain_permissions_emails.get(domain_info.domain.name, [])
+ # Count the number of invitation emails
+ dm_active = len(active_emails)
+ except KeyError:
+ pass
+
if dm_active + dm_invited > max_dm_total:
max_dm_total = dm_active + dm_invited
@@ -193,61 +210,80 @@ def write_domains_csv(
should_write_header: Conditional bc export_data_domain_growth_to_csv calls write_body twice
"""
- all_domain_infos = get_domain_infos(filter_condition, sort_fields)
+ with Timer():
+ all_domain_infos = get_domain_infos(filter_condition, sort_fields)
- # Store all security emails to avoid epp calls or excessive filters
- sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
+ # Store all security emails to avoid epp calls or excessive filters
+ sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
- security_emails_dict = _get_security_emails(sec_contact_ids)
+ security_emails_dict = _get_security_emails(sec_contact_ids)
- # Reduce the memory overhead when performing the write operation
- paginator = Paginator(all_domain_infos, 1000)
+ # Reduce the memory overhead when performing the write operation
+ paginator = Paginator(all_domain_infos, 1000)
- # We get the number of domain managers (DMs) an the domain
- # that has the most DMs so we can set the header row appropriately
+ # We get the number of domain managers (DMs) an the domain
+ # that has the most DMs so we can set the header row appropriately
- max_dm_total = 0
- update_columns = False
- invites_with_invited_status=None
+ max_dm_total = 0
+ update_columns = False
+
+ invites_with_invited_status=None
+ domain_invitation_emails = {}
+ domain_permissions_emails = {}
- if get_domain_managers:
- invites_with_invited_status = DomainInvitation.objects.filter(status=DomainInvitation.DomainInvitationStatus.INVITED).prefetch_related("domain")
+ if get_domain_managers:
+ invites_with_invited_status = DomainInvitation.objects.filter(status=DomainInvitation.DomainInvitationStatus.INVITED).select_related("domain")
- # zander = DomainInformation.objects.filter(**filter_condition).annotate(invitations_count=Count('invitation', filter=Q(invitation__status='invited'))).values_list('domain_name', 'invitations_count')
- # logger.info(f'zander {zander}')
- # zander_dict = dict(zander)
- # logger.info(f'zander_dict {zander_dict}')
+ # Iterate through each domain invitation and populate the dictionary
+ for invite in invites_with_invited_status:
+ domain_name = invite.domain.name
+ email = invite.email
+ if domain_name not in domain_invitation_emails:
+ domain_invitation_emails[domain_name] = []
+ domain_invitation_emails[domain_name].append(email)
- # This var will live outside of the nested for loops to aggregate
- # the data from those loops
- total_body_rows = []
+ domain_permissions = UserDomainRole.objects.all()
- for page_num in paginator.page_range:
- rows = []
- page = paginator.page(page_num)
- for domain_info in page.object_list:
+ # Iterate through each domain invitation and populate the dictionary
+ for permission in domain_permissions:
+ domain_name = permission.domain.name
+ email = permission.user.email
+ if domain_name not in domain_permissions_emails:
+ domain_permissions_emails[domain_name] = []
+ domain_permissions_emails[domain_name].append(email)
- # Get max number of domain managers
- if get_domain_managers:
- update_columns, columns, max_dm_total = (
- update_columns_with_domain_managers(
- domain_info,invites_with_invited_status, update_columns, columns, max_dm_total
+ # logger.info(f'domain_invitation_emails {domain_invitation_emails}')
+
+ # This var will live outside of the nested for loops to aggregate
+ # the data from those loops
+ total_body_rows = []
+
+ for page_num in paginator.page_range:
+ rows = []
+ page = paginator.page(page_num)
+ for domain_info in page.object_list:
+
+ # Get max number of domain managers
+ if get_domain_managers:
+ update_columns, columns, max_dm_total = (
+ update_columns_with_domain_managers(
+ domain_info, domain_invitation_emails,domain_permissions_emails, update_columns, columns, max_dm_total
+ )
)
- )
- try:
- row = parse_domain_row(columns, domain_info, security_emails_dict, get_domain_managers, invites_with_invited_status)
- rows.append(row)
- except ValueError:
- # This should not happen. If it does, just skip this row.
- # It indicates that DomainInformation.domain is None.
- logger.error("csv_export -> Error when parsing row, domain was None")
- continue
- total_body_rows.extend(rows)
+ try:
+ row = parse_domain_row(columns, domain_info, security_emails_dict, get_domain_managers, domain_invitation_emails,domain_permissions_emails)
+ rows.append(row)
+ except ValueError:
+ # This should not happen. If it does, just skip this row.
+ # It indicates that DomainInformation.domain is None.
+ logger.error("csv_export -> Error when parsing row, domain was None")
+ continue
+ total_body_rows.extend(rows)
- if should_write_header:
- write_header(writer, columns)
- writer.writerows(total_body_rows)
+ if should_write_header:
+ write_header(writer, columns)
+ writer.writerows(total_body_rows)
def get_requests(filter_condition, sort_fields):
@@ -360,6 +396,7 @@ def export_data_type_to_csv(csv_file):
Domain.State.READY,
Domain.State.DNS_NEEDED,
Domain.State.ON_HOLD,
+ Domain.State.UNKNOWN,
],
}
write_domains_csv(
From 045140091b11254b389eb0329fb70b342d2cedf3 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Tue, 2 Apr 2024 12:44:00 -0400
Subject: [PATCH 37/80] do not pull unknown domains
---
src/registrar/utility/csv_export.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index d8638d0d9..913fe95db 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -396,7 +396,6 @@ def export_data_type_to_csv(csv_file):
Domain.State.READY,
Domain.State.DNS_NEEDED,
Domain.State.ON_HOLD,
- Domain.State.UNKNOWN,
],
}
write_domains_csv(
From f753f3e6f1209b7c12934d327bc11dc5ce80ba09 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 2 Apr 2024 13:06:41 -0600
Subject: [PATCH 38/80] Linter
---
src/registrar/tests/test_admin.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index b73d9bce1..804b94711 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1300,7 +1300,6 @@ class TestDomainRequestAdmin(MockEppLib):
# == Check for the creator == #
# Check for the right title, email, and phone number in the response.
- expected_email = "meoward.jones@igorville.gov"
expected_creator_fields = [
# Field, expected value
("title", "Treat inspector"),
@@ -1313,7 +1312,6 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, "Meoward Jones")
# == Check for the submitter == #
- expected_email = "mayor@igorville.gov"
expected_submitter_fields = [
# Field, expected value
("title", "Admin Tester"),
@@ -2028,7 +2026,6 @@ class TestDomainInformationAdmin(TestCase):
# Check for the right title, email, and phone number in the response.
# We only need to check for the end tag
# (Otherwise this test will fail if we change classes, etc)
- expected_email = "meoward.jones@igorville.gov"
expected_creator_fields = [
# Field, expected value
("title", "Treat inspector"),
@@ -2041,7 +2038,6 @@ class TestDomainInformationAdmin(TestCase):
self.assertContains(response, "Meoward Jones")
# == Check for the submitter == #
- expected_email = "mayor@igorville.gov"
expected_submitter_fields = [
# Field, expected value
("title", "Admin Tester"),
From 7c8d7d293c5fda1c430fb6749190864a2d44b845 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 2 Apr 2024 13:50:15 -0600
Subject: [PATCH 39/80] Simplify _update_org_type_from_generic_org_and_election
---
src/registrar/signals.py | 27 ++++++++++-----------------
1 file changed, 10 insertions(+), 17 deletions(-)
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index 301459f93..135336b04 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -108,25 +108,18 @@ def _update_org_type_from_generic_org_and_election(instance, org_map):
# We convert to a string because the enum types are different.
generic_org_type = str(instance.generic_org_type)
-
- # If the election board is none, then it tells us that it is an invalid field.
- # Such as federal, interstate, or school_district.
- if instance.is_election_board is None and generic_org_type not in org_map:
- instance.organization_type = generic_org_type
- return instance
- elif instance.is_election_board is None and generic_org_type in org_map:
- # This can only happen with manual data tinkering, which causes these to be out of sync.
- instance.is_election_board = False
- logger.warning("create_or_update_organization_type() -> is_election_board is out of sync. Updating value.")
-
- if generic_org_type in org_map:
- # Swap to the election type if it is an election board. Otherwise, stick to the normal one.
- instance.organization_type = org_map[generic_org_type] if instance.is_election_board else generic_org_type
- else:
- # Election board should be reset to None if the record
+ if generic_org_type not in generic_org_type:
+ # Election board should always be reset to None if the record
# can't have one. For example, federal.
- instance.organization_type = generic_org_type
instance.is_election_board = None
+ instance.organization_type = generic_org_type
+ else:
+ # This can only happen with manual data tinkering, which causes these to be out of sync.
+ if instance.is_election_board is None:
+ logger.warning("create_or_update_organization_type() -> is_election_board is out of sync. Updating value.")
+ instance.is_election_board = False
+
+ instance.organization_type = org_map[generic_org_type] if instance.is_election_board else generic_org_type
def _update_generic_org_and_election_from_org_type(instance, election_org_map, generic_org_map):
From 20067eec699715607cd4f19b69494eb2fa2fa8b2 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 2 Apr 2024 14:19:04 -0600
Subject: [PATCH 40/80] Fix typo
---
src/registrar/signals.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index 135336b04..ad287219d 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -108,7 +108,7 @@ def _update_org_type_from_generic_org_and_election(instance, org_map):
# We convert to a string because the enum types are different.
generic_org_type = str(instance.generic_org_type)
- if generic_org_type not in generic_org_type:
+ if generic_org_type not in org_map:
# Election board should always be reset to None if the record
# can't have one. For example, federal.
instance.is_election_board = None
From 70bd79516abc00ef875aec3013eff9b93bbcb8ec Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 2 Apr 2024 15:38:12 -0600
Subject: [PATCH 41/80] Use sender
---
src/registrar/signals.py | 12 ++----------
1 file changed, 2 insertions(+), 10 deletions(-)
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index ad287219d..d106f974c 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
@receiver(pre_save, sender=DomainRequest)
@receiver(pre_save, sender=DomainInformation)
-def create_or_update_organization_type(sender, instance, **kwargs):
+def create_or_update_organization_type(sender: DomainRequest | DomainInformation, instance, **kwargs):
"""The organization_type field on DomainRequest and DomainInformation is consituted from the
generic_org_type and is_election_board fields. To keep the organization_type
field up to date, we need to update it before save based off of those field
@@ -62,15 +62,7 @@ def create_or_update_organization_type(sender, instance, **kwargs):
# == Init variables == #
# Instance is already in the database, fetch its current state
- if isinstance(instance, DomainRequest):
- current_instance = DomainRequest.objects.get(id=instance.id)
- elif isinstance(instance, DomainInformation):
- current_instance = DomainInformation.objects.get(id=instance.id)
- else:
- # This should never occur. But it never hurts to have this check anyway.
- raise ValueError(
- "create_or_update_organization_type() -> instance was not DomainRequest or DomainInformation"
- )
+ current_instance = sender.objects.get(id=instance.id)
# Check the new and old values
generic_org_type_changed = instance.generic_org_type != current_instance.generic_org_type
From c5e6295a8a5c25f480c0465b2dd328224463cde1 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Tue, 2 Apr 2024 20:54:43 -0400
Subject: [PATCH 42/80] refactor wip
---
src/registrar/tests/test_reports.py | 39 +++--
src/registrar/utility/csv_export.py | 247 +++++++++++++++-------------
src/registrar/views/admin_views.py | 10 +-
3 files changed, 162 insertions(+), 134 deletions(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index cd882c4f8..b4861560f 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -243,7 +243,12 @@ class ExportDataTest(MockDb, MockEppLib):
self.maxDiff = None
# Call the export functions
write_domains_csv(
- writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
+ writer,
+ columns,
+ sort_fields,
+ filter_condition,
+ should_get_domain_managers=False,
+ should_write_header=True,
)
# Reset the CSV file's position to the beginning
@@ -305,7 +310,12 @@ class ExportDataTest(MockDb, MockEppLib):
}
# Call the export functions
write_domains_csv(
- writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
+ writer,
+ columns,
+ sort_fields,
+ filter_condition,
+ should_get_domain_managers=False,
+ should_write_header=True,
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
@@ -358,7 +368,12 @@ class ExportDataTest(MockDb, MockEppLib):
}
# Call the export functions
write_domains_csv(
- writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
+ writer,
+ columns,
+ sort_fields,
+ filter_condition,
+ should_get_domain_managers=False,
+ should_write_header=True,
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
@@ -438,7 +453,7 @@ class ExportDataTest(MockDb, MockEppLib):
columns,
sort_fields,
filter_condition,
- get_domain_managers=False,
+ should_get_domain_managers=False,
should_write_header=True,
)
write_domains_csv(
@@ -446,7 +461,7 @@ class ExportDataTest(MockDb, MockEppLib):
columns,
sort_fields_for_deleted_domains,
filter_conditions_for_deleted_domains,
- get_domain_managers=False,
+ should_get_domain_managers=False,
should_write_header=False,
)
# Reset the CSV file's position to the beginning
@@ -514,7 +529,12 @@ class ExportDataTest(MockDb, MockEppLib):
self.maxDiff = None
# Call the export functions
write_domains_csv(
- writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
+ writer,
+ columns,
+ sort_fields,
+ filter_condition,
+ should_get_domain_managers=True,
+ should_write_header=True,
)
# Reset the CSV file's position to the beginning
@@ -697,13 +717,8 @@ class HelperFunctions(MockDb):
"domain__first_ready__lte": self.end_date,
}
# Test with distinct
- managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition, True)
- expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 2]
- self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
-
- # Test without distinct
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
- expected_content = [3, 4, 1, 0, 0, 0, 0, 0, 0, 2]
+ expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 2]
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
def test_get_sliced_requests(self):
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 913fe95db..faad25b28 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -7,7 +7,7 @@ from registrar.models.domain_request import DomainRequest
from registrar.models.domain_information import DomainInformation
from django.utils import timezone
from django.core.paginator import Paginator
-from django.db.models import F, Value, CharField, Q, Count
+from django.db.models import F, Value, CharField
from django.db.models.functions import Concat, Coalesce
from registrar.models.public_contact import PublicContact
@@ -54,7 +54,14 @@ def get_domain_infos(filter_condition, sort_fields):
return domain_infos_cleaned
-def parse_domain_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False, domain_invitation_emails=None, domain_permissions_emails=None):
+def parse_domain_row(
+ columns,
+ domain_info: DomainInformation,
+ dict_security_emails_dict=None,
+ should_get_domain_managers=False,
+ dict_domain_invitations_with_invited_status=None,
+ dict_user_domain_roles=None,
+):
"""Given a set of columns, generate a new row from cleaned column data"""
# Domain should never be none when parsing this information
@@ -66,8 +73,8 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
# Grab the security email from a preset dictionary.
# If nothing exists in the dictionary, grab from .contacts.
- if security_emails_dict is not None and domain.name in security_emails_dict:
- _email = security_emails_dict.get(domain.name)
+ if dict_security_emails_dict is not None and domain.name in dict_security_emails_dict:
+ _email = dict_security_emails_dict.get(domain.name)
security_email = _email if _email is not None else " "
else:
# If the dictionary doesn't contain that data, lets filter for it manually.
@@ -104,20 +111,20 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
"Deleted": domain.deleted,
}
- if get_domain_managers:
+ if should_get_domain_managers:
# Get lists of emails for active and invited domain managers
- dm_active_emails = domain_permissions_emails.get(domain_info.domain.name, [])
- dm_invited_emails = domain_invitation_emails.get(domain_info.domain.name, [])
+ dms_active_emails = dict_user_domain_roles.get(domain_info.domain.name, [])
+ dms_invited_emails = dict_domain_invitations_with_invited_status.get(domain_info.domain.name, [])
# Set up the "matching headers" + row field data for email and status
i = 0 # Declare i outside of the loop to avoid a reference before assignment in the second loop
- for i, dm_email in enumerate(dm_active_emails, start=1):
+ for i, dm_email in enumerate(dms_active_emails, start=1):
FIELDS[f"Domain manager {i}"] = dm_email
FIELDS[f"DM{i} status"] = "R"
# Continue enumeration from where we left off and add data for invited domain managers
- for j, dm_email in enumerate(dm_invited_emails, start=i + 1):
+ for j, dm_email in enumerate(dms_invited_emails, start=i + 1):
FIELDS[f"Domain manager {j}"] = dm_email
FIELDS[f"DM{j} status"] = "I"
@@ -129,7 +136,7 @@ def _get_security_emails(sec_contact_ids):
"""
Retrieve security contact emails for the given security contact IDs.
"""
- security_emails_dict = {}
+ dict_security_emails_dict = {}
public_contacts = (
PublicContact.objects.only("email", "domain__name")
.select_related("domain")
@@ -139,144 +146,152 @@ def _get_security_emails(sec_contact_ids):
# Populate a dictionary of domain names and their security contacts
for contact in public_contacts:
domain: Domain = contact.domain
- if domain is not None and domain.name not in security_emails_dict:
- security_emails_dict[domain.name] = contact.email
+ if domain is not None and domain.name not in dict_security_emails_dict:
+ dict_security_emails_dict[domain.name] = contact.email
else:
logger.warning("csv_export -> Domain was none for PublicContact")
- return security_emails_dict
+ return dict_security_emails_dict
+
+
+def count_domain_managers(domain_name, dict_domain_invitations_with_invited_status, dict_user_domain_roles):
+ """Count active and invited domain managers"""
+ dms_active = len(dict_user_domain_roles.get(domain_name, []))
+ dms_invited = len(dict_domain_invitations_with_invited_status.get(domain_name, []))
+ return dms_active, dms_invited
+
+
+def update_columns(columns, dms_total, should_update_columns):
+ """Update columns if necessary"""
+ if should_update_columns:
+ for i in range(1, dms_total + 1):
+ email_column_header = f"Domain manager {i}"
+ status_column_header = f"DM{i} status"
+ if email_column_header not in columns:
+ columns.append(email_column_header)
+ columns.append(status_column_header)
+ should_update_columns = False
+ return columns, should_update_columns, dms_total
def update_columns_with_domain_managers(
- domain_info,domain_invitation_emails, domain_permissions_emails, update_columns, columns, max_dm_total
+ columns,
+ domain_info,
+ should_update_columns,
+ dms_total,
+ dict_domain_invitations_with_invited_status,
+ dict_user_domain_roles,
):
- """Helper function that works with 'global' variables set in write_domains_csv
- Accepts:
- domain_info -> Domains to parse
- update_columns -> A control to make sure we only run the columns test and update when needed
- columns -> The header cells in the csv that's under construction
- max_dm_total -> Starts at 0 and gets updated and passed again through this method
- Returns:
- Updated update_columns, columns, max_dm_active, max_dm_invited, max_dm_total"""
+ """Helper function to update columns with domain manager information"""
- dm_active = 0
- dm_invited = 0
- try:
- # logger.info(f'domain_invitation_emails {domain_invitation_emails[domain_info.domain.name]}')
-
- # Get the list of invitation emails for the domain name if it exists, otherwise, return an empty list
- invitation_emails = domain_invitation_emails.get(domain_info.domain.name, [])
- # Count the number of invitation emails
- dm_invited = len(invitation_emails)
- except KeyError:
- pass
+ domain_name = domain_info.domain.name
try:
- active_emails = domain_permissions_emails.get(domain_info.domain.name, [])
- # Count the number of invitation emails
- dm_active = len(active_emails)
- except KeyError:
- pass
-
+ dms_active, dms_invited = count_domain_managers(
+ domain_name, dict_domain_invitations_with_invited_status, dict_user_domain_roles
+ )
- if dm_active + dm_invited > max_dm_total:
- max_dm_total = dm_active + dm_invited
- update_columns = True
+ if dms_active + dms_invited > dms_total:
+ dms_total = dms_active + dms_invited
+ should_update_columns = True
- if update_columns:
- for i in range(1, max_dm_total + 1):
- column_name = f"Domain manager {i}"
- column2_name = f"DM{i} status"
- if column_name not in columns:
- columns.append(column_name)
- columns.append(column2_name)
- update_columns = False
+ except Exception as err:
+ logger.error(f"Exception while parsing domain managers for reports: {err}")
- return update_columns, columns, max_dm_total
+ return update_columns(columns, dms_total, should_update_columns)
+def build_dictionaries_for_domain_managers(dict_user_domain_roles, dict_domain_invitations_with_invited_status):
+ """Helper function that builds dicts for invited users and active domain
+ managers. We do so to avoid filtering within loops."""
+
+ user_domain_roles = None
+ user_domain_roles = UserDomainRole.objects.all()
+
+ # Iterate through each user domain role and populate the dictionary
+ for user_domain_role in user_domain_roles:
+ domain_name = user_domain_role.domain.name
+ email = user_domain_role.user.email
+ if domain_name not in dict_user_domain_roles:
+ dict_user_domain_roles[domain_name] = []
+ dict_user_domain_roles[domain_name].append(email)
+
+ domain_invitations_with_invited_status = None
+ domain_invitations_with_invited_status = DomainInvitation.objects.filter(
+ status=DomainInvitation.DomainInvitationStatus.INVITED
+ ).select_related("domain")
+
+ # Iterate through each domain invitation and populate the dictionary
+ for invite in domain_invitations_with_invited_status:
+ domain_name = invite.domain.name
+ email = invite.email
+ if domain_name not in dict_domain_invitations_with_invited_status:
+ dict_domain_invitations_with_invited_status[domain_name] = []
+ dict_domain_invitations_with_invited_status[domain_name].append(email)
+
+ return dict_user_domain_roles, dict_domain_invitations_with_invited_status
+
def write_domains_csv(
writer,
columns,
sort_fields,
filter_condition,
- get_domain_managers=False,
+ should_get_domain_managers=False,
should_write_header=True,
):
"""
Receives params from the parent methods and outputs a CSV with filtered and sorted domains.
Works with write_header as long as the same writer object is passed.
- get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
+ should_get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
should_write_header: Conditional bc export_data_domain_growth_to_csv calls write_body twice
"""
with Timer():
+ # Retrieve domain information and all sec emails
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
-
- # Store all security emails to avoid epp calls or excessive filters
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
-
- security_emails_dict = _get_security_emails(sec_contact_ids)
-
- # Reduce the memory overhead when performing the write operation
+ dict_security_emails_dict = _get_security_emails(sec_contact_ids)
paginator = Paginator(all_domain_infos, 1000)
- # We get the number of domain managers (DMs) an the domain
- # that has the most DMs so we can set the header row appropriately
-
- max_dm_total = 0
- update_columns = False
-
- invites_with_invited_status=None
- domain_invitation_emails = {}
- domain_permissions_emails = {}
-
- if get_domain_managers:
- invites_with_invited_status = DomainInvitation.objects.filter(status=DomainInvitation.DomainInvitationStatus.INVITED).select_related("domain")
-
- # Iterate through each domain invitation and populate the dictionary
- for invite in invites_with_invited_status:
- domain_name = invite.domain.name
- email = invite.email
- if domain_name not in domain_invitation_emails:
- domain_invitation_emails[domain_name] = []
- domain_invitation_emails[domain_name].append(email)
-
- domain_permissions = UserDomainRole.objects.all()
-
- # Iterate through each domain invitation and populate the dictionary
- for permission in domain_permissions:
- domain_name = permission.domain.name
- email = permission.user.email
- if domain_name not in domain_permissions_emails:
- domain_permissions_emails[domain_name] = []
- domain_permissions_emails[domain_name].append(email)
-
- # logger.info(f'domain_invitation_emails {domain_invitation_emails}')
-
- # This var will live outside of the nested for loops to aggregate
- # the data from those loops
+ # Initialize variables
+ dms_total = 0
+ should_update_columns = False
total_body_rows = []
+ dict_user_domain_roles = {}
+ dict_domain_invitations_with_invited_status = {}
+ # Build dictionaries if necessary
+ if should_get_domain_managers:
+ dict_user_domain_roles, dict_domain_invitations_with_invited_status = build_dictionaries_for_domain_managers(
+ dict_user_domain_roles, dict_domain_invitations_with_invited_status
+ )
+
+ # Process domain information
for page_num in paginator.page_range:
rows = []
page = paginator.page(page_num)
for domain_info in page.object_list:
-
- # Get max number of domain managers
- if get_domain_managers:
- update_columns, columns, max_dm_total = (
- update_columns_with_domain_managers(
- domain_info, domain_invitation_emails,domain_permissions_emails, update_columns, columns, max_dm_total
- )
+ if should_get_domain_managers:
+ columns, dms_total, should_update_columns = update_columns_with_domain_managers(
+ columns,
+ domain_info,
+ should_update_columns,
+ dms_total,
+ dict_domain_invitations_with_invited_status,
+ dict_user_domain_roles,
)
try:
- row = parse_domain_row(columns, domain_info, security_emails_dict, get_domain_managers, domain_invitation_emails,domain_permissions_emails)
+ row = parse_domain_row(
+ columns,
+ domain_info,
+ dict_security_emails_dict,
+ should_get_domain_managers,
+ dict_domain_invitations_with_invited_status,
+ dict_user_domain_roles,
+ )
rows.append(row)
except ValueError:
- # This should not happen. If it does, just skip this row.
- # It indicates that DomainInformation.domain is None.
logger.error("csv_export -> Error when parsing row, domain was None")
continue
total_body_rows.extend(rows)
@@ -399,7 +414,7 @@ def export_data_type_to_csv(csv_file):
],
}
write_domains_csv(
- writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
+ writer, columns, sort_fields, filter_condition, should_get_domain_managers=True, should_write_header=True
)
@@ -432,7 +447,7 @@ def export_data_full_to_csv(csv_file):
],
}
write_domains_csv(
- writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
+ writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
)
@@ -466,7 +481,7 @@ def export_data_federal_to_csv(csv_file):
],
}
write_domains_csv(
- writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
+ writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
)
@@ -536,19 +551,19 @@ def export_data_domain_growth_to_csv(csv_file, start_date, end_date):
}
write_domains_csv(
- writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
+ writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
)
write_domains_csv(
writer,
columns,
sort_fields_for_deleted_domains,
filter_condition_for_deleted_domains,
- get_domain_managers=False,
+ should_get_domain_managers=False,
should_write_header=False,
)
-def get_sliced_domains(filter_condition, distinct=False):
+def get_sliced_domains(filter_condition):
"""Get filtered domains counts sliced by org type and election office.
Pass distinct=True when filtering by permissions so we do not to count multiples
when a domain has more that one manager.
@@ -639,7 +654,7 @@ def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
"domain__permissions__isnull": False,
"domain__first_ready__lte": start_date_formatted,
}
- managed_domains_sliced_at_start_date = get_sliced_domains(filter_managed_domains_start_date, True)
+ managed_domains_sliced_at_start_date = get_sliced_domains(filter_managed_domains_start_date)
writer.writerow(["MANAGED DOMAINS COUNTS AT START DATE"])
writer.writerow(
@@ -663,7 +678,7 @@ def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
"domain__permissions__isnull": False,
"domain__first_ready__lte": end_date_formatted,
}
- managed_domains_sliced_at_end_date = get_sliced_domains(filter_managed_domains_end_date, True)
+ managed_domains_sliced_at_end_date = get_sliced_domains(filter_managed_domains_end_date)
writer.writerow(["MANAGED DOMAINS COUNTS AT END DATE"])
writer.writerow(
@@ -688,7 +703,7 @@ def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
columns,
sort_fields,
filter_managed_domains_end_date,
- get_domain_managers=True,
+ should_get_domain_managers=True,
should_write_header=True,
)
@@ -712,7 +727,7 @@ def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
"domain__permissions__isnull": True,
"domain__first_ready__lte": start_date_formatted,
}
- unmanaged_domains_sliced_at_start_date = get_sliced_domains(filter_unmanaged_domains_start_date, True)
+ unmanaged_domains_sliced_at_start_date = get_sliced_domains(filter_unmanaged_domains_start_date)
writer.writerow(["UNMANAGED DOMAINS AT START DATE"])
writer.writerow(
@@ -736,7 +751,7 @@ def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
"domain__permissions__isnull": True,
"domain__first_ready__lte": end_date_formatted,
}
- unmanaged_domains_sliced_at_end_date = get_sliced_domains(filter_unmanaged_domains_end_date, True)
+ unmanaged_domains_sliced_at_end_date = get_sliced_domains(filter_unmanaged_domains_end_date)
writer.writerow(["UNMANAGED DOMAINS AT END DATE"])
writer.writerow(
@@ -761,7 +776,7 @@ def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
columns,
sort_fields,
filter_unmanaged_domains_end_date,
- get_domain_managers=False,
+ should_get_domain_managers=False,
should_write_header=True,
)
diff --git a/src/registrar/views/admin_views.py b/src/registrar/views/admin_views.py
index eba8423ed..01a8157f9 100644
--- a/src/registrar/views/admin_views.py
+++ b/src/registrar/views/admin_views.py
@@ -49,8 +49,8 @@ class AnalyticsView(View):
"domain__permissions__isnull": False,
"domain__first_ready__lte": end_date_formatted,
}
- managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date, True)
- managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date, True)
+ managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date)
+ managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date)
filter_unmanaged_domains_start_date = {
"domain__permissions__isnull": True,
@@ -60,10 +60,8 @@ class AnalyticsView(View):
"domain__permissions__isnull": True,
"domain__first_ready__lte": end_date_formatted,
}
- unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(
- filter_unmanaged_domains_start_date, True
- )
- unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date, True)
+ unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_unmanaged_domains_start_date)
+ unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date)
filter_ready_domains_start_date = {
"domain__state__in": [models.Domain.State.READY],
From 77d1158c1b34b8c40bd139009bb5c7784fad4149 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Tue, 2 Apr 2024 21:49:42 -0400
Subject: [PATCH 43/80] Clean up
---
src/registrar/tests/test_reports.py | 20 ++---
src/registrar/utility/csv_export.py | 123 ++++++++++++++--------------
2 files changed, 71 insertions(+), 72 deletions(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index b4861560f..b34f3d920 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -9,10 +9,10 @@ from registrar.utility.csv_export import (
export_data_unmanaged_domains_to_csv,
get_sliced_domains,
get_sliced_requests,
- write_domains_csv,
+ write_csv_for_domains,
get_default_start_date,
get_default_end_date,
- write_requests_csv,
+ write_csv_for_requests,
)
from django.core.management import call_command
@@ -242,7 +242,7 @@ class ExportDataTest(MockDb, MockEppLib):
}
self.maxDiff = None
# Call the export functions
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields,
@@ -273,7 +273,7 @@ class ExportDataTest(MockDb, MockEppLib):
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
- def test_write_domains_csv(self):
+ def test_write_csv_for_domains(self):
"""Test that write_body returns the
existing domain, test that sort by domain name works,
test that filter works"""
@@ -309,7 +309,7 @@ class ExportDataTest(MockDb, MockEppLib):
],
}
# Call the export functions
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields,
@@ -367,7 +367,7 @@ class ExportDataTest(MockDb, MockEppLib):
],
}
# Call the export functions
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields,
@@ -448,7 +448,7 @@ class ExportDataTest(MockDb, MockEppLib):
}
# Call the export functions
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields,
@@ -456,7 +456,7 @@ class ExportDataTest(MockDb, MockEppLib):
should_get_domain_managers=False,
should_write_header=True,
)
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields_for_deleted_domains,
@@ -528,7 +528,7 @@ class ExportDataTest(MockDb, MockEppLib):
}
self.maxDiff = None
# Call the export functions
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields,
@@ -673,7 +673,7 @@ class ExportDataTest(MockDb, MockEppLib):
"submission_date__lte": self.end_date,
"submission_date__gte": self.start_date,
}
- write_requests_csv(writer, columns, sort_fields, filter_condition, should_write_header=True)
+ write_csv_for_requests(writer, columns, sort_fields, filter_condition, should_write_header=True)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index faad25b28..7605d5bfe 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -12,7 +12,6 @@ from django.db.models.functions import Concat, Coalesce
from registrar.models.public_contact import PublicContact
from registrar.models.user_domain_role import UserDomainRole
-from registrar.models.utility.generic_helper import Timer
from registrar.utility.enums import DefaultEmail
logger = logging.getLogger(__name__)
@@ -54,7 +53,7 @@ def get_domain_infos(filter_condition, sort_fields):
return domain_infos_cleaned
-def parse_domain_row(
+def parse_row_for_domain(
columns,
domain_info: DomainInformation,
dict_security_emails_dict=None,
@@ -231,7 +230,8 @@ def build_dictionaries_for_domain_managers(dict_user_domain_roles, dict_domain_i
return dict_user_domain_roles, dict_domain_invitations_with_invited_status
-def write_domains_csv(
+
+def write_csv_for_domains(
writer,
columns,
sort_fields,
@@ -246,59 +246,58 @@ def write_domains_csv(
should_write_header: Conditional bc export_data_domain_growth_to_csv calls write_body twice
"""
- with Timer():
- # Retrieve domain information and all sec emails
- all_domain_infos = get_domain_infos(filter_condition, sort_fields)
- sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
- dict_security_emails_dict = _get_security_emails(sec_contact_ids)
- paginator = Paginator(all_domain_infos, 1000)
+ # Retrieve domain information and all sec emails
+ all_domain_infos = get_domain_infos(filter_condition, sort_fields)
+ sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
+ dict_security_emails_dict = _get_security_emails(sec_contact_ids)
+ paginator = Paginator(all_domain_infos, 1000)
- # Initialize variables
- dms_total = 0
- should_update_columns = False
- total_body_rows = []
- dict_user_domain_roles = {}
- dict_domain_invitations_with_invited_status = {}
+ # Initialize variables
+ dms_total = 0
+ should_update_columns = False
+ total_body_rows = []
+ dict_user_domain_roles = {}
+ dict_domain_invitations_with_invited_status = {}
- # Build dictionaries if necessary
- if should_get_domain_managers:
- dict_user_domain_roles, dict_domain_invitations_with_invited_status = build_dictionaries_for_domain_managers(
- dict_user_domain_roles, dict_domain_invitations_with_invited_status
- )
+ # Build dictionaries if necessary
+ if should_get_domain_managers:
+ dict_user_domain_roles, dict_domain_invitations_with_invited_status = build_dictionaries_for_domain_managers(
+ dict_user_domain_roles, dict_domain_invitations_with_invited_status
+ )
- # Process domain information
- for page_num in paginator.page_range:
- rows = []
- page = paginator.page(page_num)
- for domain_info in page.object_list:
- if should_get_domain_managers:
- columns, dms_total, should_update_columns = update_columns_with_domain_managers(
- columns,
- domain_info,
- should_update_columns,
- dms_total,
- dict_domain_invitations_with_invited_status,
- dict_user_domain_roles,
- )
+ # Process domain information
+ for page_num in paginator.page_range:
+ rows = []
+ page = paginator.page(page_num)
+ for domain_info in page.object_list:
+ if should_get_domain_managers:
+ columns, dms_total, should_update_columns = update_columns_with_domain_managers(
+ columns,
+ domain_info,
+ should_update_columns,
+ dms_total,
+ dict_domain_invitations_with_invited_status,
+ dict_user_domain_roles,
+ )
- try:
- row = parse_domain_row(
- columns,
- domain_info,
- dict_security_emails_dict,
- should_get_domain_managers,
- dict_domain_invitations_with_invited_status,
- dict_user_domain_roles,
- )
- rows.append(row)
- except ValueError:
- logger.error("csv_export -> Error when parsing row, domain was None")
- continue
- total_body_rows.extend(rows)
+ try:
+ row = parse_row_for_domain(
+ columns,
+ domain_info,
+ dict_security_emails_dict,
+ should_get_domain_managers,
+ dict_domain_invitations_with_invited_status,
+ dict_user_domain_roles,
+ )
+ rows.append(row)
+ except ValueError:
+ logger.error("csv_export -> Error when parsing row, domain was None")
+ continue
+ total_body_rows.extend(rows)
- if should_write_header:
- write_header(writer, columns)
- writer.writerows(total_body_rows)
+ if should_write_header:
+ write_header(writer, columns)
+ writer.writerows(total_body_rows)
def get_requests(filter_condition, sort_fields):
@@ -312,7 +311,7 @@ def get_requests(filter_condition, sort_fields):
return requests
-def parse_request_row(columns, request: DomainRequest):
+def parse_row_for_requests(columns, request: DomainRequest):
"""Given a set of columns, generate a new row from cleaned column data"""
requested_domain_name = "No requested domain"
@@ -344,7 +343,7 @@ def parse_request_row(columns, request: DomainRequest):
return row
-def write_requests_csv(
+def write_csv_for_requests(
writer,
columns,
sort_fields,
@@ -365,7 +364,7 @@ def write_requests_csv(
rows = []
for request in page.object_list:
try:
- row = parse_request_row(columns, request)
+ row = parse_row_for_requests(columns, request)
rows.append(row)
except ValueError:
# This should not happen. If it does, just skip this row.
@@ -413,7 +412,7 @@ def export_data_type_to_csv(csv_file):
Domain.State.ON_HOLD,
],
}
- write_domains_csv(
+ write_csv_for_domains(
writer, columns, sort_fields, filter_condition, should_get_domain_managers=True, should_write_header=True
)
@@ -446,7 +445,7 @@ def export_data_full_to_csv(csv_file):
Domain.State.ON_HOLD,
],
}
- write_domains_csv(
+ write_csv_for_domains(
writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
)
@@ -480,7 +479,7 @@ def export_data_federal_to_csv(csv_file):
Domain.State.ON_HOLD,
],
}
- write_domains_csv(
+ write_csv_for_domains(
writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
)
@@ -550,10 +549,10 @@ def export_data_domain_growth_to_csv(csv_file, start_date, end_date):
"domain__deleted__gte": start_date_formatted,
}
- write_domains_csv(
+ write_csv_for_domains(
writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
)
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields_for_deleted_domains,
@@ -698,7 +697,7 @@ def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
writer.writerow(managed_domains_sliced_at_end_date)
writer.writerow([])
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields,
@@ -771,7 +770,7 @@ def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
writer.writerow(unmanaged_domains_sliced_at_end_date)
writer.writerow([])
- write_domains_csv(
+ write_csv_for_domains(
writer,
columns,
sort_fields,
@@ -807,4 +806,4 @@ def export_data_requests_growth_to_csv(csv_file, start_date, end_date):
"submission_date__gte": start_date_formatted,
}
- write_requests_csv(writer, columns, sort_fields, filter_condition, should_write_header=True)
+ write_csv_for_requests(writer, columns, sort_fields, filter_condition, should_write_header=True)
From 9d8d90e45a1f6bf418c9c35cac268601f5b130cf Mon Sep 17 00:00:00 2001
From: Rachid Mrad <107004823+rachidatecs@users.noreply.github.com>
Date: Wed, 3 Apr 2024 11:26:23 -0400
Subject: [PATCH 44/80] Update src/registrar/utility/csv_export.py
Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
---
src/registrar/utility/csv_export.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 7605d5bfe..01eef295a 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -204,7 +204,6 @@ def build_dictionaries_for_domain_managers(dict_user_domain_roles, dict_domain_i
"""Helper function that builds dicts for invited users and active domain
managers. We do so to avoid filtering within loops."""
- user_domain_roles = None
user_domain_roles = UserDomainRole.objects.all()
# Iterate through each user domain role and populate the dictionary
From 602f07f2194629a3fa2130d1d2c5b7224587eb26 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Wed, 3 Apr 2024 11:31:46 -0400
Subject: [PATCH 45/80] var name correction
---
src/registrar/utility/csv_export.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 01eef295a..949b0adcd 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -56,7 +56,7 @@ def get_domain_infos(filter_condition, sort_fields):
def parse_row_for_domain(
columns,
domain_info: DomainInformation,
- dict_security_emails_dict=None,
+ dict_security_emails=None,
should_get_domain_managers=False,
dict_domain_invitations_with_invited_status=None,
dict_user_domain_roles=None,
@@ -72,8 +72,8 @@ def parse_row_for_domain(
# Grab the security email from a preset dictionary.
# If nothing exists in the dictionary, grab from .contacts.
- if dict_security_emails_dict is not None and domain.name in dict_security_emails_dict:
- _email = dict_security_emails_dict.get(domain.name)
+ if dict_security_emails is not None and domain.name in dict_security_emails:
+ _email = dict_security_emails.get(domain.name)
security_email = _email if _email is not None else " "
else:
# If the dictionary doesn't contain that data, lets filter for it manually.
@@ -135,7 +135,7 @@ def _get_security_emails(sec_contact_ids):
"""
Retrieve security contact emails for the given security contact IDs.
"""
- dict_security_emails_dict = {}
+ dict_security_emails = {}
public_contacts = (
PublicContact.objects.only("email", "domain__name")
.select_related("domain")
@@ -145,12 +145,12 @@ def _get_security_emails(sec_contact_ids):
# Populate a dictionary of domain names and their security contacts
for contact in public_contacts:
domain: Domain = contact.domain
- if domain is not None and domain.name not in dict_security_emails_dict:
- dict_security_emails_dict[domain.name] = contact.email
+ if domain is not None and domain.name not in dict_security_emails:
+ dict_security_emails[domain.name] = contact.email
else:
logger.warning("csv_export -> Domain was none for PublicContact")
- return dict_security_emails_dict
+ return dict_security_emails
def count_domain_managers(domain_name, dict_domain_invitations_with_invited_status, dict_user_domain_roles):
@@ -248,7 +248,7 @@ def write_csv_for_domains(
# Retrieve domain information and all sec emails
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
- dict_security_emails_dict = _get_security_emails(sec_contact_ids)
+ dict_security_emails = _get_security_emails(sec_contact_ids)
paginator = Paginator(all_domain_infos, 1000)
# Initialize variables
@@ -283,7 +283,7 @@ def write_csv_for_domains(
row = parse_row_for_domain(
columns,
domain_info,
- dict_security_emails_dict,
+ dict_security_emails,
should_get_domain_managers,
dict_domain_invitations_with_invited_status,
dict_user_domain_roles,
From 220560dcedbcf60d4954eae433c1da6152220806 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Wed, 3 Apr 2024 16:27:16 -0400
Subject: [PATCH 46/80] Change sort settings and unit test
---
src/registrar/admin.py | 4 +-
src/registrar/tests/test_admin.py | 74 ++++++++++++++++++++++++++++---
2 files changed, 71 insertions(+), 7 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index e0c98b7c2..9673f7df4 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1056,6 +1056,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
# Columns
list_display = [
"requested_domain",
+ "submission_date",
"status",
"generic_org_type",
"federal_type",
@@ -1064,7 +1065,6 @@ class DomainRequestAdmin(ListHeaderAdmin):
"custom_election_board",
"city",
"state_territory",
- "submission_date",
"submitter",
"investigator",
]
@@ -1192,7 +1192,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
# Table ordering
- ordering = ["requested_domain__name"]
+ ordering = ["-submission_date", "requested_domain__name"]
change_form_template = "django/admin/domain_request_change_form.html"
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 368f30721..8bdd45701 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1,4 +1,6 @@
-from datetime import date
+from datetime import date, datetime
+from django.utils import timezone
+import re
from django.test import TestCase, RequestFactory, Client, override_settings
from django.contrib.admin.sites import AdminSite
from contextlib import ExitStack
@@ -716,7 +718,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",))
def test_submitter_sortable(self):
- """Tests if the DomainRequest sorts by domain correctly"""
+ """Tests if the DomainRequest sorts by submitter correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@@ -747,7 +749,7 @@ class TestDomainRequestAdmin(MockEppLib):
)
def test_investigator_sortable(self):
- """Tests if the DomainRequest sorts by domain correctly"""
+ """Tests if the DomainRequest sorts by investigator correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@@ -760,7 +762,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Assert that our sort works correctly
self.test_helper.assert_table_sorted(
- "6",
+ "12",
(
"investigator__first_name",
"investigator__last_name",
@@ -769,13 +771,75 @@ class TestDomainRequestAdmin(MockEppLib):
# Assert that sorting in reverse works correctly
self.test_helper.assert_table_sorted(
- "-6",
+ "-12",
(
"-investigator__first_name",
"-investigator__last_name",
),
)
+ @less_console_noise_decorator
+ def test_default_sorting_in_domain_requests_list(self):
+ """
+ Make sure the default sortin in on the domain requests list page is reverse submission_date
+ then alphabetical requested_domain
+ """
+
+ # Create domain requests with different names
+ domain_requests = [
+ completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, name=name)
+ for name in ["ccc.gov", "bbb.gov", "eee.gov", "aaa.gov", "zzz.gov", "ddd.gov"]
+ ]
+
+ domain_requests[0].submission_date = timezone.make_aware(datetime(2024, 10, 16))
+ domain_requests[1].submission_date = timezone.make_aware(datetime(2001, 10, 16))
+ domain_requests[2].submission_date = timezone.make_aware(datetime(1980, 10, 16))
+ domain_requests[3].submission_date = timezone.make_aware(datetime(1998, 10, 16))
+ domain_requests[4].submission_date = timezone.make_aware(datetime(2013, 10, 16))
+ domain_requests[5].submission_date = timezone.make_aware(datetime(1980, 10, 16))
+
+ # Save the modified domain requests to update their attributes in the database
+ for domain_request in domain_requests:
+ domain_request.save()
+
+ # Refresh domain request objects from the database to reflect the changes
+ domain_requests = [DomainRequest.objects.get(pk=domain_request.pk) for domain_request in domain_requests]
+
+ # Login as superuser and retrieve the domain request list page
+ self.client.force_login(self.superuser)
+ response = self.client.get("/admin/registrar/domainrequest/")
+
+ # Check that the response is successful
+ self.assertEqual(response.status_code, 200)
+
+ # Extract the domain names from the response content using regex
+ domain_names_match = re.findall(r"(\w+\.gov)", response.content.decode("utf-8"))
+
+ logger.info(f"domain_names_match {domain_names_match}")
+
+ # Verify that domain names are found
+ self.assertTrue(domain_names_match)
+
+ # Extract the domain names
+ domain_names = [match for match in domain_names_match]
+
+ # Verify that the domain names are displayed in the expected order
+ expected_order = [
+ "ccc.gov",
+ "ccc.gov",
+ "zzz.gov",
+ "zzz.gov",
+ "bbb.gov",
+ "bbb.gov",
+ "aaa.gov",
+ "aaa.gov",
+ "ddd.gov",
+ "ddd.gov",
+ "eee.gov",
+ "eee.gov",
+ ]
+ self.assertEqual(domain_names, expected_order)
+
def test_short_org_name_in_domain_requests_list(self):
"""
Make sure the short name is displaying in admin on the list page
From d0d312598da04b347ef45af6d240b7d304078be6 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 3 Apr 2024 14:49:38 -0700
Subject: [PATCH 47/80] Edit typo on create_groups migration
---
src/registrar/migrations/0037_create_groups_v01.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/migrations/0037_create_groups_v01.py b/src/registrar/migrations/0037_create_groups_v01.py
index 3540ea2f3..0c04a8b61 100644
--- a/src/registrar/migrations/0037_create_groups_v01.py
+++ b/src/registrar/migrations/0037_create_groups_v01.py
@@ -1,5 +1,5 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
-# It is dependent on 0035 (which populates ContentType and Permissions)
+# It is dependent on 0036 (which populates ContentType and Permissions)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
From c25a082aa091caa325cbec2ba326daaa8e59ca77 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 3 Apr 2024 15:18:50 -0700
Subject: [PATCH 48/80] Add instructions for user group migrations
---
src/registrar/models/user_group.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py
index 2aa2f642e..6211094ec 100644
--- a/src/registrar/models/user_group.py
+++ b/src/registrar/models/user_group.py
@@ -5,6 +5,16 @@ logger = logging.getLogger(__name__)
class UserGroup(Group):
+ """
+ UserGroup sets read and write permissions for superusers (who have full access)
+ and analysts. To update analyst permissions do the following:
+ 1. Make desired changes to analyst group permissions in user_group.py.
+ 2. Follow the steps in 0037_create_groups_v01.py to create a duplicate
+ migration for the updated user group permissions.
+ 3. To migrate locally, run docker-compose up. To migrate on a sandbox,
+ push the new migration onto your sandbox before migrating.
+ """
+
class Meta:
verbose_name = "User group"
verbose_name_plural = "User groups"
@@ -49,7 +59,7 @@ class UserGroup(Group):
{
"app_label": "registrar",
"model": "user",
- "permissions": ["analyst_access_permission", "change_user"],
+ "permissions": ["analyst_access_permission", "change_user", "delete_user"],
},
{
"app_label": "registrar",
From c4cf7d5669e156fb755d2034c8e42c3af49f727b Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 3 Apr 2024 15:27:49 -0700
Subject: [PATCH 49/80] Update dependency typos in user group migrations
---
src/registrar/migrations/0038_create_groups_v02.py | 2 +-
src/registrar/migrations/0042_create_groups_v03.py | 2 +-
src/registrar/migrations/0044_create_groups_v04.py | 2 +-
src/registrar/migrations/0053_create_groups_v05.py | 2 +-
src/registrar/migrations/0065_create_groups_v06.py | 2 +-
src/registrar/migrations/0067_create_groups_v07.py | 2 +-
src/registrar/migrations/0075_create_groups_v08.py | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/registrar/migrations/0038_create_groups_v02.py b/src/registrar/migrations/0038_create_groups_v02.py
index fc61db3c0..70d13b61a 100644
--- a/src/registrar/migrations/0038_create_groups_v02.py
+++ b/src/registrar/migrations/0038_create_groups_v02.py
@@ -1,5 +1,5 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
-# It is dependent on 0035 (which populates ContentType and Permissions)
+# It is dependent on 0037 (which also updates user role permissions)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
diff --git a/src/registrar/migrations/0042_create_groups_v03.py b/src/registrar/migrations/0042_create_groups_v03.py
index 01b7985bf..e30841599 100644
--- a/src/registrar/migrations/0042_create_groups_v03.py
+++ b/src/registrar/migrations/0042_create_groups_v03.py
@@ -1,5 +1,5 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
-# It is dependent on 0035 (which populates ContentType and Permissions)
+# It is dependent on 0041 (which changes fields in domain request and domain information)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
diff --git a/src/registrar/migrations/0044_create_groups_v04.py b/src/registrar/migrations/0044_create_groups_v04.py
index ecb48e335..63cad49bb 100644
--- a/src/registrar/migrations/0044_create_groups_v04.py
+++ b/src/registrar/migrations/0044_create_groups_v04.py
@@ -1,5 +1,5 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
-# It is dependent on 0035 (which populates ContentType and Permissions)
+# It is dependent on 0043 (which adds an expiry date field to a domain.)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
diff --git a/src/registrar/migrations/0053_create_groups_v05.py b/src/registrar/migrations/0053_create_groups_v05.py
index aaf74a9db..91e8389df 100644
--- a/src/registrar/migrations/0053_create_groups_v05.py
+++ b/src/registrar/migrations/0053_create_groups_v05.py
@@ -1,5 +1,5 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
-# It is dependent on 0035 (which populates ContentType and Permissions)
+# It is dependent on 0052 (which alters fields in a domain request)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
diff --git a/src/registrar/migrations/0065_create_groups_v06.py b/src/registrar/migrations/0065_create_groups_v06.py
index d2cb32cee..965dc06a8 100644
--- a/src/registrar/migrations/0065_create_groups_v06.py
+++ b/src/registrar/migrations/0065_create_groups_v06.py
@@ -1,5 +1,5 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
-# It is dependent on 0035 (which populates ContentType and Permissions)
+# It is dependent on 0065 (which renames fields in domain application)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
diff --git a/src/registrar/migrations/0067_create_groups_v07.py b/src/registrar/migrations/0067_create_groups_v07.py
index 85138d4af..809738ba3 100644
--- a/src/registrar/migrations/0067_create_groups_v07.py
+++ b/src/registrar/migrations/0067_create_groups_v07.py
@@ -1,5 +1,5 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
-# It is dependent on 0035 (which populates ContentType and Permissions)
+# It is dependent on 0066 (which updates users with permission as Verified by Staff)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
diff --git a/src/registrar/migrations/0075_create_groups_v08.py b/src/registrar/migrations/0075_create_groups_v08.py
index b0b2ed740..a4df52d21 100644
--- a/src/registrar/migrations/0075_create_groups_v08.py
+++ b/src/registrar/migrations/0075_create_groups_v08.py
@@ -1,5 +1,5 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
-# It is dependent on 0035 (which populates ContentType and Permissions)
+# It is dependent on 0074 (which renames Domain Application and its fields)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
From 8e5c1aadbf841559ea6ae17f11399139a07fea72 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 3 Apr 2024 15:29:16 -0700
Subject: [PATCH 50/80] Revert user group permission changes from testing
---
src/registrar/models/user_group.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py
index 6211094ec..3071fba11 100644
--- a/src/registrar/models/user_group.py
+++ b/src/registrar/models/user_group.py
@@ -59,7 +59,7 @@ class UserGroup(Group):
{
"app_label": "registrar",
"model": "user",
- "permissions": ["analyst_access_permission", "change_user", "delete_user"],
+ "permissions": ["analyst_access_permission", "change_user"],
},
{
"app_label": "registrar",
From 34232f9b6c8199e5d5136a19972ac1794ddad988 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 4 Apr 2024 10:48:33 -0600
Subject: [PATCH 51/80] Change stlying
---
src/registrar/assets/js/get-gov-admin.js | 1 +
src/registrar/assets/sass/_theme/_admin.scss | 31 ++++++++++++++
.../assets/sass/_theme/_tooltips.scss | 6 +--
.../templates/admin/input_with_clipboard.html | 6 ++-
.../admin/includes/contact_detail_list.html | 41 ++++++++++++++-----
.../admin/includes/detail_table_fieldset.html | 27 +++++++-----
src/registrar/templates/domain_users.html | 2 +-
src/registrar/templates/home.html | 2 +-
8 files changed, 89 insertions(+), 27 deletions(-)
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index 4ae0af4a5..581c2b899 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -155,6 +155,7 @@ function openInNewTab(el, removeAttribute = false){
navigator.clipboard.writeText(input.value).then(function() {
// Change the icon to a checkmark on successful copy
let buttonIcon = button .querySelector('.usa-button__clipboard use');
+ console.log(`what is the button icon ${buttonIcon}`)
if (buttonIcon) {
let currentHref = buttonIcon.getAttribute('xlink:href');
let baseHref = currentHref.split('#')[0];
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index c636aab5c..8b5870b86 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -556,8 +556,39 @@ address.dja-address-contact-list {
min-height: 2.25rem !important;
}
+ button {
+ line-height: 15px;
+ }
+
+}
+
+.admin-icon-group.admin-icon-group__clipboard-link {
+ position: relative;
+ display: inline;
+ align-items: center;
+
+ .usa-button__icon {
+ position: absolute;
+ right: auto;
+ left: 4px;
+ height: 100%;
+ }
+ button {
+ font-size: unset !important;
+ display: inline-flex;
+ padding-top: 4px;
+ line-height: 14px;
+ }
}
.no-outline-on-click:focus {
outline: none !important;
+}
+
+svg.no-pointer-events {
+ use {
+ // USWDS has weird interactions with SVGs regarding tooltips,
+ // and other components. In this event, we need to disable pointer interactions.
+ pointer-events: none;
+ }
}
\ No newline at end of file
diff --git a/src/registrar/assets/sass/_theme/_tooltips.scss b/src/registrar/assets/sass/_theme/_tooltips.scss
index 01348e1b1..04c6f3cda 100644
--- a/src/registrar/assets/sass/_theme/_tooltips.scss
+++ b/src/registrar/assets/sass/_theme/_tooltips.scss
@@ -2,7 +2,7 @@
// Only apply this custom wrapping to desktop
@include at-media(desktop) {
- .usa-tooltip__body {
+ .usa-tooltip--registrar .usa-tooltip__body {
width: 350px;
white-space: normal;
text-align: center;
@@ -10,7 +10,7 @@
}
@include at-media(tablet) {
- .usa-tooltip__body {
+ .usa-tooltip--registrar .usa-tooltip__body {
width: 250px !important;
white-space: normal !important;
text-align: center !important;
@@ -18,7 +18,7 @@
}
@include at-media(mobile) {
- .usa-tooltip__body {
+ .usa-tooltip--registrar .usa-tooltip__body {
width: 250px !important;
white-space: normal !important;
text-align: center !important;
diff --git a/src/registrar/templates/admin/input_with_clipboard.html b/src/registrar/templates/admin/input_with_clipboard.html
index c2de55bf1..76f76b63f 100644
--- a/src/registrar/templates/admin/input_with_clipboard.html
+++ b/src/registrar/templates/admin/input_with_clipboard.html
@@ -10,10 +10,14 @@ Template for an input field with a clipboard
class="usa-button usa-button--unstyled padding-right-1 usa-button__icon usa-button__clipboard"
type="button"
>
+
+
\ No newline at end of file
diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html
index cded7526b..48a980fbc 100644
--- a/src/registrar/templates/django/admin/includes/contact_detail_list.html
+++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html
@@ -25,18 +25,37 @@
{# Email #}
{% if user.email or user.contact.email %}
{% if user.contact.email %}
-
{% else %}
diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
index 53bdbe821..459ffe438 100644
--- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
+++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
@@ -84,7 +84,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)