From c20f124a2ed1dca3ae115c43e197418c741aee7c Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 2 Apr 2024 12:01:07 -0600
Subject: [PATCH 01/71] Basic script
---
.../commands/populate_organization_type.py | 123 ++++++++++++++++++
.../commands/utility/terminal_helper.py | 11 +-
2 files changed, 130 insertions(+), 4 deletions(-)
create mode 100644 src/registrar/management/commands/populate_organization_type.py
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
new file mode 100644
index 000000000..a6d32358e
--- /dev/null
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -0,0 +1,123 @@
+import argparse
+import logging
+from typing import List
+from django.core.management import BaseCommand
+from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper, ScriptDataHelper
+from registrar.models import DomainInformation, DomainRequest, Domain
+
+logger = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ help = "Loops through each valid DomainInformation and DomainRequest object and updates its organization_type value"
+
+ def __init__(self):
+ super().__init__()
+ # Get lists for DomainRequest
+ self.request_to_update: List[DomainRequest] = []
+ self.request_failed_to_update: List[DomainRequest] = []
+ self.request_skipped: List[DomainRequest] = []
+
+ # Get lists for DomainInformation
+ self.di_to_update: List[DomainInformation] = []
+ self.di_failed_to_update: List[DomainInformation] = []
+ self.di_skipped: List[DomainInformation] = []
+
+ def add_arguments(self, parser):
+ """Adds command line arguments"""
+ parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
+ parser.add_argument(
+ "election_office_filename",
+ help=("A JSON file that holds the location and filenames" "of all the data files used for migrations"),
+ )
+
+ def handle(self, **kwargs):
+ """Loops through each valid Domain object and updates its first_created value"""
+ debug = kwargs.get("debug")
+ domain_requests = DomainRequest.objects.filter(organization_type__isnull=True)
+
+ # Code execution will stop here if the user prompts "N"
+ TerminalHelper.prompt_for_execution(
+ system_exit_on_terminate=True,
+ info_to_inspect=f"""
+ ==Proposed Changes==
+ Number of DomainRequest objects to change: {len(domain_requests)}
+
+ Organization_type data will be added for all of these fields.
+ """,
+ prompt_title="Do you wish to process DomainRequest?",
+ )
+ logger.info("Updating DomainRequest(s)...")
+
+ self.update_domain_requests(domain_requests, debug)
+
+ domain_infos = DomainInformation.objects.filter(domain_request__isnull=False, organization_type__isnull=True)
+ # Code execution will stop here if the user prompts "N"
+ TerminalHelper.prompt_for_execution(
+ system_exit_on_terminate=True,
+ info_to_inspect=f"""
+ ==Proposed Changes==
+ Number of DomainInformation objects to change: {len(domain_infos)}
+
+ Organization_type data will be added for all of these fields.
+ """,
+ prompt_title="Do you wish to process DomainInformation?",
+ )
+ logger.info("Updating DomainInformation(s)...")
+
+ self.update_domain_informations(domain_infos, debug)
+
+ def update_domain_requests(self, domain_requests, debug):
+ for request in domain_requests:
+ try:
+ # TODO - parse data from hfile ere
+ if request.generic_org_type is not None:
+ request.is_election_board = True
+ self.request_to_update.append(request)
+ if debug:
+ logger.info(f"Updating {request}")
+ else:
+ self.request_skipped.append(request)
+ if debug:
+ logger.warning(f"Skipped updating {request}")
+ except Exception as err:
+ self.request_failed_to_update.append(request)
+ logger.error(err)
+ logger.error(f"{TerminalColors.FAIL}" f"Failed to update {request}" f"{TerminalColors.ENDC}")
+
+ # Do a bulk update on the organization_type field
+ ScriptDataHelper.bulk_update_fields(DomainRequest, self.request_to_update, ["organization_type"])
+
+ # Log what happened
+ log_header = "============= FINISHED UPDATE FOR DOMAINREQUEST ==============="
+ TerminalHelper.log_script_run_summary(
+ self.request_to_update, self.request_failed_to_update, self.request_skipped, debug, log_header
+ )
+
+ def update_domain_informations(self, domain_informations, debug):
+ for info in domain_informations:
+ try:
+ # TODO - parse data from hfile ere
+ if info.generic_org_type is not None:
+ info.is_election_board = True
+ self.di_to_update.append(info)
+ if debug:
+ logger.info(f"Updating {info}")
+ else:
+ self.di_skipped.append(info)
+ if debug:
+ logger.warning(f"Skipped updating {info}")
+ except Exception as err:
+ self.di_failed_to_update.append(info)
+ logger.error(err)
+ logger.error(f"{TerminalColors.FAIL}" f"Failed to update {info}" f"{TerminalColors.ENDC}")
+
+ # Do a bulk update on the organization_type field
+ ScriptDataHelper.bulk_update_fields(DomainInformation, self.di_to_update, ["organization_type"])
+
+ # Log what happened
+ log_header = "============= FINISHED UPDATE FOR DOMAININFORMATION ==============="
+ TerminalHelper.log_script_run_summary(
+ self.di_to_update, self.di_failed_to_update, self.di_skipped, debug, log_header
+ )
+
diff --git a/src/registrar/management/commands/utility/terminal_helper.py b/src/registrar/management/commands/utility/terminal_helper.py
index 49ab89b9a..2149f429a 100644
--- a/src/registrar/management/commands/utility/terminal_helper.py
+++ b/src/registrar/management/commands/utility/terminal_helper.py
@@ -59,13 +59,16 @@ class ScriptDataHelper:
class TerminalHelper:
@staticmethod
- def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool):
+ def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool, log_header=None):
"""Prints success, failed, and skipped counts, as well as
all affected objects."""
update_success_count = len(to_update)
update_failed_count = len(failed_to_update)
update_skipped_count = len(skipped)
+ if log_header is None:
+ log_header = "============= FINISHED ==============="
+
# Prepare debug messages
debug_messages = {
"success": (f"{TerminalColors.OKCYAN}Updated: {to_update}{TerminalColors.ENDC}\n"),
@@ -85,7 +88,7 @@ class TerminalHelper:
if update_failed_count == 0 and update_skipped_count == 0:
logger.info(
f"""{TerminalColors.OKGREEN}
- ============= FINISHED ===============
+ {log_header}
Updated {update_success_count} entries
{TerminalColors.ENDC}
"""
@@ -93,7 +96,7 @@ class TerminalHelper:
elif update_failed_count == 0:
logger.warning(
f"""{TerminalColors.YELLOW}
- ============= FINISHED ===============
+ {log_header}
Updated {update_success_count} entries
----- SOME DATA WAS INVALID (NEEDS MANUAL PATCHING) -----
Skipped updating {update_skipped_count} entries
@@ -103,7 +106,7 @@ class TerminalHelper:
else:
logger.error(
f"""{TerminalColors.FAIL}
- ============= FINISHED ===============
+ {log_header}
Updated {update_success_count} entries
----- UPDATE FAILED -----
Failed to update {update_failed_count} entries,
From 3b0fa34ee22c017b44d3616214fe6a9ca1e8edfc Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 4 Apr 2024 14:37:45 -0600
Subject: [PATCH 02/71] Update script
---
src/registrar/admin.py | 2 +
.../commands/populate_organization_type.py | 106 +++++++++++-------
2 files changed, 69 insertions(+), 39 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index e4c71f8d5..2bcc604ff 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -883,6 +883,8 @@ class DomainInformationAdmin(ListHeaderAdmin):
"Type of organization",
{
"fields": [
+ "is_election_board",
+ "generic_org_type",
"organization_type",
]
},
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index a6d32358e..d79bf613f 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -1,10 +1,12 @@
import argparse
import logging
+import os
+from registrar.signals import create_or_update_organization_type
from typing import List
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper, ScriptDataHelper
from registrar.models import DomainInformation, DomainRequest, Domain
-
+from django.db import transaction
logger = logging.getLogger(__name__)
@@ -23,18 +25,37 @@ class Command(BaseCommand):
self.di_failed_to_update: List[DomainInformation] = []
self.di_skipped: List[DomainInformation] = []
+ # Define a global variable for all domains with election offices
+ self.domains_with_election_offices_set = set()
+
def add_arguments(self, parser):
"""Adds command line arguments"""
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
parser.add_argument(
- "election_office_filename",
+ "domain_election_office_filename",
help=("A JSON file that holds the location and filenames" "of all the data files used for migrations"),
)
- def handle(self, **kwargs):
+ def handle(self, domain_election_office_filename, **kwargs):
"""Loops through each valid Domain object and updates its first_created value"""
debug = kwargs.get("debug")
- domain_requests = DomainRequest.objects.filter(organization_type__isnull=True)
+
+ # Check if the provided file path is valid
+ if not os.path.isfile(domain_election_office_filename):
+ raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_office_filename}'")
+
+
+ with open(domain_election_office_filename, "r") as file:
+ for line in file:
+ # Remove any leading/trailing whitespace
+ domain = line.strip()
+ if domain not in self.domains_with_election_offices_set:
+ self.domains_with_election_offices_set.add(domain)
+
+ domain_requests = DomainRequest.objects.filter(
+ organization_type__isnull=True,
+ requested_domain__name__isnull=False
+ )
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
@@ -51,7 +72,9 @@ class Command(BaseCommand):
self.update_domain_requests(domain_requests, debug)
- domain_infos = DomainInformation.objects.filter(domain_request__isnull=False, organization_type__isnull=True)
+ # We should actually be targeting all fields with no value for organization type,
+ # but do have a value for generic_org_type. This is because there is data that we can infer.
+ domain_infos = DomainInformation.objects.filter(organization_type__isnull=True)
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
@@ -68,25 +91,28 @@ class Command(BaseCommand):
self.update_domain_informations(domain_infos, debug)
def update_domain_requests(self, domain_requests, debug):
- for request in domain_requests:
- try:
- # TODO - parse data from hfile ere
- if request.generic_org_type is not None:
- request.is_election_board = True
- self.request_to_update.append(request)
- if debug:
- logger.info(f"Updating {request}")
- else:
- self.request_skipped.append(request)
- if debug:
- logger.warning(f"Skipped updating {request}")
- except Exception as err:
- self.request_failed_to_update.append(request)
- logger.error(err)
- logger.error(f"{TerminalColors.FAIL}" f"Failed to update {request}" f"{TerminalColors.ENDC}")
+ with transaction.atomic():
+ for request in domain_requests:
+ try:
+ # TODO - parse data from hfile ere
+ if request.generic_org_type is not None:
+ domain_name = request.requested_domain.name
+ request.is_election_board = domain_name in self.domains_with_election_offices_set
+ request.save()
+ self.request_to_update.append(request)
+ if debug:
+ logger.info(f"Updated {request} => {request.organization_type}")
+ else:
+ self.request_skipped.append(request)
+ if debug:
+ logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
+ except Exception as err:
+ self.request_failed_to_update.append(request)
+ logger.error(err)
+ logger.error(f"{TerminalColors.FAIL}" f"Failed to update {request}" f"{TerminalColors.ENDC}")
# Do a bulk update on the organization_type field
- ScriptDataHelper.bulk_update_fields(DomainRequest, self.request_to_update, ["organization_type"])
+ # ScriptDataHelper.bulk_update_fields(DomainRequest, self.request_to_update, ["is_election_board"])
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAINREQUEST ==============="
@@ -95,25 +121,27 @@ class Command(BaseCommand):
)
def update_domain_informations(self, domain_informations, debug):
- for info in domain_informations:
- try:
- # TODO - parse data from hfile ere
- if info.generic_org_type is not None:
- info.is_election_board = True
- self.di_to_update.append(info)
- if debug:
- logger.info(f"Updating {info}")
- else:
- self.di_skipped.append(info)
- if debug:
- logger.warning(f"Skipped updating {info}")
- except Exception as err:
- self.di_failed_to_update.append(info)
- logger.error(err)
- logger.error(f"{TerminalColors.FAIL}" f"Failed to update {info}" f"{TerminalColors.ENDC}")
+ with transaction.atomic():
+ for info in domain_informations:
+ try:
+ if info.generic_org_type is not None:
+ domain_name = info.domain.name
+ info.is_election_board = domain_name in self.domains_with_election_offices_set
+ info.save()
+ self.di_to_update.append(info)
+ if debug:
+ logger.info(f"Updated {info} => {info.organization_type}")
+ else:
+ self.di_skipped.append(info)
+ if debug:
+ logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
+ except Exception as err:
+ self.di_failed_to_update.append(info)
+ logger.error(err)
+ logger.error(f"{TerminalColors.FAIL}" f"Failed to update {info}" f"{TerminalColors.ENDC}")
# Do a bulk update on the organization_type field
- ScriptDataHelper.bulk_update_fields(DomainInformation, self.di_to_update, ["organization_type"])
+ # ScriptDataHelper.bulk_update_fields(DomainInformation, self.di_to_update, ["organization_type", "is_election_board", "generic_org_type"])
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAININFORMATION ==============="
From 755c7a9a561b0dcdf89644f2764da3b82db4eae4 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 4 Apr 2024 14:55:34 -0600
Subject: [PATCH 03/71] Finish script
---
.../commands/populate_organization_type.py | 25 ++++++++++---------
src/registrar/signals.py | 15 ++++-------
2 files changed, 18 insertions(+), 22 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index d79bf613f..f10b0ae3c 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -7,6 +7,7 @@ from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper, ScriptDataHelper
from registrar.models import DomainInformation, DomainRequest, Domain
from django.db import transaction
+
logger = logging.getLogger(__name__)
@@ -44,7 +45,6 @@ class Command(BaseCommand):
if not os.path.isfile(domain_election_office_filename):
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_office_filename}'")
-
with open(domain_election_office_filename, "r") as file:
for line in file:
# Remove any leading/trailing whitespace
@@ -53,8 +53,7 @@ class Command(BaseCommand):
self.domains_with_election_offices_set.add(domain)
domain_requests = DomainRequest.objects.filter(
- organization_type__isnull=True,
- requested_domain__name__isnull=False
+ organization_type__isnull=True, requested_domain__name__isnull=False
)
# Code execution will stop here if the user prompts "N"
@@ -94,14 +93,13 @@ class Command(BaseCommand):
with transaction.atomic():
for request in domain_requests:
try:
- # TODO - parse data from hfile ere
if request.generic_org_type is not None:
domain_name = request.requested_domain.name
request.is_election_board = domain_name in self.domains_with_election_offices_set
- request.save()
+ request = create_or_update_organization_type(DomainRequest, request, return_instance=True)
self.request_to_update.append(request)
if debug:
- logger.info(f"Updated {request} => {request.organization_type}")
+ logger.info(f"Updating {request} => {request.organization_type}")
else:
self.request_skipped.append(request)
if debug:
@@ -112,14 +110,16 @@ class Command(BaseCommand):
logger.error(f"{TerminalColors.FAIL}" f"Failed to update {request}" f"{TerminalColors.ENDC}")
# Do a bulk update on the organization_type field
- # ScriptDataHelper.bulk_update_fields(DomainRequest, self.request_to_update, ["is_election_board"])
+ ScriptDataHelper.bulk_update_fields(
+ DomainRequest, self.request_to_update, ["organization_type", "is_election_board", "generic_org_type"]
+ )
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAINREQUEST ==============="
TerminalHelper.log_script_run_summary(
self.request_to_update, self.request_failed_to_update, self.request_skipped, debug, log_header
)
-
+
def update_domain_informations(self, domain_informations, debug):
with transaction.atomic():
for info in domain_informations:
@@ -127,10 +127,10 @@ class Command(BaseCommand):
if info.generic_org_type is not None:
domain_name = info.domain.name
info.is_election_board = domain_name in self.domains_with_election_offices_set
- info.save()
+ info = create_or_update_organization_type(DomainInformation, info, return_instance=True)
self.di_to_update.append(info)
if debug:
- logger.info(f"Updated {info} => {info.organization_type}")
+ logger.info(f"Updating {info} => {info.organization_type}")
else:
self.di_skipped.append(info)
if debug:
@@ -141,11 +141,12 @@ class Command(BaseCommand):
logger.error(f"{TerminalColors.FAIL}" f"Failed to update {info}" f"{TerminalColors.ENDC}")
# Do a bulk update on the organization_type field
- # ScriptDataHelper.bulk_update_fields(DomainInformation, self.di_to_update, ["organization_type", "is_election_board", "generic_org_type"])
+ ScriptDataHelper.bulk_update_fields(
+ DomainInformation, self.di_to_update, ["organization_type", "is_election_board", "generic_org_type"]
+ )
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAININFORMATION ==============="
TerminalHelper.log_script_run_summary(
self.di_to_update, self.di_failed_to_update, self.di_skipped, debug, log_header
)
-
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index ad287219d..2e1e9ea1b 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, instance, return_instance=False, **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
@@ -100,6 +92,9 @@ 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
)
+
+ if return_instance:
+ return instance
def _update_org_type_from_generic_org_and_election(instance, org_map):
From ebee04e5012de5685be148cdab42460be771081d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 5 Apr 2024 08:59:27 -0600
Subject: [PATCH 04/71] Update test_reports.py
---
src/registrar/tests/test_reports.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 6299349c5..5b2569d05 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -718,7 +718,7 @@ class HelperFunctions(MockDb):
}
# Test with distinct
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
- expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
+ expected_content = [3, 4, 1, 0, 0, 0, 0, 0, 0, 0]
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
# Test without distinct
From 17c231a22a12d83a448fea3bcb1de5fb6c99aa8d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 5 Apr 2024 10:30:55 -0600
Subject: [PATCH 05/71] Add tests
---
.../tests/data/fake_election_domains.csv | 1 +
.../tests/test_management_scripts.py | 223 +++++++++++++++++-
2 files changed, 223 insertions(+), 1 deletion(-)
create mode 100644 src/registrar/tests/data/fake_election_domains.csv
diff --git a/src/registrar/tests/data/fake_election_domains.csv b/src/registrar/tests/data/fake_election_domains.csv
new file mode 100644
index 000000000..4ec005bb1
--- /dev/null
+++ b/src/registrar/tests/data/fake_election_domains.csv
@@ -0,0 +1 @@
+manualtransmission.gov
\ No newline at end of file
diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py
index 34178e262..303c3dfd6 100644
--- a/src/registrar/tests/test_management_scripts.py
+++ b/src/registrar/tests/test_management_scripts.py
@@ -1,5 +1,6 @@
import copy
from datetime import date, datetime, time
+from unittest import skip
from django.utils import timezone
from django.test import TestCase
@@ -7,6 +8,9 @@ from django.test import TestCase
from registrar.models import (
User,
Domain,
+ DomainRequest,
+ Contact,
+ Website,
DomainInvitation,
TransitionDomain,
DomainInformation,
@@ -18,7 +22,224 @@ from django.core.management import call_command
from unittest.mock import patch, call
from epplibwrapper import commands, common
-from .common import MockEppLib, less_console_noise
+from .common import MockEppLib, less_console_noise, completed_domain_request
+from api.tests.common import less_console_noise_decorator
+
+class TestPopulateOrganizationType(MockEppLib):
+ """Tests for the populate_organization_type script"""
+
+ def setUp(self):
+ """Creates a fake domain object"""
+ super().setUp()
+
+ # Get the domain requests
+ self.domain_request_1 = completed_domain_request(
+ name="lasers.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
+ is_election_board=True,
+ )
+ self.domain_request_2 = completed_domain_request(
+ name="readysetgo.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ )
+ self.domain_request_3 = completed_domain_request(
+ name="manualtransmission.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
+ )
+ self.domain_request_4 = completed_domain_request(
+ name="saladandfries.gov",
+ generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
+ is_election_board=True,
+ )
+
+ # Approve all three requests
+ self.domain_request_1.approve()
+ self.domain_request_2.approve()
+ self.domain_request_3.approve()
+ self.domain_request_4.approve()
+
+ # Get the domains
+ self.domain_1 = Domain.objects.get(name="lasers.gov")
+ self.domain_2 = Domain.objects.get(name="readysetgo.gov")
+ self.domain_3 = Domain.objects.get(name="manualtransmission.gov")
+ self.domain_4 = Domain.objects.get(name="saladandfries.gov")
+
+ # Get the domain infos
+ self.domain_info_1 = DomainInformation.objects.get(domain=self.domain_1)
+ self.domain_info_2 = DomainInformation.objects.get(domain=self.domain_2)
+ self.domain_info_3 = DomainInformation.objects.get(domain=self.domain_3)
+ self.domain_info_4 = DomainInformation.objects.get(domain=self.domain_4)
+
+ def tearDown(self):
+ """Deletes all DB objects related to migrations"""
+ super().tearDown()
+
+ # Delete domains and related information
+ Domain.objects.all().delete()
+ DomainInformation.objects.all().delete()
+ DomainRequest.objects.all().delete()
+ User.objects.all().delete()
+ Contact.objects.all().delete()
+ Website.objects.all().delete()
+
+ @less_console_noise_decorator
+ def run_populate_organization_type(self):
+ """
+ This method executes the populate_organization_type command.
+
+ The 'call_command' function from Django's management framework is then used to
+ execute the populate_organization_type command with the specified arguments.
+ """
+ with patch(
+ "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
+ return_value=True,
+ ):
+ call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv", debug=True)
+
+ def assert_expected_org_values_on_request_and_info(
+ self,
+ domain_request: DomainRequest,
+ domain_info: DomainInformation,
+ expected_values: dict,
+ ):
+ """
+ This is a a helper function that ensures that:
+ 1. DomainRequest and DomainInformation (on given objects) are equivalent
+ 2. That generic_org_type, is_election_board, and organization_type are equal to passed in values
+ """
+
+ # Test domain request
+ self.assertEqual(domain_request.generic_org_type, expected_values['generic_org_type'])
+ self.assertEqual(domain_request.is_election_board, expected_values['is_election_board'])
+ self.assertEqual(domain_request.organization_type, expected_values['organization_type'])
+
+ # Test domain info
+ self.assertEqual(domain_info.generic_org_type, expected_values['generic_org_type'])
+ self.assertEqual(domain_info.is_election_board, expected_values['is_election_board'])
+ self.assertEqual(domain_info.organization_type, expected_values['organization_type'])
+
+ def test_request_and_info_city_not_in_csv(self):
+ """Tests what happens to a city domain that is not defined in the CSV"""
+ city_request = self.domain_request_2
+ city_info = self.domain_request_2
+
+ # Make sure that all data is correct before proceeding.
+ # Since the presave fixture is in effect, we should expect that
+ # is_election_board is equal to none, even though we tried to define it as "True"
+ expected_values = {
+ 'is_election_board': False,
+ 'generic_org_type': DomainRequest.OrganizationChoices.CITY,
+ 'organization_type': DomainRequest.OrgChoicesElectionOffice.CITY,
+ }
+ self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
+
+ # Run the populate script
+ try:
+ self.run_populate_organization_type()
+ except Exception as e:
+ self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
+
+ # All values should be the same
+ self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
+
+ def test_request_and_info_federal(self):
+ """Tests what happens to a federal domain with is_election_board=True (which should be reverted to None)"""
+ federal_request = self.domain_request_1
+ federal_info = self.domain_info_1
+
+ # Make sure that all data is correct before proceeding.
+ # Since the presave fixture is in effect, we should expect that
+ # is_election_board is equal to none, even though we tried to define it as "True"
+ # TODO - either add some logging if this happens or a ValueError in the original ticket.
+ # Or FSM? Probably FSM.
+ expected_values = {
+ 'is_election_board': None,
+ 'generic_org_type': DomainRequest.OrganizationChoices.FEDERAL,
+ 'organization_type': DomainRequest.OrgChoicesElectionOffice.FEDERAL,
+ }
+ self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
+
+ # Run the populate script
+ try:
+ self.run_populate_organization_type()
+ except Exception as e:
+ self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
+
+ # All values should be the same
+ self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
+
+ def test_request_and_info_tribal_add_election_office(self):
+ """
+ Tests if a tribal domain in the election csv changes organization_type to TRIBAL - ELECTION
+ for the domain request and the domain info
+ """
+
+ tribal_request = self.domain_request_3
+ tribal_info = self.domain_info_3
+
+ # Make sure that all data is correct before proceeding.
+ # Because the presave fixture is in place when creating this, we should expect that the
+ # organization_type variable is already pre-populated. We will test what happens when
+ # it is not in another test.
+
+ expected_values = {
+ 'is_election_board': False,
+ 'generic_org_type': DomainRequest.OrganizationChoices.TRIBAL,
+ 'organization_type': DomainRequest.OrgChoicesElectionOffice.TRIBAL
+ }
+ self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
+
+ # Run the populate script
+ try:
+ self.run_populate_organization_type()
+ except Exception as e:
+ self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
+
+ # Because we define this in the "csv", we expect that is election board will switch to True,
+ # and organization_type will now be tribal_election
+ expected_values["is_election_board"] = True
+ expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
+
+ self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
+
+ def test_request_and_info_tribal_remove_election_office(self):
+ """
+ Tests if a tribal domain in the election csv changes organization_type to TRIBAL
+ when it used to be TRIBAL - ELECTION
+ for the domain request and the domain info
+ """
+
+ tribal_election_request = self.domain_request_4
+ tribal_election_info = self.domain_info_4
+
+ # Make sure that all data is correct before proceeding.
+ # Because the presave fixture is in place when creating this, we should expect that the
+ # organization_type variable is already pre-populated. We will test what happens when
+ # it is not in another test.
+ expected_values = {
+ 'is_election_board': True,
+ 'generic_org_type': DomainRequest.OrganizationChoices.TRIBAL,
+ 'organization_type': DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
+ }
+ self.assert_expected_org_values_on_request_and_info(tribal_election_request, tribal_election_info, expected_values)
+
+ # Run the populate script
+ try:
+ self.run_populate_organization_type()
+ except Exception as e:
+ self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
+
+ # Because we don't define this in the "csv", we expect that is election board will switch to False,
+ # and organization_type will now be tribal
+ expected_values["is_election_board"] = False
+ expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL
+ self.assert_expected_org_values_on_request_and_info(tribal_election_request, tribal_election_info, expected_values)
+
+ @skip("TODO")
+ def test_transition_data(self):
+ """Tests for how this script interacts with prexisting data (for instance, stable)"""
+ # Make instead of mocking we can literally just run the transition domain scripts?
+ pass
class TestPopulateFirstReady(TestCase):
From 7edc6e75f453e6f01155e9b039f85e7a6b1a5b3d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 5 Apr 2024 12:04:42 -0600
Subject: [PATCH 06/71] Unit tests
---
.../commands/populate_organization_type.py | 75 ++++++++++---------
src/registrar/signals.py | 13 ++--
.../tests/test_management_scripts.py | 51 ++++++++-----
3 files changed, 77 insertions(+), 62 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index f10b0ae3c..ce0dfae9f 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -90,24 +90,30 @@ class Command(BaseCommand):
self.update_domain_informations(domain_infos, debug)
def update_domain_requests(self, domain_requests, debug):
- with transaction.atomic():
- for request in domain_requests:
- try:
- if request.generic_org_type is not None:
- domain_name = request.requested_domain.name
- request.is_election_board = domain_name in self.domains_with_election_offices_set
- request = create_or_update_organization_type(DomainRequest, request, return_instance=True)
- self.request_to_update.append(request)
- if debug:
- logger.info(f"Updating {request} => {request.organization_type}")
- else:
+ for request in domain_requests:
+ try:
+ if request.generic_org_type is not None:
+ domain_name = request.requested_domain.name
+ request.is_election_board = domain_name in self.domains_with_election_offices_set
+ new_request = create_or_update_organization_type(DomainRequest, request, return_instance=True)
+ print(f"what is the new request? {new_request}")
+ if not new_request:
self.request_skipped.append(request)
- if debug:
- logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
- except Exception as err:
- self.request_failed_to_update.append(request)
- logger.error(err)
- logger.error(f"{TerminalColors.FAIL}" f"Failed to update {request}" f"{TerminalColors.ENDC}")
+ logger.warning(f"Skipped updating {request}. No changes to be made.")
+ else:
+ request = new_request
+ self.request_to_update.append(request)
+
+ if debug:
+ logger.info(f"Updating {request} => {request.organization_type}")
+ else:
+ self.request_skipped.append(request)
+ if debug:
+ logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
+ except Exception as err:
+ self.request_failed_to_update.append(request)
+ logger.error(err)
+ logger.error(f"{TerminalColors.FAIL}" f"Failed to update {request}" f"{TerminalColors.ENDC}")
# Do a bulk update on the organization_type field
ScriptDataHelper.bulk_update_fields(
@@ -121,24 +127,23 @@ class Command(BaseCommand):
)
def update_domain_informations(self, domain_informations, debug):
- with transaction.atomic():
- for info in domain_informations:
- try:
- if info.generic_org_type is not None:
- domain_name = info.domain.name
- info.is_election_board = domain_name in self.domains_with_election_offices_set
- info = create_or_update_organization_type(DomainInformation, info, return_instance=True)
- self.di_to_update.append(info)
- if debug:
- logger.info(f"Updating {info} => {info.organization_type}")
- else:
- self.di_skipped.append(info)
- if debug:
- logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
- except Exception as err:
- self.di_failed_to_update.append(info)
- logger.error(err)
- logger.error(f"{TerminalColors.FAIL}" f"Failed to update {info}" f"{TerminalColors.ENDC}")
+ for info in domain_informations:
+ try:
+ if info.generic_org_type is not None:
+ domain_name = info.domain.name
+ info.is_election_board = domain_name in self.domains_with_election_offices_set
+ info = create_or_update_organization_type(DomainInformation, info, return_instance=True)
+ self.di_to_update.append(info)
+ if debug:
+ logger.info(f"Updating {info} => {info.organization_type}")
+ else:
+ self.di_skipped.append(info)
+ if debug:
+ logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
+ except Exception as err:
+ self.di_failed_to_update.append(info)
+ logger.error(err)
+ logger.error(f"{TerminalColors.FAIL}" f"Failed to update {info}" f"{TerminalColors.ENDC}")
# Do a bulk update on the organization_type field
ScriptDataHelper.bulk_update_fields(
diff --git a/src/registrar/signals.py b/src/registrar/signals.py
index aab267f5a..a4530b8c2 100644
--- a/src/registrar/signals.py
+++ b/src/registrar/signals.py
@@ -37,7 +37,6 @@ def create_or_update_organization_type(sender: DomainRequest | DomainInformation
# 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 == #
@@ -54,7 +53,7 @@ def create_or_update_organization_type(sender: DomainRequest | DomainInformation
# 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, generic_org_to_org_map)
- elif generic_org_type_needs_update:
+ elif generic_org_type_needs_update and instance.organization_type is not None:
_update_generic_org_and_election_from_org_type(
instance, election_org_to_generic_org_map, generic_org_to_org_map
)
@@ -63,12 +62,12 @@ def create_or_update_organization_type(sender: DomainRequest | DomainInformation
# == Init variables == #
# Instance is already in the database, fetch its current state
current_instance = sender.objects.get(id=instance.id)
-
+ print(f"what is the current instance? {current_instance.__dict__}")
# Check the new and old values
generic_org_type_changed = instance.generic_org_type != current_instance.generic_org_type
is_election_board_changed = instance.is_election_board != current_instance.is_election_board
organization_type_changed = instance.organization_type != current_instance.organization_type
-
+ print(f"whats changing? generic {generic_org_type_changed} vs election {is_election_board_changed} vs org {organization_type_changed}")
# == Check for invalid conditions before proceeding == #
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,
@@ -88,7 +87,7 @@ def create_or_update_organization_type(sender: DomainRequest | DomainInformation
# 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:
+ elif generic_org_type_needs_update and instance.organization_type is not None:
_update_generic_org_and_election_from_org_type(
instance, election_org_to_generic_org_map, generic_org_to_org_map
)
@@ -114,13 +113,13 @@ def _update_org_type_from_generic_org_and_election(instance, org_map):
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
+ instance.organization_type = org_map.get(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):
"""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).
diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py
index 303c3dfd6..71ce01424 100644
--- a/src/registrar/tests/test_management_scripts.py
+++ b/src/registrar/tests/test_management_scripts.py
@@ -37,19 +37,23 @@ class TestPopulateOrganizationType(MockEppLib):
name="lasers.gov",
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
is_election_board=True,
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
self.domain_request_2 = completed_domain_request(
name="readysetgo.gov",
generic_org_type=DomainRequest.OrganizationChoices.CITY,
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
self.domain_request_3 = completed_domain_request(
name="manualtransmission.gov",
generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
self.domain_request_4 = completed_domain_request(
name="saladandfries.gov",
generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
is_election_board=True,
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
# Approve all three requests
@@ -82,7 +86,7 @@ class TestPopulateOrganizationType(MockEppLib):
Contact.objects.all().delete()
Website.objects.all().delete()
- @less_console_noise_decorator
+ #@less_console_noise_decorator
def run_populate_organization_type(self):
"""
This method executes the populate_organization_type command.
@@ -109,14 +113,16 @@ class TestPopulateOrganizationType(MockEppLib):
"""
# Test domain request
- self.assertEqual(domain_request.generic_org_type, expected_values['generic_org_type'])
- self.assertEqual(domain_request.is_election_board, expected_values['is_election_board'])
- self.assertEqual(domain_request.organization_type, expected_values['organization_type'])
+ with self.subTest(field="DomainRequest"):
+ self.assertEqual(domain_request.generic_org_type, expected_values['generic_org_type'])
+ self.assertEqual(domain_request.is_election_board, expected_values['is_election_board'])
+ self.assertEqual(domain_request.organization_type, expected_values['organization_type'])
# Test domain info
- self.assertEqual(domain_info.generic_org_type, expected_values['generic_org_type'])
- self.assertEqual(domain_info.is_election_board, expected_values['is_election_board'])
- self.assertEqual(domain_info.organization_type, expected_values['organization_type'])
+ with self.subTest(field="DomainInformation"):
+ self.assertEqual(domain_info.generic_org_type, expected_values['generic_org_type'])
+ self.assertEqual(domain_info.is_election_board, expected_values['is_election_board'])
+ self.assertEqual(domain_info.organization_type, expected_values['organization_type'])
def test_request_and_info_city_not_in_csv(self):
"""Tests what happens to a city domain that is not defined in the CSV"""
@@ -174,18 +180,19 @@ class TestPopulateOrganizationType(MockEppLib):
for the domain request and the domain info
"""
+ # Set org type fields to none to mimic an environment without this data
tribal_request = self.domain_request_3
+ tribal_request.organization_type = None
+ tribal_request.save()
tribal_info = self.domain_info_3
+ tribal_info.organization_type = None
+ tribal_info.save()
# Make sure that all data is correct before proceeding.
- # Because the presave fixture is in place when creating this, we should expect that the
- # organization_type variable is already pre-populated. We will test what happens when
- # it is not in another test.
-
expected_values = {
'is_election_board': False,
'generic_org_type': DomainRequest.OrganizationChoices.TRIBAL,
- 'organization_type': DomainRequest.OrgChoicesElectionOffice.TRIBAL
+ 'organization_type': None,
}
self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
@@ -194,7 +201,10 @@ class TestPopulateOrganizationType(MockEppLib):
self.run_populate_organization_type()
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
-
+
+ tribal_request.refresh_from_db()
+ tribal_info.refresh_from_db()
+
# Because we define this in the "csv", we expect that is election board will switch to True,
# and organization_type will now be tribal_election
expected_values["is_election_board"] = True
@@ -209,8 +219,13 @@ class TestPopulateOrganizationType(MockEppLib):
for the domain request and the domain info
"""
+ # Set org type fields to none to mimic an environment without this data
tribal_election_request = self.domain_request_4
+ tribal_election_request.organization_type = None
+ tribal_election_request.save()
tribal_election_info = self.domain_info_4
+ tribal_election_info.organization_type = None
+ tribal_election_info.save()
# Make sure that all data is correct before proceeding.
# Because the presave fixture is in place when creating this, we should expect that the
@@ -219,7 +234,7 @@ class TestPopulateOrganizationType(MockEppLib):
expected_values = {
'is_election_board': True,
'generic_org_type': DomainRequest.OrganizationChoices.TRIBAL,
- 'organization_type': DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
+ 'organization_type': None
}
self.assert_expected_org_values_on_request_and_info(tribal_election_request, tribal_election_info, expected_values)
@@ -233,14 +248,10 @@ class TestPopulateOrganizationType(MockEppLib):
# and organization_type will now be tribal
expected_values["is_election_board"] = False
expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL
+ tribal_election_request.refresh_from_db()
+ tribal_election_info.refresh_from_db()
self.assert_expected_org_values_on_request_and_info(tribal_election_request, tribal_election_info, expected_values)
- @skip("TODO")
- def test_transition_data(self):
- """Tests for how this script interacts with prexisting data (for instance, stable)"""
- # Make instead of mocking we can literally just run the transition domain scripts?
- pass
-
class TestPopulateFirstReady(TestCase):
"""Tests for the populate_first_ready script"""
From 574cb1bafae8b211b419a3d4d4df3c29440158ca Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 8 Apr 2024 09:45:12 -0600
Subject: [PATCH 07/71] Update test_management_scripts.py
---
src/registrar/tests/test_management_scripts.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py
index 71ce01424..c2067556d 100644
--- a/src/registrar/tests/test_management_scripts.py
+++ b/src/registrar/tests/test_management_scripts.py
@@ -149,15 +149,13 @@ class TestPopulateOrganizationType(MockEppLib):
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
def test_request_and_info_federal(self):
- """Tests what happens to a federal domain with is_election_board=True (which should be reverted to None)"""
+ """Tests what happens to a federal domain after the script is run (should be unchanged)"""
federal_request = self.domain_request_1
federal_info = self.domain_info_1
# Make sure that all data is correct before proceeding.
# Since the presave fixture is in effect, we should expect that
# is_election_board is equal to none, even though we tried to define it as "True"
- # TODO - either add some logging if this happens or a ValueError in the original ticket.
- # Or FSM? Probably FSM.
expected_values = {
'is_election_board': None,
'generic_org_type': DomainRequest.OrganizationChoices.FEDERAL,
From 212d2002e3c49dad4890b430c663661d628bb571 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 8 Apr 2024 14:06:13 -0600
Subject: [PATCH 08/71] Fix script
---
.../management/commands/populate_organization_type.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index ce0dfae9f..4f79008db 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -95,8 +95,6 @@ class Command(BaseCommand):
if request.generic_org_type is not None:
domain_name = request.requested_domain.name
request.is_election_board = domain_name in self.domains_with_election_offices_set
- new_request = create_or_update_organization_type(DomainRequest, request, return_instance=True)
- print(f"what is the new request? {new_request}")
if not new_request:
self.request_skipped.append(request)
logger.warning(f"Skipped updating {request}. No changes to be made.")
@@ -132,7 +130,6 @@ class Command(BaseCommand):
if info.generic_org_type is not None:
domain_name = info.domain.name
info.is_election_board = domain_name in self.domains_with_election_offices_set
- info = create_or_update_organization_type(DomainInformation, info, return_instance=True)
self.di_to_update.append(info)
if debug:
logger.info(f"Updating {info} => {info.organization_type}")
From a976171179cd78fbd80ec27876a19ab1a409f7ad Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 8 Apr 2024 14:59:07 -0600
Subject: [PATCH 09/71] fix script
---
.../commands/populate_organization_type.py | 13 ++++---------
.../management/commands/utility/terminal_helper.py | 1 +
src/registrar/models/domain_information.py | 13 +++++++++++--
src/registrar/models/domain_request.py | 12 +++++++++---
4 files changed, 25 insertions(+), 14 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index 4f79008db..1610c1f9b 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -1,12 +1,10 @@
import argparse
import logging
import os
-from registrar.signals import create_or_update_organization_type
from typing import List
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper, ScriptDataHelper
-from registrar.models import DomainInformation, DomainRequest, Domain
-from django.db import transaction
+from registrar.models import DomainInformation, DomainRequest
logger = logging.getLogger(__name__)
@@ -95,12 +93,8 @@ class Command(BaseCommand):
if request.generic_org_type is not None:
domain_name = request.requested_domain.name
request.is_election_board = domain_name in self.domains_with_election_offices_set
- if not new_request:
- self.request_skipped.append(request)
- logger.warning(f"Skipped updating {request}. No changes to be made.")
- else:
- request = new_request
- self.request_to_update.append(request)
+ request.sync_organization_type()
+ self.request_to_update.append(request)
if debug:
logger.info(f"Updating {request} => {request.organization_type}")
@@ -130,6 +124,7 @@ class Command(BaseCommand):
if info.generic_org_type is not None:
domain_name = info.domain.name
info.is_election_board = domain_name in self.domains_with_election_offices_set
+ info = info.sync_organization_type()
self.di_to_update.append(info)
if debug:
logger.info(f"Updating {info} => {info.organization_type}")
diff --git a/src/registrar/management/commands/utility/terminal_helper.py b/src/registrar/management/commands/utility/terminal_helper.py
index 2149f429a..b54209750 100644
--- a/src/registrar/management/commands/utility/terminal_helper.py
+++ b/src/registrar/management/commands/utility/terminal_helper.py
@@ -49,6 +49,7 @@ class ScriptDataHelper:
Usage:
bulk_update_fields(Domain, page.object_list, ["first_ready"])
"""
+ logger.info(f"{TerminalColors.YELLOW} Bulk updating fields... {TerminalColors.ENDC}")
# Create a Paginator object. Bulk_update on the full dataset
# is too memory intensive for our current app config, so we can chunk this data instead.
paginator = Paginator(update_list, batch_size)
diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py
index 2ed27504c..6bdc6c00d 100644
--- a/src/registrar/models/domain_information.py
+++ b/src/registrar/models/domain_information.py
@@ -236,8 +236,11 @@ class DomainInformation(TimeStampedModel):
except Exception:
return ""
- def save(self, *args, **kwargs):
- """Save override for custom properties"""
+ def sync_organization_type(self):
+ """
+ Updates the organization_type (without saving) to match
+ the is_election_board and generic_organization_type fields.
+ """
# Define mappings between generic org and election org.
# These have to be defined here, as you'd get a cyclical import error
@@ -262,6 +265,12 @@ class DomainInformation(TimeStampedModel):
# Actually updates the organization_type field
org_type_helper.create_or_update_organization_type()
+
+ return self
+
+ def save(self, *args, **kwargs):
+ """Save override for custom properties"""
+ self.sync_organization_type()
super().save(*args, **kwargs)
@classmethod
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index bd529f7e6..1b8a519a0 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -666,9 +666,11 @@ class DomainRequest(TimeStampedModel):
help_text="Notes about this request",
)
- def save(self, *args, **kwargs):
- """Save override for custom properties"""
-
+ def sync_organization_type(self):
+ """
+ Updates the organization_type (without saving) to match
+ the is_election_board and generic_organization_type fields.
+ """
# Define mappings between generic org and election org.
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
@@ -692,6 +694,10 @@ class DomainRequest(TimeStampedModel):
# Actually updates the organization_type field
org_type_helper.create_or_update_organization_type()
+
+ def save(self, *args, **kwargs):
+ """Save override for custom properties"""
+ self.sync_organization_type()
super().save(*args, **kwargs)
def __str__(self):
From c1b7e7476cf423a30862e870a5e178a3797f7be7 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 8 Apr 2024 20:05:27 -0600
Subject: [PATCH 10/71] Update populate_organization_type.py
---
.../commands/populate_organization_type.py | 35 +++++++++++++++++--
1 file changed, 33 insertions(+), 2 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index 1610c1f9b..4c41716d2 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -5,6 +5,7 @@ from typing import List
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper, ScriptDataHelper
from registrar.models import DomainInformation, DomainRequest
+from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
logger = logging.getLogger(__name__)
@@ -93,7 +94,7 @@ class Command(BaseCommand):
if request.generic_org_type is not None:
domain_name = request.requested_domain.name
request.is_election_board = domain_name in self.domains_with_election_offices_set
- request.sync_organization_type()
+ request = self.sync_organization_type(DomainRequest, request)
self.request_to_update.append(request)
if debug:
@@ -124,7 +125,7 @@ class Command(BaseCommand):
if info.generic_org_type is not None:
domain_name = info.domain.name
info.is_election_board = domain_name in self.domains_with_election_offices_set
- info = info.sync_organization_type()
+ info = self.sync_organization_type(DomainInformation, info)
self.di_to_update.append(info)
if debug:
logger.info(f"Updating {info} => {info.organization_type}")
@@ -147,3 +148,33 @@ class Command(BaseCommand):
TerminalHelper.log_script_run_summary(
self.di_to_update, self.di_failed_to_update, self.di_skipped, debug, log_header
)
+
+ def sync_organization_type(self, sender, instance, force_update=False):
+ """
+ Updates the organization_type (without saving) to match
+ the is_election_board and generic_organization_type fields.
+ """
+
+ # Define mappings between generic org and election org.
+ # These have to be defined here, as you'd get a cyclical import error
+ # otherwise.
+
+ # For any given organization type, return the "_election" variant.
+ # For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
+ generic_org_map = DomainRequest.OrgChoicesElectionOffice.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_map = DomainRequest.OrgChoicesElectionOffice.get_org_election_to_org_generic()
+
+ # Manages the "organization_type" variable and keeps in sync with
+ # "is_election_office" and "generic_organization_type"
+ org_type_helper = CreateOrUpdateOrganizationTypeHelper(
+ sender=sender,
+ instance=instance,
+ generic_org_to_org_map=generic_org_map,
+ election_org_to_generic_org_map=election_org_map,
+ )
+
+ instance = org_type_helper.create_or_update_organization_type(force_update)
+ return instance
From 5313ab33b3a8bc0bdb7dcf0a87e9be9ca4e2ef36 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 8 Apr 2024 21:03:32 -0600
Subject: [PATCH 11/71] fix unit tests
---
.../commands/populate_organization_type.py | 4 +-
.../tests/test_management_scripts.py | 74 +++++++++++--------
src/registrar/tests/test_signals.py | 3 +-
3 files changed, 46 insertions(+), 35 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index 4c41716d2..9e2b1bf6a 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -149,7 +149,7 @@ class Command(BaseCommand):
self.di_to_update, self.di_failed_to_update, self.di_skipped, debug, log_header
)
- def sync_organization_type(self, sender, instance, force_update=False):
+ def sync_organization_type(self, sender, instance):
"""
Updates the organization_type (without saving) to match
the is_election_board and generic_organization_type fields.
@@ -176,5 +176,5 @@ class Command(BaseCommand):
election_org_to_generic_org_map=election_org_map,
)
- instance = org_type_helper.create_or_update_organization_type(force_update)
+ instance = org_type_helper.create_or_update_organization_type()
return instance
diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py
index c2067556d..26ec6fd1d 100644
--- a/src/registrar/tests/test_management_scripts.py
+++ b/src/registrar/tests/test_management_scripts.py
@@ -1,6 +1,5 @@
import copy
from datetime import date, datetime, time
-from unittest import skip
from django.utils import timezone
from django.test import TestCase
@@ -25,6 +24,7 @@ from epplibwrapper import commands, common
from .common import MockEppLib, less_console_noise, completed_domain_request
from api.tests.common import less_console_noise_decorator
+
class TestPopulateOrganizationType(MockEppLib):
"""Tests for the populate_organization_type script"""
@@ -86,7 +86,7 @@ class TestPopulateOrganizationType(MockEppLib):
Contact.objects.all().delete()
Website.objects.all().delete()
- #@less_console_noise_decorator
+ @less_console_noise_decorator
def run_populate_organization_type(self):
"""
This method executes the populate_organization_type command.
@@ -99,7 +99,7 @@ class TestPopulateOrganizationType(MockEppLib):
return_value=True,
):
call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv", debug=True)
-
+
def assert_expected_org_values_on_request_and_info(
self,
domain_request: DomainRequest,
@@ -114,15 +114,15 @@ class TestPopulateOrganizationType(MockEppLib):
# Test domain request
with self.subTest(field="DomainRequest"):
- self.assertEqual(domain_request.generic_org_type, expected_values['generic_org_type'])
- self.assertEqual(domain_request.is_election_board, expected_values['is_election_board'])
- self.assertEqual(domain_request.organization_type, expected_values['organization_type'])
+ self.assertEqual(domain_request.generic_org_type, expected_values["generic_org_type"])
+ self.assertEqual(domain_request.is_election_board, expected_values["is_election_board"])
+ self.assertEqual(domain_request.organization_type, expected_values["organization_type"])
# Test domain info
with self.subTest(field="DomainInformation"):
- self.assertEqual(domain_info.generic_org_type, expected_values['generic_org_type'])
- self.assertEqual(domain_info.is_election_board, expected_values['is_election_board'])
- self.assertEqual(domain_info.organization_type, expected_values['organization_type'])
+ self.assertEqual(domain_info.generic_org_type, expected_values["generic_org_type"])
+ self.assertEqual(domain_info.is_election_board, expected_values["is_election_board"])
+ self.assertEqual(domain_info.organization_type, expected_values["organization_type"])
def test_request_and_info_city_not_in_csv(self):
"""Tests what happens to a city domain that is not defined in the CSV"""
@@ -133,9 +133,9 @@ class TestPopulateOrganizationType(MockEppLib):
# Since the presave fixture is in effect, we should expect that
# is_election_board is equal to none, even though we tried to define it as "True"
expected_values = {
- 'is_election_board': False,
- 'generic_org_type': DomainRequest.OrganizationChoices.CITY,
- 'organization_type': DomainRequest.OrgChoicesElectionOffice.CITY,
+ "is_election_board": False,
+ "generic_org_type": DomainRequest.OrganizationChoices.CITY,
+ "organization_type": DomainRequest.OrgChoicesElectionOffice.CITY,
}
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
@@ -144,7 +144,7 @@ class TestPopulateOrganizationType(MockEppLib):
self.run_populate_organization_type()
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
-
+
# All values should be the same
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
@@ -157,9 +157,9 @@ class TestPopulateOrganizationType(MockEppLib):
# Since the presave fixture is in effect, we should expect that
# is_election_board is equal to none, even though we tried to define it as "True"
expected_values = {
- 'is_election_board': None,
- 'generic_org_type': DomainRequest.OrganizationChoices.FEDERAL,
- 'organization_type': DomainRequest.OrgChoicesElectionOffice.FEDERAL,
+ "is_election_board": None,
+ "generic_org_type": DomainRequest.OrganizationChoices.FEDERAL,
+ "organization_type": DomainRequest.OrgChoicesElectionOffice.FEDERAL,
}
self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
@@ -168,10 +168,14 @@ class TestPopulateOrganizationType(MockEppLib):
self.run_populate_organization_type()
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
-
+
# All values should be the same
self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
+ def do_nothing(self):
+ """Does nothing for mocking purposes"""
+ pass
+
def test_request_and_info_tribal_add_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL - ELECTION
@@ -181,16 +185,18 @@ class TestPopulateOrganizationType(MockEppLib):
# Set org type fields to none to mimic an environment without this data
tribal_request = self.domain_request_3
tribal_request.organization_type = None
- tribal_request.save()
tribal_info = self.domain_info_3
tribal_info.organization_type = None
- tribal_info.save()
+ with patch.object(DomainRequest, "sync_organization_type", self.do_nothing):
+ with patch.object(DomainInformation, "sync_organization_type", self.do_nothing):
+ tribal_request.save()
+ tribal_info.save()
# Make sure that all data is correct before proceeding.
expected_values = {
- 'is_election_board': False,
- 'generic_org_type': DomainRequest.OrganizationChoices.TRIBAL,
- 'organization_type': None,
+ "is_election_board": False,
+ "generic_org_type": DomainRequest.OrganizationChoices.TRIBAL,
+ "organization_type": None,
}
self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
@@ -219,36 +225,42 @@ class TestPopulateOrganizationType(MockEppLib):
# Set org type fields to none to mimic an environment without this data
tribal_election_request = self.domain_request_4
- tribal_election_request.organization_type = None
- tribal_election_request.save()
tribal_election_info = self.domain_info_4
+ tribal_election_request.organization_type = None
tribal_election_info.organization_type = None
- tribal_election_info.save()
+ with patch.object(DomainRequest, "sync_organization_type", self.do_nothing):
+ with patch.object(DomainInformation, "sync_organization_type", self.do_nothing):
+ tribal_election_request.save()
+ tribal_election_info.save()
# Make sure that all data is correct before proceeding.
# Because the presave fixture is in place when creating this, we should expect that the
# organization_type variable is already pre-populated. We will test what happens when
# it is not in another test.
expected_values = {
- 'is_election_board': True,
- 'generic_org_type': DomainRequest.OrganizationChoices.TRIBAL,
- 'organization_type': None
+ "is_election_board": True,
+ "generic_org_type": DomainRequest.OrganizationChoices.TRIBAL,
+ "organization_type": None,
}
- self.assert_expected_org_values_on_request_and_info(tribal_election_request, tribal_election_info, expected_values)
+ self.assert_expected_org_values_on_request_and_info(
+ tribal_election_request, tribal_election_info, expected_values
+ )
# Run the populate script
try:
self.run_populate_organization_type()
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
-
+
# Because we don't define this in the "csv", we expect that is election board will switch to False,
# and organization_type will now be tribal
expected_values["is_election_board"] = False
expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL
tribal_election_request.refresh_from_db()
tribal_election_info.refresh_from_db()
- self.assert_expected_org_values_on_request_and_info(tribal_election_request, tribal_election_info, expected_values)
+ self.assert_expected_org_values_on_request_and_info(
+ tribal_election_request, tribal_election_info, expected_values
+ )
class TestPopulateFirstReady(TestCase):
diff --git a/src/registrar/tests/test_signals.py b/src/registrar/tests/test_signals.py
index 7af6012a9..e796bd12a 100644
--- a/src/registrar/tests/test_signals.py
+++ b/src/registrar/tests/test_signals.py
@@ -1,7 +1,6 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
-from registrar.models import Contact, DomainRequest, Domain, DomainInformation
-from registrar.tests.common import completed_domain_request
+from registrar.models import Contact
class TestUserPostSave(TestCase):
From 1ecbb2b7962b1330fbecc30567ae096b15473a09 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 9 Apr 2024 08:15:30 -0600
Subject: [PATCH 12/71] Update test_reports.py
---
src/registrar/tests/test_reports.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 1fe923f5d..be66cb876 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -718,7 +718,7 @@ class HelperFunctions(MockDb):
}
# Test with distinct
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
- expected_content = [3, 4, 1, 0, 0, 0, 0, 0, 0, 0]
+ 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
From fc4ccd72ae6c72d3551e21f1ca38a4d0df928f9f Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 9 Apr 2024 15:47:04 -0600
Subject: [PATCH 13/71] Change org type
---
src/registrar/utility/csv_export.py | 117 ++++++++++++----------------
1 file changed, 48 insertions(+), 69 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 949b0adcd..91eaad09e 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -3,6 +3,7 @@ import logging
from datetime import datetime
from registrar.models.domain import Domain
from registrar.models.domain_invitation import DomainInvitation
+from django.db.models import Case, When, Count, Value
from registrar.models.domain_request import DomainRequest
from registrar.models.domain_information import DomainInformation
from django.utils import timezone
@@ -87,10 +88,10 @@ def parse_row_for_domain(
if security_email.lower() in invalid_emails:
security_email = "(blank)"
- if domain_info.federal_type and domain_info.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
- domain_type = f"{domain_info.get_generic_org_type_display()} - {domain_info.get_federal_type_display()}"
+ if domain_info.federal_type and domain_info.organization_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
+ domain_type = f"{domain_info.organization_type} - {domain_info.get_federal_type_display()}"
else:
- domain_type = domain_info.get_generic_org_type_display()
+ domain_type = domain_info.organization_type
# create a dictionary of fields which can be included in output
FIELDS = {
@@ -319,9 +320,9 @@ def parse_row_for_requests(columns, request: DomainRequest):
requested_domain_name = request.requested_domain.name
if request.federal_type:
- request_type = f"{request.get_generic_org_type_display()} - {request.get_federal_type_display()}"
+ request_type = f"{request.organization_type} - {request.get_federal_type_display()}"
else:
- request_type = request.get_generic_org_type_display()
+ request_type = request.organization_type
# create a dictionary of fields which can be included in output
FIELDS = {
@@ -399,7 +400,7 @@ def export_data_type_to_csv(csv_file):
# Coalesce is used to replace federal_type of None with ZZZZZ
sort_fields = [
- "generic_org_type",
+ "organization_type",
Coalesce("federal_type", Value("ZZZZZ")),
"federal_agency",
"domain__name",
@@ -432,7 +433,7 @@ def export_data_full_to_csv(csv_file):
]
# Coalesce is used to replace federal_type of None with ZZZZZ
sort_fields = [
- "generic_org_type",
+ "organization_type",
Coalesce("federal_type", Value("ZZZZZ")),
"federal_agency",
"domain__name",
@@ -465,13 +466,13 @@ def export_data_federal_to_csv(csv_file):
]
# Coalesce is used to replace federal_type of None with ZZZZZ
sort_fields = [
- "generic_org_type",
+ "organization_type",
Coalesce("federal_type", Value("ZZZZZ")),
"federal_agency",
"domain__name",
]
filter_condition = {
- "generic_org_type__icontains": "federal",
+ "organization_type__icontains": "federal",
"domain__state__in": [
Domain.State.READY,
Domain.State.DNS_NEEDED,
@@ -566,74 +567,52 @@ def get_sliced_domains(filter_condition):
Pass distinct=True when filtering by permissions so we do not to count multiples
when a domain has more that one manager.
"""
-
- domains = DomainInformation.objects.all().filter(**filter_condition).distinct()
- domains_count = domains.count()
- federal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
- interstate = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).count()
- state_or_territory = (
- domains.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
- )
- 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,
- federal,
- interstate,
- state_or_territory,
- tribal,
- county,
- city,
- special_district,
- school_district,
- election_board,
- ]
-
+ return get_org_type_counts(DomainInformation, filter_condition)
def get_sliced_requests(filter_condition):
"""Get filtered requests counts sliced by org type and election office."""
+ return get_org_type_counts(DomainRequest, filter_condition)
- 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()
- )
- 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()
+def _org_type_count_query_builder(generic_org_type):
+ return Count(Case(When(generic_org_type=generic_org_type, then=1)))
+
+def get_org_type_counts(model_class, filter_condition):
+ """Returns a list of counts for each org type"""
+
+ dynamic_count_dict = {}
+ for choice in DomainRequest.OrganizationChoices:
+ choice_name = f"{choice}_count"
+ dynamic_count_dict[choice_name] = _org_type_count_query_builder(choice)
+
+ # Static counts
+ static_count_dict = {
+ # Count all distinct records
+ "total_count": Count('id'),
+ "election_board_count": Count(Case(When(is_election_board=True, then=1))),
+ }
+
+ # Merge static aggregates with dynamic organization type counts
+ merged_count_dict = {**static_count_dict, **dynamic_count_dict}
+
+ # Perform a single query with conditional aggregation
+ model_queryset = model_class.objects.filter(**filter_condition).distinct()
+ aggregates = model_queryset.aggregate(**merged_count_dict)
+
+ # TODO - automate this
return [
- requests_count,
- federal,
- interstate,
- state_or_territory,
- tribal,
- county,
- city,
- special_district,
- school_district,
- election_board,
+ aggregates['total_count'], # total count
+ aggregates['federal_count'], # federal count
+ aggregates['interstate_count'], # interstate count
+ aggregates['state_or_territory_count'], # state or territory count
+ aggregates['tribal_count'], # tribal count
+ aggregates['county_count'], # county count
+ aggregates['city_count'], # city count
+ aggregates['special_district_count'], # special district count
+ aggregates['school_district_count'], # school district count
+ aggregates['election_board_count'], # election board count
]
-
def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
"""Get counts for domains that have domain managers for two different dates,
get list of managed domains at end_date."""
From 31958d3cb500c1f48ccee94b331e361826dc5fa4 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 08:39:37 -0600
Subject: [PATCH 14/71] Change org type
---
src/registrar/utility/csv_export.py | 54 ++++++++++++++++++++---------
1 file changed, 38 insertions(+), 16 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 91eaad09e..2706a0d87 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -573,13 +573,10 @@ def get_sliced_requests(filter_condition):
"""Get filtered requests counts sliced by org type and election office."""
return get_org_type_counts(DomainRequest, filter_condition)
-
-def _org_type_count_query_builder(generic_org_type):
- return Count(Case(When(generic_org_type=generic_org_type, then=1)))
-
def get_org_type_counts(model_class, filter_condition):
"""Returns a list of counts for each org type"""
+ # Count all org types, such as federal
dynamic_count_dict = {}
for choice in DomainRequest.OrganizationChoices:
choice_name = f"{choice}_count"
@@ -589,30 +586,55 @@ def get_org_type_counts(model_class, filter_condition):
static_count_dict = {
# Count all distinct records
"total_count": Count('id'),
+ # Count all election boards
"election_board_count": Count(Case(When(is_election_board=True, then=1))),
}
- # Merge static aggregates with dynamic organization type counts
+ # Merge static counts with dynamic organization type counts
merged_count_dict = {**static_count_dict, **dynamic_count_dict}
# Perform a single query with conditional aggregation
model_queryset = model_class.objects.filter(**filter_condition).distinct()
aggregates = model_queryset.aggregate(**merged_count_dict)
- # TODO - automate this
+ # This can be automated but for the sake of readability, this is fixed for now.
+ # To automate this would also mean the added benefit of
+ # auto-updating (automatically adds new org types) charts,
+ # but that requires an upstream refactor.
return [
- aggregates['total_count'], # total count
- aggregates['federal_count'], # federal count
- aggregates['interstate_count'], # interstate count
- aggregates['state_or_territory_count'], # state or territory count
- aggregates['tribal_count'], # tribal count
- aggregates['county_count'], # county count
- aggregates['city_count'], # city count
- aggregates['special_district_count'], # special district count
- aggregates['school_district_count'], # school district count
- aggregates['election_board_count'], # election board count
+ # Total number of records
+ aggregates['total_count'],
+ # Number of records with org type FEDERAL
+ aggregates['federal_count'],
+ # Number of records with org type INTERSTATE
+ aggregates['interstate_count'],
+ # Number of records with org type STATE_OR_TERRITORY
+ aggregates['state_or_territory_count'],
+ # Number of records for TRIBAL
+ aggregates['tribal_count'],
+ # Number of records for COUNTY
+ aggregates['county_count'],
+ # Number of records for CITY
+ aggregates['city_count'],
+ # Number of records for SPECIAL_DISTRICT
+ aggregates['special_district_count'],
+ # Number of records for SCHOOL_DISTRICT
+ aggregates['school_district_count'],
+ # Number of records for ELECTION_BOARD
+ aggregates['election_board_count'],
]
+def _org_type_count_query_builder(generic_org_type):
+ """
+ Returns an expression that counts the number of a given generic_org_type.
+ On the backend (the DB), this returns an array of "1" which is then counted by the expression.
+ Used within an .aggregate call, but this essentially performs the same as queryset.count()
+
+ We use this as opposed to queryset.count() because when this operation is repeated multiple times,
+ it is more efficient to do these once in the DB rather than multiple times (as each count consitutes a call)
+ """
+ return Count(Case(When(generic_org_type=generic_org_type, then=1)))
+
def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
"""Get counts for domains that have domain managers for two different dates,
get list of managed domains at end_date."""
From e38c1681990f02d4259da4d4dfc8dd1d83079ecc Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 08:44:58 -0600
Subject: [PATCH 15/71] Add display type
---
src/registrar/utility/csv_export.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 2706a0d87..80db11d6e 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -89,9 +89,9 @@ def parse_row_for_domain(
security_email = "(blank)"
if domain_info.federal_type and domain_info.organization_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
- domain_type = f"{domain_info.organization_type} - {domain_info.get_federal_type_display()}"
+ domain_type = f"{domain_info.get_organization_type_display()} - {domain_info.get_federal_type_display()}"
else:
- domain_type = domain_info.organization_type
+ domain_type = domain_info.get_organization_type_display()
# create a dictionary of fields which can be included in output
FIELDS = {
@@ -320,9 +320,9 @@ def parse_row_for_requests(columns, request: DomainRequest):
requested_domain_name = request.requested_domain.name
if request.federal_type:
- request_type = f"{request.organization_type} - {request.get_federal_type_display()}"
+ request_type = f"{request.get_organization_type_display()} - {request.get_federal_type_display()}"
else:
- request_type = request.organization_type
+ request_type = request.get_organization_type_display()
# create a dictionary of fields which can be included in output
FIELDS = {
From 7250e6587ca498f59ee85888473835f92f587d2b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 09:13:48 -0600
Subject: [PATCH 16/71] Add documentation
---
docs/operations/data_migration.md | 44 +++++++++++++++++++
.../commands/populate_organization_type.py | 2 +-
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md
index b7e413c05..e40a1ee50 100644
--- a/docs/operations/data_migration.md
+++ b/docs/operations/data_migration.md
@@ -586,3 +586,47 @@ Example: `cf ssh getgov-za`
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **debug** | Increases logging detail. Defaults to False. |
+
+
+## Populate First Ready
+This section outlines how to run the `populate_organization_type` script.
+
+### Running on sandboxes
+
+#### Step 1: Login to CloudFoundry
+```cf login -a api.fr.cloud.gov --sso```
+
+#### Step 2: SSH into your environment
+```cf ssh getgov-{space}```
+
+Example: `cf ssh getgov-za`
+
+#### Step 3: Create a shell instance
+```/tmp/lifecycle/shell```
+
+#### Step 4: Running the script
+```./manage.py populate_organization_type {domain_election_office_filename} --debug```
+
+- The domain_election_office_filename file must adhere to this format:
+ - example.gov\
+ example2.gov\
+ example3.gov
+
+Example:
+`./manage.py populate_organization_type migrationdata/election-domains.csv --debug`
+
+### Running locally
+```docker-compose exec app ./manage.py populate_organization_type {domain_election_office_filename} --debug```
+
+Example (assuming that this is being ran from src/):
+`docker-compose exec app ./manage.py populate_organization_type migrationdata/election-domains.csv --debug`
+
+##### Required parameters
+| | Parameter | Description |
+|:-:|:------------------------------------|:-------------------------------------------------------------------|
+| 1 | **domain_election_office_filename** | A file containing every domain that is an election office.
+
+##### Optional parameters
+| | Parameter | Description |
+|:-:|:-------------------------- |:----------------------------------------------------------------------------|
+| 1 | **debug** | Increases logging detail. Defaults to False.
\ No newline at end of file
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index 9e2b1bf6a..a0a1c8633 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -33,7 +33,7 @@ class Command(BaseCommand):
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
parser.add_argument(
"domain_election_office_filename",
- help=("A JSON file that holds the location and filenames" "of all the data files used for migrations"),
+ help=("A file that contains" " all the domains that are election offices."),
)
def handle(self, domain_election_office_filename, **kwargs):
From 82f94600b70782b463fe0c5f19bb77fbb5d789e8 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 10:13:45 -0600
Subject: [PATCH 17/71] Linting
---
src/registrar/utility/csv_export.py | 32 ++++++++++++++++-------------
1 file changed, 18 insertions(+), 14 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 80db11d6e..6d254ce7b 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -569,23 +569,25 @@ def get_sliced_domains(filter_condition):
"""
return get_org_type_counts(DomainInformation, filter_condition)
+
def get_sliced_requests(filter_condition):
"""Get filtered requests counts sliced by org type and election office."""
return get_org_type_counts(DomainRequest, filter_condition)
+
def get_org_type_counts(model_class, filter_condition):
"""Returns a list of counts for each org type"""
-
+
# Count all org types, such as federal
dynamic_count_dict = {}
for choice in DomainRequest.OrganizationChoices:
choice_name = f"{choice}_count"
dynamic_count_dict[choice_name] = _org_type_count_query_builder(choice)
-
+
# Static counts
static_count_dict = {
# Count all distinct records
- "total_count": Count('id'),
+ "total_count": Count("id"),
# Count all election boards
"election_board_count": Count(Case(When(is_election_board=True, then=1))),
}
@@ -598,32 +600,33 @@ def get_org_type_counts(model_class, filter_condition):
aggregates = model_queryset.aggregate(**merged_count_dict)
# This can be automated but for the sake of readability, this is fixed for now.
- # To automate this would also mean the added benefit of
+ # To automate this would also mean the added benefit of
# auto-updating (automatically adds new org types) charts,
# but that requires an upstream refactor.
return [
# Total number of records
- aggregates['total_count'],
+ aggregates["total_count"],
# Number of records with org type FEDERAL
- aggregates['federal_count'],
+ aggregates["federal_count"],
# Number of records with org type INTERSTATE
- aggregates['interstate_count'],
+ aggregates["interstate_count"],
# Number of records with org type STATE_OR_TERRITORY
- aggregates['state_or_territory_count'],
+ aggregates["state_or_territory_count"],
# Number of records for TRIBAL
- aggregates['tribal_count'],
+ aggregates["tribal_count"],
# Number of records for COUNTY
- aggregates['county_count'],
+ aggregates["county_count"],
# Number of records for CITY
- aggregates['city_count'],
+ aggregates["city_count"],
# Number of records for SPECIAL_DISTRICT
- aggregates['special_district_count'],
+ aggregates["special_district_count"],
# Number of records for SCHOOL_DISTRICT
- aggregates['school_district_count'],
+ aggregates["school_district_count"],
# Number of records for ELECTION_BOARD
- aggregates['election_board_count'],
+ aggregates["election_board_count"],
]
+
def _org_type_count_query_builder(generic_org_type):
"""
Returns an expression that counts the number of a given generic_org_type.
@@ -635,6 +638,7 @@ def _org_type_count_query_builder(generic_org_type):
"""
return Count(Case(When(generic_org_type=generic_org_type, then=1)))
+
def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
"""Get counts for domains that have domain managers for two different dates,
get list of managed domains at end_date."""
From 990a6f51e88b7f291b84b5eaedac58ff4b56adf8 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 12:12:00 -0600
Subject: [PATCH 18/71] Add custom inline
---
src/registrar/admin.py | 3 +-
src/registrar/templates/admin/stacked.html | 51 +++++++++++++++++++
...domain_information_inline_change_form.html | 6 +++
3 files changed, 59 insertions(+), 1 deletion(-)
create mode 100644 src/registrar/templates/admin/stacked.html
create mode 100644 src/registrar/templates/django/admin/domain_information_inline_change_form.html
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index b06111e5b..21850364e 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -69,6 +69,7 @@ class DomainInformationInlineForm(forms.ModelForm):
widgets = {
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
}
+ template = "django/admin/domain_information_change_form.html"
class DomainRequestAdminForm(forms.ModelForm):
@@ -1440,8 +1441,8 @@ class DomainInformationInline(admin.StackedInline):
from DomainInformationAdmin"""
form = DomainInformationInlineForm
-
model = models.DomainInformation
+ template = "django/admin/domain_information_inline_change_form.html"
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
# remove .gov domain from fieldset
diff --git a/src/registrar/templates/admin/stacked.html b/src/registrar/templates/admin/stacked.html
new file mode 100644
index 000000000..8eac01864
--- /dev/null
+++ b/src/registrar/templates/admin/stacked.html
@@ -0,0 +1,51 @@
+{% load i18n admin_urls %}
+{% load i18n static %}
+
+{% comment %}
+This is copied from Djangos implementation of this template, with added "blocks"
+It is not inherently customizable on its own, so we can modify this instead.
+https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/edit_inline/stacked.html
+{% endcomment %}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/registrar/templates/django/admin/domain_information_inline_change_form.html b/src/registrar/templates/django/admin/domain_information_inline_change_form.html
new file mode 100644
index 000000000..a84981847
--- /dev/null
+++ b/src/registrar/templates/django/admin/domain_information_inline_change_form.html
@@ -0,0 +1,6 @@
+{% extends 'admin/stacked.html' %}
+{% load i18n static %}
+
+{% block fieldset %}
+ {% include "django/admin/includes/detail_table_fieldset.html" %}
+{% endblock %}
From 1bcc57e51ee67832b42cb5151d66d2e2ccae5a4c Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 13:05:17 -0600
Subject: [PATCH 19/71] Add inline
---
src/registrar/admin.py | 9 ++++-----
.../templates/django/admin/domain_change_form.html | 6 ++++++
.../admin/domain_information_change_form.html | 2 +-
.../django/admin/domain_request_change_form.html | 2 +-
.../admin/includes/detail_table_fieldset.html | 14 +++++++-------
.../domain_information_inline_change_form.html | 2 +-
6 files changed, 20 insertions(+), 15 deletions(-)
rename src/registrar/templates/django/admin/{ => includes}/domain_information_inline_change_form.html (79%)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 21850364e..e357e36b1 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1,7 +1,7 @@
from datetime import date
import logging
import copy
-
+from django.forms.models import BaseInlineFormSet
from django import forms
from django.db.models import Value, CharField, Q
from django.db.models.functions import Concat, Coalesce
@@ -69,7 +69,6 @@ class DomainInformationInlineForm(forms.ModelForm):
widgets = {
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
}
- template = "django/admin/domain_information_change_form.html"
class DomainRequestAdminForm(forms.ModelForm):
@@ -1442,7 +1441,6 @@ class DomainInformationInline(admin.StackedInline):
form = DomainInformationInlineForm
model = models.DomainInformation
- template = "django/admin/domain_information_inline_change_form.html"
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
# remove .gov domain from fieldset
@@ -1450,7 +1448,8 @@ class DomainInformationInline(admin.StackedInline):
if title == ".gov domain":
del fieldsets[index]
break
-
+
+ readonly_fields = DomainInformationAdmin.readonly_fields
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
# to activate the edit/delete/view buttons
@@ -1616,7 +1615,7 @@ class DomainAdmin(ListHeaderAdmin):
"""Custom changeform implementation to pass in context information"""
if extra_context is None:
extra_context = {}
-
+ extra_context["original_object"] = self.model.objects.get(pk=object_id)
# Pass in what the an extended expiration date would be for the expiration date modal
if object_id is not None:
domain = Domain.objects.get(pk=object_id)
diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html
index 44fe6851b..8d9f59675 100644
--- a/src/registrar/templates/django/admin/domain_change_form.html
+++ b/src/registrar/templates/django/admin/domain_change_form.html
@@ -36,6 +36,12 @@
{{ block.super }}
{% endblock %}
+{% block inline_field_sets %}
+ {% for inline_admin_formset in inline_admin_formsets %}
+ {% include "django/admin/includes/domain_information_inline_change_form.html" with original_object=original.domain_info%}
+ {% endfor %}
+{% endblock %}
+
{% block submit_buttons_bottom %}
{% comment %}
Modals behave very weirdly in django admin.
diff --git a/src/registrar/templates/django/admin/domain_information_change_form.html b/src/registrar/templates/django/admin/domain_information_change_form.html
index f58ee2239..d20e33151 100644
--- a/src/registrar/templates/django/admin/domain_information_change_form.html
+++ b/src/registrar/templates/django/admin/domain_information_change_form.html
@@ -10,6 +10,6 @@
Use detail_table_fieldset as an example, or just extend it.
{% endcomment %}
- {% include "django/admin/includes/detail_table_fieldset.html" %}
+ {% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
{% endfor %}
{% endblock %}
diff --git a/src/registrar/templates/django/admin/domain_request_change_form.html b/src/registrar/templates/django/admin/domain_request_change_form.html
index 3b4fa7283..cb1ba38c0 100644
--- a/src/registrar/templates/django/admin/domain_request_change_form.html
+++ b/src/registrar/templates/django/admin/domain_request_change_form.html
@@ -14,7 +14,7 @@
Use detail_table_fieldset as an example, or just extend it.
{% endcomment %}
- {% include "django/admin/includes/detail_table_fieldset.html" %}
+ {% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
{% endfor %}
{% endblock %}
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 a0a679290..9a89ec510 100644
--- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
+++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
@@ -5,7 +5,7 @@
This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endcomment %}
{% block field_readonly %}
- {% with all_contacts=original.other_contacts.all %}
+ {% with all_contacts=original_object.other_contacts.all %}
{% if field.field.name == "other_contacts" %}
{% if all_contacts.count > 2 %}
@@ -49,7 +49,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
- {% for alt_domain in original.alternative_domains.all %}
+ {% for alt_domain in original_object.alternative_domains.all %}
{{ alt_domain }}{% if not forloop.last %}, {% endif %}
{% endfor %}
@@ -63,20 +63,20 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% if field.field.name == "creator" %}
- {% include "django/admin/includes/contact_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %}
+ {% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
{% elif field.field.name == "submitter" %}
- {% include "django/admin/includes/contact_detail_list.html" with user=original.submitter no_title_top_padding=field.is_readonly %}
+ {% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
- {% include "django/admin/includes/contact_detail_list.html" with user=original.authorizing_official no_title_top_padding=field.is_readonly %}
+ {% include "django/admin/includes/contact_detail_list.html" with user=original_object.authorizing_official no_title_top_padding=field.is_readonly %}
- {% elif field.field.name == "other_contacts" and original.other_contacts.all %}
- {% with all_contacts=original.other_contacts.all %}
+ {% elif field.field.name == "other_contacts" and original_object.other_contacts.all %}
+ {% with all_contacts=original_object.other_contacts.all %}
{% if all_contacts.count > 2 %}
Details
diff --git a/src/registrar/templates/django/admin/domain_information_inline_change_form.html b/src/registrar/templates/django/admin/includes/domain_information_inline_change_form.html
similarity index 79%
rename from src/registrar/templates/django/admin/domain_information_inline_change_form.html
rename to src/registrar/templates/django/admin/includes/domain_information_inline_change_form.html
index a84981847..414c485e5 100644
--- a/src/registrar/templates/django/admin/domain_information_inline_change_form.html
+++ b/src/registrar/templates/django/admin/includes/domain_information_inline_change_form.html
@@ -2,5 +2,5 @@
{% load i18n static %}
{% block fieldset %}
- {% include "django/admin/includes/detail_table_fieldset.html" %}
+ {% include "django/admin/includes/detail_table_fieldset.html" with original_object=original_object %}
{% endblock %}
From 8cee9ef8caaec3afcc13429ab35c6b2ae2fd026a Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 14:34:34 -0600
Subject: [PATCH 20/71] Unit test
---
src/registrar/admin.py | 3 +-
src/registrar/tests/test_admin.py | 71 +++++++++++++++++++++++++++++++
2 files changed, 72 insertions(+), 2 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index e357e36b1..0c720c8af 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1,7 +1,6 @@
from datetime import date
import logging
import copy
-from django.forms.models import BaseInlineFormSet
from django import forms
from django.db.models import Value, CharField, Q
from django.db.models.functions import Concat, Coalesce
@@ -1448,7 +1447,7 @@ class DomainInformationInline(admin.StackedInline):
if title == ".gov domain":
del fieldsets[index]
break
-
+
readonly_fields = DomainInformationAdmin.readonly_fields
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 370051b8a..c6b20abce 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -76,6 +76,77 @@ class TestDomainAdmin(MockEppLib, WebTest):
)
super().setUp()
+ @less_console_noise_decorator
+ def test_contact_fields_have_detail_table(self):
+ """Tests if the contact fields have the detail table which displays title, email, and phone"""
+
+ # Create fake creator
+ _creator = User.objects.create(
+ username="MrMeoward",
+ first_name="Meoward",
+ last_name="Jones",
+ )
+
+ # Due to the relation between User <==> Contact,
+ # the underlying contact has to be modified this way.
+ _creator.contact.email = "meoward.jones@igorville.gov"
+ _creator.contact.phone = "(555) 123 12345"
+ _creator.contact.title = "Treat inspector"
+ _creator.contact.save()
+
+ # Create a fake domain request
+ domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
+ domain_request.approve()
+ _domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
+ domain = Domain.objects.filter(domain_info=_domain_info).get()
+
+ p = "adminpass"
+ self.client.login(username="superuser", password=p)
+ response = self.client.get(
+ "/admin/registrar/domain/{}/change/".format(domain.pk),
+ follow=True,
+ )
+
+ # Make sure the page loaded, and that we're on the right page
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, domain.name)
+
+ # Check that the fields have the right values.
+ # == Check for the creator == #
+
+ # 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)
+ self.assertContains(response, "Treat inspector")
+ self.assertContains(response, "meoward.jones@igorville.gov")
+ self.assertContains(response, "(555) 123 12345")
+
+ # Check for the field itself
+ self.assertContains(response, "Meoward Jones")
+
+ # == Check for the submitter == #
+ self.assertContains(response, "mayor@igorville.gov")
+
+ self.assertContains(response, "Admin Tester")
+ self.assertContains(response, "(555) 555 5556")
+ self.assertContains(response, "Testy2 Tester2")
+
+ # == Check for the authorizing_official == #
+ self.assertContains(response, "testy@town.com")
+ self.assertContains(response, "Chief Tester")
+ self.assertContains(response, "(555) 555 5555")
+
+ # Includes things like readonly fields
+ self.assertContains(response, "Testy Tester")
+
+ # == Test the other_employees field == #
+ self.assertContains(response, "testy2@town.com")
+ self.assertContains(response, "Another Tester")
+ self.assertContains(response, "(555) 555 5557")
+
+ # Test for the copy link
+ self.assertContains(response, "usa-button__clipboard")
+
@skip("TODO for another ticket. This test case is grabbing old db data.")
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
def test_extend_expiration_date_button(self, mock_date_today):
From f37eafdc53898db1362584dd7eb7910f4785b4a6 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 15:22:49 -0600
Subject: [PATCH 21/71] Update admin.py
---
src/registrar/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 0c720c8af..730cb5060 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1614,7 +1614,7 @@ class DomainAdmin(ListHeaderAdmin):
"""Custom changeform implementation to pass in context information"""
if extra_context is None:
extra_context = {}
- extra_context["original_object"] = self.model.objects.get(pk=object_id)
+
# Pass in what the an extended expiration date would be for the expiration date modal
if object_id is not None:
domain = Domain.objects.get(pk=object_id)
From 58c6d6d9bec3af63ae8a8818ce6df535e3460178 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 10 Apr 2024 15:31:31 -0600
Subject: [PATCH 22/71] Remove double import of value
---
src/registrar/utility/csv_export.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 6d254ce7b..61480f4a0 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -3,7 +3,7 @@ import logging
from datetime import datetime
from registrar.models.domain import Domain
from registrar.models.domain_invitation import DomainInvitation
-from django.db.models import Case, When, Count, Value
+from django.db.models import Case, When, Count
from registrar.models.domain_request import DomainRequest
from registrar.models.domain_information import DomainInformation
from django.utils import timezone
From 8714c1c4d6a71df889f1c1c3e58223dd8177c284 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 11 Apr 2024 08:08:17 -0600
Subject: [PATCH 23/71] Update docs/operations/data_migration.md
Co-authored-by: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com>
---
docs/operations/data_migration.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md
index e40a1ee50..59c2ad52c 100644
--- a/docs/operations/data_migration.md
+++ b/docs/operations/data_migration.md
@@ -588,7 +588,7 @@ Example: `cf ssh getgov-za`
| 1 | **debug** | Increases logging detail. Defaults to False. |
-## Populate First Ready
+## Populate Organization type
This section outlines how to run the `populate_organization_type` script.
### Running on sandboxes
From c9e3948b04ccfc50e12c2e28adbfa4aee9d807d7 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 11 Apr 2024 09:13:56 -0600
Subject: [PATCH 24/71] PR suggestions
---
docs/operations/data_migration.md | 31 +++++++--
.../commands/populate_organization_type.py | 64 +++++++++++++------
2 files changed, 70 insertions(+), 25 deletions(-)
diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md
index 59c2ad52c..4bfe37174 100644
--- a/docs/operations/data_migration.md
+++ b/docs/operations/data_migration.md
@@ -589,13 +589,22 @@ Example: `cf ssh getgov-za`
## Populate Organization type
-This section outlines how to run the `populate_organization_type` script.
+This section outlines how to run the `populate_organization_type` script.
+The script is used to update the organization_type field on DomainRequest and DomainInformation when it is None.
+That data are synthesized from the generic_org_type field and the is_election_board field
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
+#### Step 2: Get the domain_election_board file
+The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
+After downloading this file, place it in `src/migrationdata`
+
+#### Step 2: Upload the domain_election_board file to your sandbox
+Follow [Step 1: Transfer data to sandboxes](#step-1-transfer-data-to-sandboxes) and [Step 2: Transfer uploaded files to the getgov directory](#step-2-transfer-uploaded-files-to-the-getgov-directory) from the [Set Up Migrations on Sandbox](#set-up-migrations-on-sandbox) portion of this doc.
+
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
@@ -605,9 +614,9 @@ Example: `cf ssh getgov-za`
```/tmp/lifecycle/shell```
#### Step 4: Running the script
-```./manage.py populate_organization_type {domain_election_office_filename} --debug```
+```./manage.py populate_organization_type {domain_election_board_filename} --debug```
-- The domain_election_office_filename file must adhere to this format:
+- The domain_election_board_filename file must adhere to this format:
- example.gov\
example2.gov\
example3.gov
@@ -616,17 +625,25 @@ Example:
`./manage.py populate_organization_type migrationdata/election-domains.csv --debug`
### Running locally
-```docker-compose exec app ./manage.py populate_organization_type {domain_election_office_filename} --debug```
+
+#### Step 1: Get the domain_election_board file
+The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
+After downloading this file, place it in `src/migrationdata`
+
+
+#### Step 2: Running the script
+```docker-compose exec app ./manage.py populate_organization_type {domain_election_board_filename} --debug```
Example (assuming that this is being ran from src/):
`docker-compose exec app ./manage.py populate_organization_type migrationdata/election-domains.csv --debug`
-##### Required parameters
+
+### Required parameters
| | Parameter | Description |
|:-:|:------------------------------------|:-------------------------------------------------------------------|
-| 1 | **domain_election_office_filename** | A file containing every domain that is an election office.
+| 1 | **domain_election_board_filename** | A file containing every domain that is an election office.
-##### Optional parameters
+### Optional parameters
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **debug** | Increases logging detail. Defaults to False.
\ No newline at end of file
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index a0a1c8633..67fc9397e 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -11,7 +11,11 @@ logger = logging.getLogger(__name__)
class Command(BaseCommand):
- help = "Loops through each valid DomainInformation and DomainRequest object and updates its organization_type value"
+ help = (
+ "Loops through each valid DomainInformation and DomainRequest object and updates its organization_type value. "
+ "A valid DomainInformation/DomainRequest in this sense is one that has the value None for organization_type. "
+ "In other words, we populate the organization_type field if it is not already populated."
+ )
def __init__(self):
super().__init__()
@@ -26,34 +30,28 @@ class Command(BaseCommand):
self.di_skipped: List[DomainInformation] = []
# Define a global variable for all domains with election offices
- self.domains_with_election_offices_set = set()
+ self.domains_with_election_boards_set = set()
def add_arguments(self, parser):
"""Adds command line arguments"""
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
parser.add_argument(
- "domain_election_office_filename",
+ "domain_election_board_filename",
help=("A file that contains" " all the domains that are election offices."),
)
- def handle(self, domain_election_office_filename, **kwargs):
+ def handle(self, domain_election_board_filename, **kwargs):
"""Loops through each valid Domain object and updates its first_created value"""
debug = kwargs.get("debug")
# Check if the provided file path is valid
- if not os.path.isfile(domain_election_office_filename):
- raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_office_filename}'")
+ if not os.path.isfile(domain_election_board_filename):
+ raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_board_filename}'")
- with open(domain_election_office_filename, "r") as file:
- for line in file:
- # Remove any leading/trailing whitespace
- domain = line.strip()
- if domain not in self.domains_with_election_offices_set:
- self.domains_with_election_offices_set.add(domain)
+ # Read the election office csv
+ self.read_election_board_file(domain_election_board_filename)
- domain_requests = DomainRequest.objects.filter(
- organization_type__isnull=True, requested_domain__name__isnull=False
- )
+ domain_requests = DomainRequest.objects.filter(organization_type__isnull=True)
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
@@ -88,12 +86,37 @@ class Command(BaseCommand):
self.update_domain_informations(domain_infos, debug)
+ def read_election_board_file(self, domain_election_board_filename):
+ """
+ Reads the election board file and adds each parsed domain to self.domains_with_election_boards_set.
+ As previously implied, this file contains information about Domains which have election boards.
+
+ The file must adhere to this format:
+ ```
+ domain1.gov
+ domain2.gov
+ domain3.gov
+ ```
+ (and so on)
+ """
+ with open(domain_election_board_filename, "r") as file:
+ for line in file:
+ # Remove any leading/trailing whitespace
+ domain = line.strip()
+ if domain not in self.domains_with_election_boards_set:
+ self.domains_with_election_boards_set.add(domain)
+
def update_domain_requests(self, domain_requests, debug):
+ """
+ Updates the organization_type for a list of DomainRequest objects using the `sync_organization_type` function.
+ Results are then logged.
+ Debug mode provides detailed logging.
+ """
for request in domain_requests:
try:
if request.generic_org_type is not None:
domain_name = request.requested_domain.name
- request.is_election_board = domain_name in self.domains_with_election_offices_set
+ request.is_election_board = domain_name in self.domains_with_election_boards_set
request = self.sync_organization_type(DomainRequest, request)
self.request_to_update.append(request)
@@ -120,11 +143,16 @@ class Command(BaseCommand):
)
def update_domain_informations(self, domain_informations, debug):
+ """
+ Updates the organization_type for a list of DomainInformation objects using the `sync_organization_type` function.
+ Results are then logged.
+ Debug mode provides detailed logging.
+ """
for info in domain_informations:
try:
if info.generic_org_type is not None:
domain_name = info.domain.name
- info.is_election_board = domain_name in self.domains_with_election_offices_set
+ info.is_election_board = domain_name in self.domains_with_election_boards_set
info = self.sync_organization_type(DomainInformation, info)
self.di_to_update.append(info)
if debug:
@@ -168,7 +196,7 @@ class Command(BaseCommand):
election_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_election_to_org_generic()
# Manages the "organization_type" variable and keeps in sync with
- # "is_election_office" and "generic_organization_type"
+ # "is_election_board" and "generic_organization_type"
org_type_helper = CreateOrUpdateOrganizationTypeHelper(
sender=sender,
instance=instance,
From 09c0ad59cd388cfda86a5b90e18768c6bf7f233c Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 11 Apr 2024 09:25:08 -0600
Subject: [PATCH 25/71] Linting
---
.../management/commands/populate_organization_type.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index 67fc9397e..8428beb82 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -144,9 +144,9 @@ class Command(BaseCommand):
def update_domain_informations(self, domain_informations, debug):
"""
- Updates the organization_type for a list of DomainInformation objects using the `sync_organization_type` function.
+ Updates the organization_type for a list of DomainInformation objects
+ using the `sync_organization_type` function.
Results are then logged.
- Debug mode provides detailed logging.
"""
for info in domain_informations:
try:
From 4d043e586f8ca035bb3be148269054a02a3e746b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 11 Apr 2024 11:16:42 -0600
Subject: [PATCH 26/71] Change file name
---
src/registrar/admin.py | 10 +++++++++-
src/registrar/templates/admin/stacked.html | 3 ++-
.../django/admin/domain_change_form.html | 16 +++++++++++++++-
.../admin/includes/detail_table_fieldset.html | 1 -
...form.html => domain_info_inline_stacked.html} | 0
5 files changed, 26 insertions(+), 4 deletions(-)
rename src/registrar/templates/django/admin/includes/{domain_information_inline_change_form.html => domain_info_inline_stacked.html} (100%)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 730cb5060..83054b7b9 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1,6 +1,7 @@
from datetime import date
import logging
import copy
+
from django import forms
from django.db.models import Value, CharField, Q
from django.db.models.functions import Concat, Coalesce
@@ -1436,9 +1437,16 @@ class DomainInformationInline(admin.StackedInline):
We had issues inheriting from both StackedInline
and the source DomainInformationAdmin since these
classes conflict, so we'll just pull what we need
- from DomainInformationAdmin"""
+ from DomainInformationAdmin
+
+ Note that `template` cannot be set through this function,
+ due to how admin.StackedInline behaves.
+
+ See `domain_change_form.html` for more information.
+ """
form = DomainInformationInlineForm
+
model = models.DomainInformation
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
diff --git a/src/registrar/templates/admin/stacked.html b/src/registrar/templates/admin/stacked.html
index 8eac01864..5ca9324df 100644
--- a/src/registrar/templates/admin/stacked.html
+++ b/src/registrar/templates/admin/stacked.html
@@ -36,6 +36,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
{% block fieldset %}
{% include "admin/includes/fieldset.html" %}
{% endblock fieldset%}
+ {# End of .gov override #}
{% endfor %}
{% if inline_admin_form.needs_explicit_pk_field %}
@@ -48,4 +49,4 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
{% endfor %}
-
\ No newline at end of file
+
diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html
index 8d9f59675..d1767facd 100644
--- a/src/registrar/templates/django/admin/domain_change_form.html
+++ b/src/registrar/templates/django/admin/domain_change_form.html
@@ -36,9 +36,23 @@
{{ block.super }}
{% endblock %}
+{% comment %}
+The custom inline definition MUST be passed in this way.
+This is because we can't pass in additional context information from this scope
+unless by overriding a bunch of base formset functions.
+In this version of Django, inlines are handled much differently on the backend when there isn't a clear
+reason as to why. It appears to be that they simply haven't been updated for a while.
+
+Alternatively, we could create a second "duplicate" detail_table_fieldset.html file (or many if statements),
+but we lose out on centralizing all this logic inside of one file. The tradeoff here seems to be between
+code duplication vs not overriding the default.
+
+As a consequence, this means that we can't override the template on this inline by
+passing in template="path/to/file". We get much more control this way.
+{% endcomment %}
{% block inline_field_sets %}
{% for inline_admin_formset in inline_admin_formsets %}
- {% include "django/admin/includes/domain_information_inline_change_form.html" with original_object=original.domain_info%}
+ {% include "django/admin/includes/domain_info_inline_stacked.html" with original_object=original.domain_info%}
{% endfor %}
{% endblock %}
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 9a89ec510..4b0b36391 100644
--- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
+++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
@@ -94,7 +94,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{{ contact.title }}
{{ contact.email }}
-
{{ contact.phone }}
diff --git a/src/registrar/templates/django/admin/includes/domain_information_inline_change_form.html b/src/registrar/templates/django/admin/includes/domain_info_inline_stacked.html
similarity index 100%
rename from src/registrar/templates/django/admin/includes/domain_information_inline_change_form.html
rename to src/registrar/templates/django/admin/includes/domain_info_inline_stacked.html
From c4b772df6edc6d4000353b979c6f510894ff5f4f Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 12 Apr 2024 09:01:20 -0600
Subject: [PATCH 27/71] Simplify how template is dealt with
---
src/registrar/admin.py | 14 ++++++-------
.../django/admin/domain_change_form.html | 20 -------------------
2 files changed, 6 insertions(+), 28 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index a06588c9e..264257b35 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1438,15 +1438,10 @@ class DomainInformationInline(admin.StackedInline):
and the source DomainInformationAdmin since these
classes conflict, so we'll just pull what we need
from DomainInformationAdmin
-
- Note that `template` cannot be set through this function,
- due to how admin.StackedInline behaves.
-
- See `domain_change_form.html` for more information.
"""
form = DomainInformationInlineForm
-
+ template = "django/admin/includes/domain_info_inline_stacked.html"
model = models.DomainInformation
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
@@ -1623,11 +1618,14 @@ class DomainAdmin(ListHeaderAdmin):
if extra_context is None:
extra_context = {}
- # Pass in what the an extended expiration date would be for the expiration date modal
if object_id is not None:
domain = Domain.objects.get(pk=object_id)
- years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
+ # Use in the custom contact view
+ extra_context["original_object"] = domain.domain_info
+
+ # Pass in what the an extended expiration date would be for the expiration date modal
+ years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
try:
curr_exp_date = domain.registry_expiration_date
except KeyError:
diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html
index d1767facd..44fe6851b 100644
--- a/src/registrar/templates/django/admin/domain_change_form.html
+++ b/src/registrar/templates/django/admin/domain_change_form.html
@@ -36,26 +36,6 @@
{{ block.super }}
{% endblock %}
-{% comment %}
-The custom inline definition MUST be passed in this way.
-This is because we can't pass in additional context information from this scope
-unless by overriding a bunch of base formset functions.
-In this version of Django, inlines are handled much differently on the backend when there isn't a clear
-reason as to why. It appears to be that they simply haven't been updated for a while.
-
-Alternatively, we could create a second "duplicate" detail_table_fieldset.html file (or many if statements),
-but we lose out on centralizing all this logic inside of one file. The tradeoff here seems to be between
-code duplication vs not overriding the default.
-
-As a consequence, this means that we can't override the template on this inline by
-passing in template="path/to/file". We get much more control this way.
-{% endcomment %}
-{% block inline_field_sets %}
- {% for inline_admin_formset in inline_admin_formsets %}
- {% include "django/admin/includes/domain_info_inline_stacked.html" with original_object=original.domain_info%}
- {% endfor %}
-{% endblock %}
-
{% block submit_buttons_bottom %}
{% comment %}
Modals behave very weirdly in django admin.
From 9e7f8785490fb0a5c5d0c82e1cdd06fb6127998d Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Fri, 12 Apr 2024 15:24:50 -0400
Subject: [PATCH 28/71] allow clanking of first and or second nameserver if
enough entries
---
src/registrar/forms/domain.py | 36 +++++++++++---
src/registrar/tests/test_views_domain.py | 62 ++++++++++++++++++++++++
2 files changed, 91 insertions(+), 7 deletions(-)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 7b0ac2956..5b70d3e9b 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -83,25 +83,34 @@ class DomainNameserverForm(forms.Form):
# after clean_fields. it is used to determine form level errors.
# is_valid is typically called from view during a post
cleaned_data = super().clean()
+
self.clean_empty_strings(cleaned_data)
+
server = cleaned_data.get("server", "")
- # remove ANY spaces in the server field
- server = server.replace(" ", "")
- # lowercase the server
- server = server.lower()
+ server = server.replace(" ", "").lower()
cleaned_data["server"] = server
- ip = cleaned_data.get("ip", None)
- # remove ANY spaces in the ip field
+
+ ip = cleaned_data.get("ip", "")
ip = ip.replace(" ", "")
cleaned_data["ip"] = ip
+
domain = cleaned_data.get("domain", "")
ip_list = self.extract_ip_list(ip)
- # validate if the form has a server or an ip
+ # Capture the server_value
+ server_value = self.cleaned_data["server"]
+
+ # Validate if the form has a server or an ip
if (ip and ip_list) or server:
self.validate_nameserver_ip_combo(domain, server, ip_list)
+ # Re-set the server value:
+ # add_error which is called on validate_nameserver_ip_combo will clean-up (delete) any invalid data.
+ # We need that data because we need to know the total server entries (even if invalid) in the formset
+ # clean method where we determine whether a blank first and/or second entry should throw a required error.
+ self.cleaned_data["server"] = server_value
+
return cleaned_data
def clean_empty_strings(self, cleaned_data):
@@ -149,6 +158,19 @@ class BaseNameserverFormset(forms.BaseFormSet):
"""
Check for duplicate entries in the formset.
"""
+
+ # Check if there are at least two valid servers
+ valid_servers_count = sum(
+ 1 for form in self.forms if form.cleaned_data.get("server") and form.cleaned_data.get("server").strip()
+ )
+ if valid_servers_count >= 2:
+ # If there are, remove the "At least two name servers are required" error from each form
+ # This will allow for successful submissions when the first or second entries are blanked
+ # but there are enough entries total
+ for form in self.forms:
+ if form.errors.get("server") == ["At least two name servers are required."]:
+ form.errors.pop("server")
+
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own
return
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 064c5efdb..ba229fc10 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -974,6 +974,68 @@ class TestDomainNameservers(TestDomainOverview):
page = result.follow()
self.assertContains(page, "The name servers for this domain have been updated")
+ def test_domain_nameservers_can_blank_out_first_or_second_one_if_enough_entries(self):
+ """Nameserver form submits successfully with 2 valid inputs, even if the first or
+ second entries are blanked out.
+
+ Uses self.app WebTest because we need to interact with forms.
+ """
+
+ nameserver1 = ""
+ nameserver2 = "ns2.igorville.gov"
+ nameserver3 = "ns3.igorville.gov"
+ valid_ip = ""
+ valid_ip_2 = "128.0.0.2"
+ valid_ip_3 = "128.0.0.3"
+ nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ nameservers_page.form["form-0-server"] = nameserver1
+ nameservers_page.form["form-0-ip"] = valid_ip
+ nameservers_page.form["form-1-server"] = nameserver2
+ nameservers_page.form["form-1-ip"] = valid_ip_2
+ nameservers_page.form["form-2-server"] = nameserver3
+ nameservers_page.form["form-2-ip"] = valid_ip_3
+ with less_console_noise(): # swallow log warning message
+ result = nameservers_page.form.submit()
+
+ # form submission was a successful post, response should be a 302
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(
+ result["Location"],
+ reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
+ )
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ nameservers_page = result.follow()
+ self.assertContains(nameservers_page, "The name servers for this domain have been updated")
+
+ nameserver1 = "ns1.igorville.gov"
+ nameserver2 = ""
+ nameserver3 = "ns3.igorville.gov"
+ valid_ip = "128.0.0.1"
+ valid_ip_2 = ""
+ valid_ip_3 = "128.0.0.3"
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ nameservers_page.form["form-0-server"] = nameserver1
+ nameservers_page.form["form-0-ip"] = valid_ip
+ nameservers_page.form["form-1-server"] = nameserver2
+ nameservers_page.form["form-1-ip"] = valid_ip_2
+ nameservers_page.form["form-2-server"] = nameserver3
+ nameservers_page.form["form-2-ip"] = valid_ip_3
+ with less_console_noise(): # swallow log warning message
+ result = nameservers_page.form.submit()
+
+ # form submission was a successful post, response should be a 302
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(
+ result["Location"],
+ reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
+ )
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ nameservers_page = result.follow()
+ self.assertContains(nameservers_page, "The name servers for this domain have been updated")
+
def test_domain_nameservers_form_invalid(self):
"""Nameserver form does not submit with invalid data.
From 0a17f396cb534011518b12f57504bc0c5ea040a5 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Fri, 12 Apr 2024 15:44:40 -0400
Subject: [PATCH 29/71] UX bug fixes
---
src/registrar/assets/js/get-gov.js | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index 587b95305..b2ed71bea 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -530,7 +530,7 @@ function hideDeletedForms() {
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
// The Nameservers formset features 2 required and 11 optionals
if (isNameserversForm) {
- cloneIndex = 2;
+ // cloneIndex = 2;
formLabel = "Name server";
// DNSSEC: DS Data
} else if (isDsDataForm) {
@@ -766,3 +766,21 @@ function toggleTwoDomElements(ele1, ele2, index) {
}
})();
+/**
+ * An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
+ *
+ */
+(function otherContactsFormListener() {
+ let isNameserversForm = document.querySelector(".nameservers-form");
+ if (isNameserversForm) {
+ let forms = document.querySelectorAll(".repeatable-form");
+ if (forms.length < 3) {
+ // Hide the delete buttons on the 2 nameservers
+ forms.forEach((form, index) => {
+ Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
+ deleteButton.setAttribute("disabled", "true");
+ });
+ });
+ }
+ }
+})();
From 28177030210adbfd40d2ec25e9691c3e4b83fa95 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Fri, 12 Apr 2024 15:46:11 -0400
Subject: [PATCH 30/71] update IIFE definition
---
src/registrar/assets/js/get-gov.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index b2ed71bea..f2771e51c 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -767,10 +767,10 @@ function toggleTwoDomElements(ele1, ele2, index) {
})();
/**
- * An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
+ * An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
*
*/
-(function otherContactsFormListener() {
+(function nameserversFormListener() {
let isNameserversForm = document.querySelector(".nameservers-form");
if (isNameserversForm) {
let forms = document.querySelectorAll(".repeatable-form");
From 85bc9c272ede537a52ff283a039dc1b349959223 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Fri, 12 Apr 2024 15:47:47 -0400
Subject: [PATCH 31/71] cleanup
---
src/registrar/assets/js/get-gov.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index f2771e51c..b4c41ecf1 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -776,7 +776,7 @@ function toggleTwoDomElements(ele1, ele2, index) {
let forms = document.querySelectorAll(".repeatable-form");
if (forms.length < 3) {
// Hide the delete buttons on the 2 nameservers
- forms.forEach((form, index) => {
+ forms.forEach((form) => {
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
From c22438d2ad360a00c2a2542975e44a279b77d12b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 12 Apr 2024 13:56:55 -0600
Subject: [PATCH 32/71] Adjust styling
---
src/registrar/assets/sass/_theme/_admin.scss | 3 +++
.../templates/django/admin/domain_request_change_form.html | 4 ++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index 4b69dc8e3..40ab682be 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -495,6 +495,8 @@ address.dja-address-contact-list {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
+ // This has to be set here due to style overrides
+ font-size: medium;
}
}
@@ -505,6 +507,7 @@ address.dja-address-contact-list {
@media screen and (min-width:768px) {
.visible-768 {
display: block;
+ padding-top: 0;
}
}
diff --git a/src/registrar/templates/django/admin/domain_request_change_form.html b/src/registrar/templates/django/admin/domain_request_change_form.html
index 3b4fa7283..1640f0b68 100644
--- a/src/registrar/templates/django/admin/domain_request_change_form.html
+++ b/src/registrar/templates/django/admin/domain_request_change_form.html
@@ -116,8 +116,8 @@
-
{{ block.super }}
From 34e2e82a0c7c00c4d3d71567647c452514a116cb Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 12 Apr 2024 14:34:48 -0600
Subject: [PATCH 33/71] Update test_admin.py
---
src/registrar/tests/test_admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index bf54efe60..45c37ee23 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1538,7 +1538,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Since we're using client to mock the request, we can only test against
# non-interpolated values
- expected_content = "Requested domain:"
+ expected_content = "Requested domain:"
expected_content2 = ''
expected_content3 = '
'
self.assertContains(request, expected_content)
From da149e479225f09cc3eeb59bcade74dbd2ff25dd Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 12 Apr 2024 14:38:48 -0600
Subject: [PATCH 34/71] Add padding
---
src/registrar/assets/sass/_theme/_admin.scss | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index 40ab682be..ea49d7b8e 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -497,6 +497,7 @@ address.dja-address-contact-list {
text-overflow: ellipsis;
// This has to be set here due to style overrides
font-size: medium;
+ padding-top: 3px;
}
}
From 1b62ad558345ab26361c3227236f1fd3e8681add Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 12 Apr 2024 14:46:33 -0600
Subject: [PATCH 35/71] Fix test (pt.2)
---
src/registrar/tests/test_admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 45c37ee23..a9f9ab557 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1509,7 +1509,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Since we're using client to mock the request, we can only test against
# non-interpolated values
- expected_content = "Requested domain:"
+ expected_content = "Requested domain:"
expected_content2 = ''
expected_content3 = '
'
not_expected_content = "submit-row-wrapper--analyst-view>"
From 27ef823b93d7cd7d3c60e21ea859fca11e21b0fc Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Fri, 12 Apr 2024 17:56:57 -0400
Subject: [PATCH 36/71] handle edge case of 0 and 1 empty and duplicate error
---
src/registrar/forms/domain.py | 4 +-
src/registrar/tests/common.py | 13 ++++++
src/registrar/tests/test_views_domain.py | 55 +++++++++++++++++++++++-
3 files changed, 68 insertions(+), 4 deletions(-)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 5b70d3e9b..165d32fd8 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -178,10 +178,10 @@ class BaseNameserverFormset(forms.BaseFormSet):
data = []
duplicates = []
- for form in self.forms:
+ for index, form in enumerate(self.forms):
if form.cleaned_data:
value = form.cleaned_data["server"]
- if value in data:
+ if value in data and not (form.cleaned_data.get("server", "").strip() == '' and index == 1):
form.add_error(
"server",
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index a0b0e774f..4b9461b65 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -1152,6 +1152,18 @@ class MockEppLib(TestCase):
],
)
+ infoDomainFourHosts = fakedEppObject(
+ "my-nameserver.gov",
+ cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
+ contacts=[],
+ hosts=[
+ "ns1.my-nameserver-1.com",
+ "ns1.my-nameserver-2.com",
+ "ns1.cats-are-superior3.com",
+ "ns1.explosive-chicken-nuggets.com",
+ ],
+ )
+
infoDomainNoHost = fakedEppObject(
"my-nameserver.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
@@ -1475,6 +1487,7 @@ class MockEppLib(TestCase):
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
+ "fournameserversDomain.gov": (self.infoDomainFourHosts, None),
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index ba229fc10..eeab00ae3 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -5,7 +5,7 @@ from django.conf import settings
from django.urls import reverse
from django.contrib.auth import get_user_model
-from .common import MockSESClient, create_user # type: ignore
+from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@@ -727,7 +727,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertContains(home_page, self.domain.name)
-class TestDomainNameservers(TestDomainOverview):
+class TestDomainNameservers(TestDomainOverview, MockEppLib):
def test_domain_nameservers(self):
"""Can load domain's nameservers page."""
page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
@@ -1036,6 +1036,57 @@ class TestDomainNameservers(TestDomainOverview):
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
+ @skip('wip')
+ def test_domain_nameservers_can_blank_out_first_and_second_one_if_enough_entries(self):
+ """Nameserver form submits successfully with 2 valid inputs, even if the first and
+ second entries are blanked out.
+
+ Uses self.app WebTest because we need to interact with forms.
+ """
+
+ # Submit a formset with 3 valid forms
+ # The returned page (after the redirect) will have 4 forms that we can use to test
+ # our use case.
+
+
+ infoDomainFourHosts, _ = Domain.objects.get_or_create(name="fournameserversDomain.gov", state=Domain.State.READY)
+ UserDomainRole.objects.get_or_create(user=self.user, domain=infoDomainFourHosts)
+ DomainInformation.objects.get_or_create(creator=self.user, domain=infoDomainFourHosts)
+ self.client.force_login(self.user)
+
+ nameserver1 = ""
+ nameserver2 = ""
+ nameserver3 = "ns3.igorville.gov"
+ nameserver4 = "ns4.igorville.gov"
+ valid_ip = ""
+ valid_ip_2 = ""
+ valid_ip_3 = "128.0.0.3"
+ valid_ip_4 = "128.0.0.4"
+ nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": infoDomainFourHosts.id}))
+ print(nameservers_page.content.decode('utf-8'))
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ nameservers_page.form["form-0-server"] = nameserver1
+ nameservers_page.form["form-0-ip"] = valid_ip
+ nameservers_page.form["form-1-server"] = nameserver2
+ nameservers_page.form["form-1-ip"] = valid_ip_2
+ nameservers_page.form["form-2-server"] = nameserver3
+ nameservers_page.form["form-2-ip"] = valid_ip_3
+ nameservers_page.form["form-3-server"] = nameserver4
+ nameservers_page.form["form-3-ip"] = valid_ip_4
+ with less_console_noise(): # swallow log warning message
+ result = nameservers_page.form.submit()
+
+ # form submission was a successful post, response should be a 302
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(
+ result["Location"],
+ reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
+ )
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ nameservers_page = result.follow()
+ self.assertContains(nameservers_page, "The name servers for this domain have been updated")
+
def test_domain_nameservers_form_invalid(self):
"""Nameserver form does not submit with invalid data.
From d05f016632252e0d1e97b7c27fdfeb5e8bb5edb7 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Fri, 12 Apr 2024 18:00:06 -0400
Subject: [PATCH 37/71] comment
---
src/registrar/forms/domain.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 165d32fd8..b7c37277c 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -181,6 +181,9 @@ class BaseNameserverFormset(forms.BaseFormSet):
for index, form in enumerate(self.forms):
if form.cleaned_data:
value = form.cleaned_data["server"]
+ # We need to make sure not to trigger the duplicate error in case the first and second nameservers are empty
+ # If there are enough records in the formset, that error is an unecessary blocker. If there aren't, the required
+ # error will block the submit.
if value in data and not (form.cleaned_data.get("server", "").strip() == '' and index == 1):
form.add_error(
"server",
From 2e86c167b9692b0e0f846f016f9bf2684320ef29 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Fri, 12 Apr 2024 21:40:41 -0400
Subject: [PATCH 38/71] Fix unit test for 2 empties
---
src/registrar/forms/domain.py | 8 ++---
src/registrar/tests/common.py | 10 ++++---
src/registrar/tests/test_views_domain.py | 38 ++++++++++++++----------
3 files changed, 32 insertions(+), 24 deletions(-)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index b7c37277c..8fc7a6497 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -181,10 +181,10 @@ class BaseNameserverFormset(forms.BaseFormSet):
for index, form in enumerate(self.forms):
if form.cleaned_data:
value = form.cleaned_data["server"]
- # We need to make sure not to trigger the duplicate error in case the first and second nameservers are empty
- # If there are enough records in the formset, that error is an unecessary blocker. If there aren't, the required
- # error will block the submit.
- if value in data and not (form.cleaned_data.get("server", "").strip() == '' and index == 1):
+ # We need to make sure not to trigger the duplicate error in case the first and second nameservers
+ # are empty. If there are enough records in the formset, that error is an unecessary blocker.
+ # If there aren't, the required error will block the submit.
+ if value in data and not (form.cleaned_data.get("server", "").strip() == "" and index == 1):
form.add_error(
"server",
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 4b9461b65..07dc08f8a 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -1153,7 +1153,7 @@ class MockEppLib(TestCase):
)
infoDomainFourHosts = fakedEppObject(
- "my-nameserver.gov",
+ "fournameserversDomain.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
contacts=[],
hosts=[
@@ -1464,7 +1464,9 @@ class MockEppLib(TestCase):
)
def mockInfoDomainCommands(self, _request, cleaned):
- request_name = getattr(_request, "name", None)
+ request_name = getattr(_request, "name", None).lower()
+
+ print(request_name)
# Define a dictionary to map request names to data and extension values
request_mappings = {
@@ -1486,8 +1488,8 @@ class MockEppLib(TestCase):
"nameserverwithip.gov": (self.infoDomainHasIP, None),
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),
- "threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
- "fournameserversDomain.gov": (self.infoDomainFourHosts, None),
+ "threenameserversdomain.gov": (self.infoDomainThreeHosts, None),
+ "fournameserversdomain.gov": (self.infoDomainFourHosts, None),
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index eeab00ae3..3a5ce7e7b 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -71,11 +71,14 @@ class TestWithDomainPermissions(TestWithUser):
# that inherit this setUp
self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov")
+ self.domain_with_four_nameservers, _ = Domain.objects.get_or_create(name="fournameserversDomain.gov")
+
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none)
+ DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_four_nameservers)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold)
@@ -98,6 +101,11 @@ class TestWithDomainPermissions(TestWithUser):
domain=self.domain_dnssec_none,
role=UserDomainRole.Roles.MANAGER,
)
+ UserDomainRole.objects.get_or_create(
+ user=self.user,
+ domain=self.domain_with_four_nameservers,
+ role=UserDomainRole.Roles.MANAGER,
+ )
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_with_ip,
@@ -1036,7 +1044,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
- @skip('wip')
def test_domain_nameservers_can_blank_out_first_and_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first and
second entries are blanked out.
@@ -1044,28 +1051,27 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
Uses self.app WebTest because we need to interact with forms.
"""
- # Submit a formset with 3 valid forms
- # The returned page (after the redirect) will have 4 forms that we can use to test
- # our use case.
-
-
- infoDomainFourHosts, _ = Domain.objects.get_or_create(name="fournameserversDomain.gov", state=Domain.State.READY)
- UserDomainRole.objects.get_or_create(user=self.user, domain=infoDomainFourHosts)
- DomainInformation.objects.get_or_create(creator=self.user, domain=infoDomainFourHosts)
- self.client.force_login(self.user)
-
+ # We need to start with a domain with 4 nameservers otherwise the formset in the test environment
+ # will only have 3 forms
nameserver1 = ""
nameserver2 = ""
nameserver3 = "ns3.igorville.gov"
nameserver4 = "ns4.igorville.gov"
valid_ip = ""
valid_ip_2 = ""
- valid_ip_3 = "128.0.0.3"
- valid_ip_4 = "128.0.0.4"
- nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": infoDomainFourHosts.id}))
- print(nameservers_page.content.decode('utf-8'))
+ valid_ip_3 = ""
+ valid_ip_4 = ""
+ nameservers_page = self.app.get(
+ reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id})
+ )
+
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+
+ # Minimal check to ensure the form is loaded correctly
+ self.assertEqual(nameservers_page.form["form-0-server"].value, "ns1.my-nameserver-1.com")
+ self.assertEqual(nameservers_page.form["form-3-server"].value, "ns1.explosive-chicken-nuggets.com")
+
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
@@ -1081,7 +1087,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
- reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
+ reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
From ffa7b59eb586d433d0aaa72a14be5e39e661949f Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Fri, 12 Apr 2024 21:52:16 -0400
Subject: [PATCH 39/71] cleanup
---
src/registrar/forms/domain.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 8fc7a6497..da1462bdb 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -99,7 +99,7 @@ class DomainNameserverForm(forms.Form):
ip_list = self.extract_ip_list(ip)
# Capture the server_value
- server_value = self.cleaned_data["server"]
+ server_value = self.cleaned_data.get("server")
# Validate if the form has a server or an ip
if (ip and ip_list) or server:
From 2ffeda3be7340e9294f9ece9f0cd2d3892a19281 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Mon, 15 Apr 2024 07:46:07 -0400
Subject: [PATCH 40/71] added column for Is user to Contact table in DJA
---
src/registrar/admin.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 05bfc06b6..5116888dd 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -663,6 +663,7 @@ class ContactAdmin(ListHeaderAdmin):
list_display = [
"contact",
"email",
+ "user_exists",
]
# this ordering effects the ordering of results
# in autocomplete_fields for user
@@ -679,6 +680,12 @@ class ContactAdmin(ListHeaderAdmin):
change_form_template = "django/admin/email_clipboard_change_form.html"
+ def user_exists(self, obj):
+ """Check if the Contact has a related User"""
+ return obj.user is not None
+ user_exists.boolean = True
+ user_exists.short_description = "Is user"
+
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
From 3909ba8212fdb9c7643af59f25d43d45c58defee Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Mon, 15 Apr 2024 07:53:24 -0400
Subject: [PATCH 41/71] formatted for readability
---
src/registrar/admin.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 5116888dd..b9114f6f5 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -683,6 +683,7 @@ class ContactAdmin(ListHeaderAdmin):
def user_exists(self, obj):
"""Check if the Contact has a related User"""
return obj.user is not None
+
user_exists.boolean = True
user_exists.short_description = "Is user"
From 77d2c998d3d62d15fdab6c581ef9ad049d7cb4b4 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Mon, 15 Apr 2024 07:57:34 -0400
Subject: [PATCH 42/71] satisfying linter
---
src/registrar/admin.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index b9114f6f5..fe0730d31 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -684,8 +684,8 @@ class ContactAdmin(ListHeaderAdmin):
"""Check if the Contact has a related User"""
return obj.user is not None
- user_exists.boolean = True
- user_exists.short_description = "Is user"
+ user_exists.boolean = True # type: ignore
+ user_exists.short_description = "Is user" # type: ignore
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
From 5673d5951ef197ee6ef8edd2c4811bf16cecf605 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Mon, 15 Apr 2024 08:50:13 -0400
Subject: [PATCH 43/71] default filter applied when clicking domain requests
from editing a domain request
---
src/registrar/admin.py | 27 +++++++++++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 05bfc06b6..efd8c29c9 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1435,12 +1435,35 @@ class DomainRequestAdmin(ListHeaderAdmin):
"""
Override changelist_view to set the selected value of status filter.
"""
+ # there are two conditions which should set the default selected filter:
+ # 1 - there are no query parameters in the request and the request is the
+ # initial request for this view
+ # 2 - there are no query parameters in the request and the referring url is
+ # the change view for a domain request
+ should_apply_default_filter = False
# use http_referer in order to distinguish between request as a link from another page
# and request as a removal of all filters
http_referer = request.META.get("HTTP_REFERER", "")
# if there are no query parameters in the request
- # and the request is the initial request for this view
- if not bool(request.GET) and request.path not in http_referer:
+ if not bool(request.GET):
+ # if the request is the initial request for this view
+ if request.path not in http_referer:
+ should_apply_default_filter = True
+ # elif the request is a referral from changelist view or from
+ # domain request change view
+ elif request.path in http_referer:
+ # find the index to determine the referring url after the path
+ index = http_referer.find(request.path)
+ # Check if there is a character following the path in http_referer
+ if index + len(request.path) < len(http_referer):
+ next_char = http_referer[index + len(request.path)]
+
+ # Check if the next character is a digit, if so, this indicates
+ # a change view for domain request
+ if next_char.isdigit():
+ should_apply_default_filter = True
+
+ if should_apply_default_filter:
# modify the GET of the request to set the selected filter
modified_get = copy.deepcopy(request.GET)
modified_get["status__in"] = "submitted,in review,action needed"
From df1dee65cb689351f78f730966b7d741ffc37079 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 15 Apr 2024 09:43:19 -0600
Subject: [PATCH 44/71] Fix bug
---
src/registrar/admin.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 25ac8a9d6..b7c8ddef6 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1664,8 +1664,9 @@ class DomainAdmin(ListHeaderAdmin):
if object_id is not None:
domain = Domain.objects.get(pk=object_id)
- # Use in the custom contact view
- extra_context["original_object"] = domain.domain_info
+ # Used in the custom contact view
+ if domain is not None and hasattr(domain, "domain_info"):
+ extra_context["original_object"] = domain.domain_info
# Pass in what the an extended expiration date would be for the expiration date modal
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
From 69c665e4fa893e6cb60f580ca1d32a3b49370ead Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 16 Apr 2024 11:12:17 -0400
Subject: [PATCH 45/71] made is_user column sortable
---
src/registrar/admin.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index fe0730d31..f17aaf1b2 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -686,6 +686,7 @@ class ContactAdmin(ListHeaderAdmin):
user_exists.boolean = True # type: ignore
user_exists.short_description = "Is user" # type: ignore
+ user_exists.admin_order_field = 'user' # type: ignore
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
From 72a816acdafc1f2f0e5f875d62dd8ac97fe2105d Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 16 Apr 2024 11:13:17 -0400
Subject: [PATCH 46/71] reformatted
---
src/registrar/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index f17aaf1b2..17486e983 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -686,7 +686,7 @@ class ContactAdmin(ListHeaderAdmin):
user_exists.boolean = True # type: ignore
user_exists.short_description = "Is user" # type: ignore
- user_exists.admin_order_field = 'user' # type: ignore
+ user_exists.admin_order_field = "user" # type: ignore
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
From ee961c9cd801c876e950bdca5bb4d7c90deb4b5f Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 16 Apr 2024 12:52:48 -0400
Subject: [PATCH 47/71] changed icon to text for column
---
src/registrar/admin.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 17486e983..b61b21f87 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -682,9 +682,8 @@ class ContactAdmin(ListHeaderAdmin):
def user_exists(self, obj):
"""Check if the Contact has a related User"""
- return obj.user is not None
+ return "Yes" if obj.user is not None else "No"
- user_exists.boolean = True # type: ignore
user_exists.short_description = "Is user" # type: ignore
user_exists.admin_order_field = "user" # type: ignore
From b1a772d1f28a2f2b6ef39861014cb707663f1172 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 16 Apr 2024 12:14:32 -0600
Subject: [PATCH 48/71] Remove comment
---
src/registrar/assets/sass/_theme/_admin.scss | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index ea49d7b8e..93b437f1d 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -495,7 +495,6 @@ address.dja-address-contact-list {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
- // This has to be set here due to style overrides
font-size: medium;
padding-top: 3px;
}
From 582f35bc07e8c7a5188b8c493a1cbe8970c88587 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 16 Apr 2024 15:24:56 -0600
Subject: [PATCH 49/71] PR suggestions
---
docs/operations/data_migration.md | 15 ++--
.../commands/populate_organization_type.py | 89 +++++++++++++------
src/registrar/models/domain_information.py | 2 +-
src/registrar/models/domain_request.py | 2 +-
.../tests/test_management_scripts.py | 49 ++++++++--
5 files changed, 110 insertions(+), 47 deletions(-)
diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md
index 4bfe37174..0846208de 100644
--- a/docs/operations/data_migration.md
+++ b/docs/operations/data_migration.md
@@ -591,7 +591,7 @@ Example: `cf ssh getgov-za`
## Populate Organization type
This section outlines how to run the `populate_organization_type` script.
The script is used to update the organization_type field on DomainRequest and DomainInformation when it is None.
-That data are synthesized from the generic_org_type field and the is_election_board field
+That data are synthesized from the generic_org_type field and the is_election_board field by concatenating " - Elections" on the end of generic_org_type string if is_elections_board is True.
### Running on sandboxes
@@ -614,7 +614,7 @@ Example: `cf ssh getgov-za`
```/tmp/lifecycle/shell```
#### Step 4: Running the script
-```./manage.py populate_organization_type {domain_election_board_filename} --debug```
+```./manage.py populate_organization_type {domain_election_board_filename}```
- The domain_election_board_filename file must adhere to this format:
- example.gov\
@@ -622,7 +622,7 @@ Example: `cf ssh getgov-za`
example3.gov
Example:
-`./manage.py populate_organization_type migrationdata/election-domains.csv --debug`
+`./manage.py populate_organization_type migrationdata/election-domains.csv`
### Running locally
@@ -632,18 +632,13 @@ After downloading this file, place it in `src/migrationdata`
#### Step 2: Running the script
-```docker-compose exec app ./manage.py populate_organization_type {domain_election_board_filename} --debug```
+```docker-compose exec app ./manage.py populate_organization_type {domain_election_board_filename}```
Example (assuming that this is being ran from src/):
-`docker-compose exec app ./manage.py populate_organization_type migrationdata/election-domains.csv --debug`
+`docker-compose exec app ./manage.py populate_organization_type migrationdata/election-domains.csv`
### Required parameters
| | Parameter | Description |
|:-:|:------------------------------------|:-------------------------------------------------------------------|
| 1 | **domain_election_board_filename** | A file containing every domain that is an election office.
-
-### Optional parameters
-| | Parameter | Description |
-|:-:|:-------------------------- |:----------------------------------------------------------------------------|
-| 1 | **debug** | Increases logging detail. Defaults to False.
\ No newline at end of file
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index 8428beb82..f2d9c062d 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -34,7 +34,6 @@ class Command(BaseCommand):
def add_arguments(self, parser):
"""Adds command line arguments"""
- parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
parser.add_argument(
"domain_election_board_filename",
help=("A file that contains" " all the domains that are election offices."),
@@ -42,7 +41,6 @@ class Command(BaseCommand):
def handle(self, domain_election_board_filename, **kwargs):
"""Loops through each valid Domain object and updates its first_created value"""
- debug = kwargs.get("debug")
# Check if the provided file path is valid
if not os.path.isfile(domain_election_board_filename):
@@ -66,7 +64,7 @@ class Command(BaseCommand):
)
logger.info("Updating DomainRequest(s)...")
- self.update_domain_requests(domain_requests, debug)
+ self.update_domain_requests(domain_requests)
# We should actually be targeting all fields with no value for organization type,
# but do have a value for generic_org_type. This is because there is data that we can infer.
@@ -84,7 +82,7 @@ class Command(BaseCommand):
)
logger.info("Updating DomainInformation(s)...")
- self.update_domain_informations(domain_infos, debug)
+ self.update_domain_informations(domain_infos)
def read_election_board_file(self, domain_election_board_filename):
"""
@@ -106,26 +104,37 @@ class Command(BaseCommand):
if domain not in self.domains_with_election_boards_set:
self.domains_with_election_boards_set.add(domain)
- def update_domain_requests(self, domain_requests, debug):
+ def update_domain_requests(self, domain_requests):
"""
Updates the organization_type for a list of DomainRequest objects using the `sync_organization_type` function.
Results are then logged.
- Debug mode provides detailed logging.
+
+ This function updates the following variables:
+ - self.request_to_update list is appended to if the field was updated successfully.
+ - self.request_skipped list is appended to if the field has `None` for `request.generic_org_type`.
+ - self.request_failed_to_update list is appended to if an exception is caught during update.
"""
for request in domain_requests:
try:
if request.generic_org_type is not None:
- domain_name = request.requested_domain.name
- request.is_election_board = domain_name in self.domains_with_election_boards_set
- request = self.sync_organization_type(DomainRequest, request)
- self.request_to_update.append(request)
+ domain_name = None
+ if (
+ request.requested_domain is not None and
+ request.requested_domain.name is not None
+ ):
+ domain_name = request.requested_domain.name
- if debug:
- logger.info(f"Updating {request} => {request.organization_type}")
+ request_is_approved = request.state == DomainRequest.DomainRequestStatus.APPROVED
+ if request_is_approved and domain_name is not None:
+ request.is_election_board = domain_name in self.domains_with_election_boards_set
+
+ self.sync_organization_type(DomainRequest, request)
+
+ self.request_to_update.append(request)
+ logger.info(f"Updating {request} => {request.organization_type}")
else:
self.request_skipped.append(request)
- if debug:
- logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
+ logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
except Exception as err:
self.request_failed_to_update.append(request)
logger.error(err)
@@ -139,28 +148,44 @@ class Command(BaseCommand):
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAINREQUEST ==============="
TerminalHelper.log_script_run_summary(
- self.request_to_update, self.request_failed_to_update, self.request_skipped, debug, log_header
+ self.request_to_update, self.request_failed_to_update, self.request_skipped, True, log_header
)
- def update_domain_informations(self, domain_informations, debug):
+ update_skipped_count = len(self.request_to_update)
+ if update_skipped_count > 0:
+ logger.warning(
+ f"""{TerminalColors.MAGENTA}
+ Note: Entries are skipped when generic_org_type is None
+ {TerminalColors.ENDC}
+ """
+ )
+
+ def update_domain_informations(self, domain_informations):
"""
Updates the organization_type for a list of DomainInformation objects
- using the `sync_organization_type` function.
+ and updates is_election_board if the domain is in the provided csv.
Results are then logged.
+
+ This function updates the following variables:
+ - self.di_to_update list is appended to if the field was updated successfully.
+ - self.di_skipped list is appended to if the field has `None` for `request.generic_org_type`.
+ - self.di_failed_to_update list is appended to if an exception is caught during update.
"""
for info in domain_informations:
try:
if info.generic_org_type is not None:
domain_name = info.domain.name
- info.is_election_board = domain_name in self.domains_with_election_boards_set
- info = self.sync_organization_type(DomainInformation, info)
+
+ if not info.is_election_board:
+ info.is_election_board = domain_name in self.domains_with_election_boards_set
+
+ self.sync_organization_type(DomainInformation, info)
+
self.di_to_update.append(info)
- if debug:
- logger.info(f"Updating {info} => {info.organization_type}")
+ logger.info(f"Updating {info} => {info.organization_type}")
else:
self.di_skipped.append(info)
- if debug:
- logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
+ logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
except Exception as err:
self.di_failed_to_update.append(info)
logger.error(err)
@@ -170,13 +195,22 @@ class Command(BaseCommand):
ScriptDataHelper.bulk_update_fields(
DomainInformation, self.di_to_update, ["organization_type", "is_election_board", "generic_org_type"]
)
-
+
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAININFORMATION ==============="
TerminalHelper.log_script_run_summary(
- self.di_to_update, self.di_failed_to_update, self.di_skipped, debug, log_header
+ self.di_to_update, self.di_failed_to_update, self.di_skipped, True, log_header
)
+ update_skipped_count = len(self.di_skipped)
+ if update_skipped_count > 0:
+ logger.warning(
+ f"""{TerminalColors.MAGENTA}
+ Note: Entries are skipped when generic_org_type is None
+ {TerminalColors.ENDC}
+ """
+ )
+
def sync_organization_type(self, sender, instance):
"""
Updates the organization_type (without saving) to match
@@ -187,7 +221,7 @@ class Command(BaseCommand):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
- # For any given organization type, return the "_election" variant.
+ # For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()
@@ -204,5 +238,4 @@ class Command(BaseCommand):
election_org_to_generic_org_map=election_org_map,
)
- instance = org_type_helper.create_or_update_organization_type()
- return instance
+ org_type_helper.create_or_update_organization_type()
diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py
index 6bdc6c00d..b02385d84 100644
--- a/src/registrar/models/domain_information.py
+++ b/src/registrar/models/domain_information.py
@@ -246,7 +246,7 @@ class DomainInformation(TimeStampedModel):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
- # For any given organization type, return the "_election" variant.
+ # For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 1b8a519a0..8fb4b78b9 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -675,7 +675,7 @@ class DomainRequest(TimeStampedModel):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
- # For any given organization type, return the "_election" variant.
+ # For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = self.OrgChoicesElectionOffice.get_org_generic_to_org_election()
diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py
index 26ec6fd1d..7b422e44b 100644
--- a/src/registrar/tests/test_management_scripts.py
+++ b/src/registrar/tests/test_management_scripts.py
@@ -107,9 +107,23 @@ class TestPopulateOrganizationType(MockEppLib):
expected_values: dict,
):
"""
- This is a a helper function that ensures that:
+ This is a helper function that tests the following conditions:
1. DomainRequest and DomainInformation (on given objects) are equivalent
2. That generic_org_type, is_election_board, and organization_type are equal to passed in values
+
+ Args:
+ domain_request (DomainRequest): The DomainRequest object to test
+
+ domain_info (DomainInformation): The DomainInformation object to test
+
+ expected_values (dict): Container for what we expect is_electionboard, generic_org_type,
+ and organization_type to be on DomainRequest and DomainInformation.
+ Example:
+ expected_values = {
+ "is_election_board": False,
+ "generic_org_type": DomainRequest.OrganizationChoices.CITY,
+ "organization_type": DomainRequest.OrgChoicesElectionOffice.CITY,
+ }
"""
# Test domain request
@@ -124,8 +138,23 @@ class TestPopulateOrganizationType(MockEppLib):
self.assertEqual(domain_info.is_election_board, expected_values["is_election_board"])
self.assertEqual(domain_info.organization_type, expected_values["organization_type"])
+ def do_nothing(self):
+ """Does nothing for mocking purposes"""
+ pass
+
def test_request_and_info_city_not_in_csv(self):
- """Tests what happens to a city domain that is not defined in the CSV"""
+ """
+ Tests what happens to a city domain that is not defined in the CSV.
+
+ Scenario: A domain request (of type city) is made that is not defined in the CSV file.
+ When a domain request is made for a city that is not listed in the CSV,
+ Then the `is_election_board` value should remain False,
+ and the `generic_org_type` and `organization_type` should both be `city`.
+
+ Expected Result: The `is_election_board` and `generic_org_type` attributes should be unchanged.
+ The `organization_type` field should now be `city`.
+ """
+
city_request = self.domain_request_2
city_info = self.domain_request_2
@@ -149,7 +178,17 @@ class TestPopulateOrganizationType(MockEppLib):
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
def test_request_and_info_federal(self):
- """Tests what happens to a federal domain after the script is run (should be unchanged)"""
+ """
+ Tests what happens to a federal domain after the script is run (should be unchanged).
+
+ Scenario: A domain request (of type federal) is processed after running the populate_organization_type script.
+ When a federal domain request is made,
+ Then the `is_election_board` value should remain None,
+ and the `generic_org_type` and `organization_type` fields should both be `federal`.
+
+ Expected Result: The `is_election_board` and `generic_org_type` attributes should be unchanged.
+ The `organization_type` field should now be `federal`.
+ """
federal_request = self.domain_request_1
federal_info = self.domain_info_1
@@ -172,10 +211,6 @@ class TestPopulateOrganizationType(MockEppLib):
# All values should be the same
self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
- def do_nothing(self):
- """Does nothing for mocking purposes"""
- pass
-
def test_request_and_info_tribal_add_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL - ELECTION
From 50ede0347670d1effc299ef184112b209e6ade2e Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 16 Apr 2024 15:32:33 -0600
Subject: [PATCH 50/71] Linting / fix unit test passing in debug
---
.../management/commands/populate_organization_type.py | 7 ++-----
src/registrar/tests/test_management_scripts.py | 4 ++--
2 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index f2d9c062d..cfa44d0a5 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -118,10 +118,7 @@ class Command(BaseCommand):
try:
if request.generic_org_type is not None:
domain_name = None
- if (
- request.requested_domain is not None and
- request.requested_domain.name is not None
- ):
+ if request.requested_domain is not None and request.requested_domain.name is not None:
domain_name = request.requested_domain.name
request_is_approved = request.state == DomainRequest.DomainRequestStatus.APPROVED
@@ -195,7 +192,7 @@ class Command(BaseCommand):
ScriptDataHelper.bulk_update_fields(
DomainInformation, self.di_to_update, ["organization_type", "is_election_board", "generic_org_type"]
)
-
+
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAININFORMATION ==============="
TerminalHelper.log_script_run_summary(
diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py
index 7b422e44b..cad9e0ebe 100644
--- a/src/registrar/tests/test_management_scripts.py
+++ b/src/registrar/tests/test_management_scripts.py
@@ -98,7 +98,7 @@ class TestPopulateOrganizationType(MockEppLib):
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
- call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv", debug=True)
+ call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv")
def assert_expected_org_values_on_request_and_info(
self,
@@ -118,7 +118,7 @@ class TestPopulateOrganizationType(MockEppLib):
expected_values (dict): Container for what we expect is_electionboard, generic_org_type,
and organization_type to be on DomainRequest and DomainInformation.
- Example:
+ Example:
expected_values = {
"is_election_board": False,
"generic_org_type": DomainRequest.OrganizationChoices.CITY,
From 40a64a1e868ef61dcce9745b756527ac14b15ceb Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 16 Apr 2024 15:51:10 -0600
Subject: [PATCH 51/71] Fix typo causing test fail
---
src/registrar/management/commands/populate_organization_type.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index cfa44d0a5..cef8e9433 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -121,7 +121,7 @@ class Command(BaseCommand):
if request.requested_domain is not None and request.requested_domain.name is not None:
domain_name = request.requested_domain.name
- request_is_approved = request.state == DomainRequest.DomainRequestStatus.APPROVED
+ request_is_approved = request.status == DomainRequest.DomainRequestStatus.APPROVED
if request_is_approved and domain_name is not None:
request.is_election_board = domain_name in self.domains_with_election_boards_set
From e4abe28b61be7d523b9e05485b6c4c1ed56f1b36 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 08:33:35 -0600
Subject: [PATCH 52/71] Hotfix
---
src/registrar/assets/sass/_theme/_admin.scss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index 93b437f1d..93b3e1581 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -496,7 +496,7 @@ address.dja-address-contact-list {
white-space: nowrap;
text-overflow: ellipsis;
font-size: medium;
- padding-top: 3px;
+ padding-top: 3px !important;
}
}
From 4b90ff833c1a1686229d2e15bc880f8bd52ccb00 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 09:26:05 -0600
Subject: [PATCH 53/71] Fix unit test
---
.../commands/populate_organization_type.py | 5 ++-
.../models/utility/generic_helper.py | 33 +++++++++++--------
.../tests/test_management_scripts.py | 19 ++++++-----
3 files changed, 33 insertions(+), 24 deletions(-)
diff --git a/src/registrar/management/commands/populate_organization_type.py b/src/registrar/management/commands/populate_organization_type.py
index cef8e9433..a7dd98b24 100644
--- a/src/registrar/management/commands/populate_organization_type.py
+++ b/src/registrar/management/commands/populate_organization_type.py
@@ -122,11 +122,10 @@ class Command(BaseCommand):
domain_name = request.requested_domain.name
request_is_approved = request.status == DomainRequest.DomainRequestStatus.APPROVED
- if request_is_approved and domain_name is not None:
+ if request_is_approved and domain_name is not None and not request.is_election_board:
request.is_election_board = domain_name in self.domains_with_election_boards_set
self.sync_organization_type(DomainRequest, request)
-
self.request_to_update.append(request)
logger.info(f"Updating {request} => {request.organization_type}")
else:
@@ -235,4 +234,4 @@ class Command(BaseCommand):
election_org_to_generic_org_map=election_org_map,
)
- org_type_helper.create_or_update_organization_type()
+ org_type_helper.create_or_update_organization_type(force_update=True)
diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py
index 32f767ede..77351b11c 100644
--- a/src/registrar/models/utility/generic_helper.py
+++ b/src/registrar/models/utility/generic_helper.py
@@ -49,7 +49,7 @@ class CreateOrUpdateOrganizationTypeHelper:
self.generic_org_to_org_map = generic_org_to_org_map
self.election_org_to_generic_org_map = election_org_to_generic_org_map
- def create_or_update_organization_type(self):
+ def create_or_update_organization_type(self, force_update = False):
"""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
@@ -59,6 +59,11 @@ class CreateOrUpdateOrganizationTypeHelper:
one of the excluded types (FEDERAL, INTERSTATE, SCHOOL_DISTRICT), the
organization_type is set to a corresponding election variant. Otherwise, it directly
mirrors the generic_org_type value.
+
+ args:
+ force_update (bool): If an existing instance has no values to change,
+ try to update the organization_type field (or related fields) anyway.
+ This is done by invoking the new instance handler.
"""
# A new record is added with organization_type not defined.
@@ -67,7 +72,7 @@ class CreateOrUpdateOrganizationTypeHelper:
if is_new_instance:
self._handle_new_instance()
else:
- self._handle_existing_instance()
+ self._handle_existing_instance(force_update)
return self.instance
@@ -92,7 +97,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
- def _handle_existing_instance(self):
+ def _handle_existing_instance(self, force_update_when_no_are_changes_found = False):
# == Init variables == #
# Instance is already in the database, fetch its current state
current_instance = self.sender.objects.get(id=self.instance.id)
@@ -109,17 +114,19 @@ class CreateOrUpdateOrganizationTypeHelper:
# 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):
- # No values to update - do nothing
- return None
- # == Program flow will halt here if there is no reason to update == #
+ # No changes found
+ if force_update_when_no_are_changes_found:
+ # If we want to force an update anyway, we can treat this record like
+ # its a new one in that we check for "None" values rather than changes.
+ self._handle_new_instance()
+ else:
+ # == Update the linked values == #
+ # Find out which field needs updating
+ organization_type_needs_update = generic_org_type_changed or is_election_board_changed
+ generic_org_type_needs_update = organization_type_changed
- # == Update the linked values == #
- # Find out which field needs updating
- organization_type_needs_update = generic_org_type_changed or is_election_board_changed
- generic_org_type_needs_update = organization_type_changed
-
- # Update the field
- self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
+ # Update the field
+ self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
"""
diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py
index cad9e0ebe..26161b272 100644
--- a/src/registrar/tests/test_management_scripts.py
+++ b/src/registrar/tests/test_management_scripts.py
@@ -251,11 +251,14 @@ class TestPopulateOrganizationType(MockEppLib):
self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
- def test_request_and_info_tribal_remove_election_office(self):
+ def test_request_and_info_tribal_doesnt_remove_election_office(self):
"""
- Tests if a tribal domain in the election csv changes organization_type to TRIBAL
- when it used to be TRIBAL - ELECTION
- for the domain request and the domain info
+ Tests if a tribal domain in the election csv changes organization_type to TRIBAL_ELECTION
+ when the is_election_board is True, and generic_org_type is Tribal when it is not
+ present in the CSV.
+
+ To avoid overwriting data, the script should not set any domain specified as
+ an election_office (that doesn't exist in the CSV) to false.
"""
# Set org type fields to none to mimic an environment without this data
@@ -287,10 +290,10 @@ class TestPopulateOrganizationType(MockEppLib):
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
- # Because we don't define this in the "csv", we expect that is election board will switch to False,
- # and organization_type will now be tribal
- expected_values["is_election_board"] = False
- expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL
+ # If we don't define this in the "csv", but the value was already true,
+ # we expect that is election board will stay True, and the org type will be tribal,
+ # and organization_type will now be tribal_election
+ expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
tribal_election_request.refresh_from_db()
tribal_election_info.refresh_from_db()
self.assert_expected_org_values_on_request_and_info(
From cdd34936b2c4a33542c0b0a05709fd037291aac7 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 09:29:09 -0600
Subject: [PATCH 54/71] Linter
---
src/registrar/models/utility/generic_helper.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py
index 77351b11c..991231841 100644
--- a/src/registrar/models/utility/generic_helper.py
+++ b/src/registrar/models/utility/generic_helper.py
@@ -49,7 +49,7 @@ class CreateOrUpdateOrganizationTypeHelper:
self.generic_org_to_org_map = generic_org_to_org_map
self.election_org_to_generic_org_map = election_org_to_generic_org_map
- def create_or_update_organization_type(self, force_update = False):
+ def create_or_update_organization_type(self, force_update=False):
"""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
@@ -97,7 +97,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
- def _handle_existing_instance(self, force_update_when_no_are_changes_found = False):
+ def _handle_existing_instance(self, force_update_when_no_are_changes_found=False):
# == Init variables == #
# Instance is already in the database, fetch its current state
current_instance = self.sender.objects.get(id=self.instance.id)
From c1c428b6c3025fbf87f84cee049a3c15e7facf14 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Wed, 17 Apr 2024 12:40:27 -0400
Subject: [PATCH 55/71] updated code for readability
---
src/registrar/admin.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index efd8c29c9..a0d2a0b6b 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1455,8 +1455,9 @@ class DomainRequestAdmin(ListHeaderAdmin):
# find the index to determine the referring url after the path
index = http_referer.find(request.path)
# Check if there is a character following the path in http_referer
- if index + len(request.path) < len(http_referer):
- next_char = http_referer[index + len(request.path)]
+ next_char_index = index + len(request.path)
+ if index + next_char_index < len(http_referer):
+ next_char = http_referer[next_char_index]
# Check if the next character is a digit, if so, this indicates
# a change view for domain request
From c0b95f22f9a5f65680f896386a09bffd0a50f251 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Wed, 17 Apr 2024 09:52:18 -0700
Subject: [PATCH 56/71] Add documentation on how to access the db and roll back
a migration
---
docs/developer/database-access.md | 7 +++++++
docs/developer/migration-troubleshooting.md | 15 +++++++++++++++
2 files changed, 22 insertions(+)
diff --git a/docs/developer/database-access.md b/docs/developer/database-access.md
index 859ef2fd6..e13f970b3 100644
--- a/docs/developer/database-access.md
+++ b/docs/developer/database-access.md
@@ -56,6 +56,13 @@ cf ssh getgov-ENVIRONMENT
./manage.py dumpdata
```
+## Access certain table in the database
+1. `cf connect-to-service getgov-ENVIRONMENT getgov-ENVIRONMENT-database` gets you into whichever environments database you'd like
+2. `\c [table name here that starts cgaws];` connects to the [cgaws...etc] table
+3. `\dt` retrieves information about that table and displays it
+4. Make sure the table you are looking for exists. For this example, we are looking for `django_migrations`
+5. Run `SELECT * FROM django_migrations` to see everything that's in it!
+
## Dropping and re-creating the database
For your sandbox environment, it might be necessary to start the database over from scratch.
diff --git a/docs/developer/migration-troubleshooting.md b/docs/developer/migration-troubleshooting.md
index b90c02ae3..e2208f860 100644
--- a/docs/developer/migration-troubleshooting.md
+++ b/docs/developer/migration-troubleshooting.md
@@ -121,3 +121,18 @@ https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1697810600723069
2. `./manage.py migrate model_name_here file_name_WITH_create` (run the last data creation migration AND ONLY THAT ONE)
3. `./manage.py migrate --fake model_name_here most_recent_file_name` (fake migrate the last migration in the migration list)
4. `./manage.py load` (rerun fixtures)
+
+### Scenario 9: Inconsistent Migration History
+If you see `django.db.migrations.exceptions.InconsistentMigrationHistory` error, or when you run `./manage.py showmigrations` it looks like:
+
+[x] 0056_example_migration
+[ ] 0057_other_migration
+[x] 0058_some_other_migration
+
+1. Go to `database-access.md` to see the commands on how to access a certain table in the database.
+2. In this case, we want to remove the migration "history" from the `django_migrations` table
+3. Once you are in the `cgaws....` table, select the `django_migrations` table with the command `SELECT * FROM djangomigrations;`
+4. Find the id of the "history" you want to delete. In this example, the id would be 58.
+5. Run `DELETE FROM django_migrations WHERE id=58;` where 58 is an example id as seen above.
+6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state
+
From 1bc4f002a9d7a1744e60150479f931bc4d554014 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Wed, 17 Apr 2024 09:58:01 -0700
Subject: [PATCH 57/71] Fix table name and add in semicolon for statement
---
docs/developer/database-access.md | 4 ++--
docs/developer/migration-troubleshooting.md | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/developer/database-access.md b/docs/developer/database-access.md
index e13f970b3..f77261bbb 100644
--- a/docs/developer/database-access.md
+++ b/docs/developer/database-access.md
@@ -58,10 +58,10 @@ cf ssh getgov-ENVIRONMENT
## Access certain table in the database
1. `cf connect-to-service getgov-ENVIRONMENT getgov-ENVIRONMENT-database` gets you into whichever environments database you'd like
-2. `\c [table name here that starts cgaws];` connects to the [cgaws...etc] table
+2. `\c [table name here that starts cgaws...etc];` connects to the [cgaws...etc] table
3. `\dt` retrieves information about that table and displays it
4. Make sure the table you are looking for exists. For this example, we are looking for `django_migrations`
-5. Run `SELECT * FROM django_migrations` to see everything that's in it!
+5. Run `SELECT * FROM django_migrations;` to see everything that's in it!
## Dropping and re-creating the database
diff --git a/docs/developer/migration-troubleshooting.md b/docs/developer/migration-troubleshooting.md
index e2208f860..51422d4c4 100644
--- a/docs/developer/migration-troubleshooting.md
+++ b/docs/developer/migration-troubleshooting.md
@@ -131,7 +131,7 @@ If you see `django.db.migrations.exceptions.InconsistentMigrationHistory` error,
1. Go to `database-access.md` to see the commands on how to access a certain table in the database.
2. In this case, we want to remove the migration "history" from the `django_migrations` table
-3. Once you are in the `cgaws....` table, select the `django_migrations` table with the command `SELECT * FROM djangomigrations;`
+3. Once you are in the `cgaws....` table, select the `django_migrations` table with the command `SELECT * FROM django_migrations;`
4. Find the id of the "history" you want to delete. In this example, the id would be 58.
5. Run `DELETE FROM django_migrations WHERE id=58;` where 58 is an example id as seen above.
6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state
From e5105d076d1bccc63fbd14e6f3eaa2dcfb2b1468 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Wed, 17 Apr 2024 10:12:25 -0700
Subject: [PATCH 58/71] Point to the exact point in db access
---
docs/developer/migration-troubleshooting.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/developer/migration-troubleshooting.md b/docs/developer/migration-troubleshooting.md
index 51422d4c4..b2b8f8662 100644
--- a/docs/developer/migration-troubleshooting.md
+++ b/docs/developer/migration-troubleshooting.md
@@ -129,7 +129,7 @@ If you see `django.db.migrations.exceptions.InconsistentMigrationHistory` error,
[ ] 0057_other_migration
[x] 0058_some_other_migration
-1. Go to `database-access.md` to see the commands on how to access a certain table in the database.
+1. Go to [database-access.md](../database-access.md#access-certain-table-in-the-database) to see the commands on how to access a certain table in the database.
2. In this case, we want to remove the migration "history" from the `django_migrations` table
3. Once you are in the `cgaws....` table, select the `django_migrations` table with the command `SELECT * FROM django_migrations;`
4. Find the id of the "history" you want to delete. In this example, the id would be 58.
From 3988ffd653bb29bbcf1245db433f35768d5b57fb Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Wed, 17 Apr 2024 10:18:15 -0700
Subject: [PATCH 59/71] Address feedback of wording
---
docs/developer/migration-troubleshooting.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/docs/developer/migration-troubleshooting.md b/docs/developer/migration-troubleshooting.md
index b2b8f8662..f096a876a 100644
--- a/docs/developer/migration-troubleshooting.md
+++ b/docs/developer/migration-troubleshooting.md
@@ -132,7 +132,8 @@ If you see `django.db.migrations.exceptions.InconsistentMigrationHistory` error,
1. Go to [database-access.md](../database-access.md#access-certain-table-in-the-database) to see the commands on how to access a certain table in the database.
2. In this case, we want to remove the migration "history" from the `django_migrations` table
3. Once you are in the `cgaws....` table, select the `django_migrations` table with the command `SELECT * FROM django_migrations;`
-4. Find the id of the "history" you want to delete. In this example, the id would be 58.
-5. Run `DELETE FROM django_migrations WHERE id=58;` where 58 is an example id as seen above.
-6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state
+4. Find the id of the "history" you want to delete. This will be the one in the far left column. For this example, let's pretend the id is 101.
+5. Run `DELETE FROM django_migrations WHERE id=101;` where 101 is an example id as seen above.
+6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state. Most likely you will show several unapplied migrations.
+7. If you still have unapplied migrations, run `./manage.py migrate`. If an error occurs saying one has already been applied, fake that particular migration `./manage.py migrate --fake model_name_here migration_number` and then run the normal `./manage.py migrate` command to then apply those migrations that come after the one that threw the error.
From 436b87bc9654309de57ab840dcba2505f2a990a6 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Wed, 17 Apr 2024 10:20:55 -0700
Subject: [PATCH 60/71] Fix wording
---
docs/developer/migration-troubleshooting.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/developer/migration-troubleshooting.md b/docs/developer/migration-troubleshooting.md
index f096a876a..22a02503d 100644
--- a/docs/developer/migration-troubleshooting.md
+++ b/docs/developer/migration-troubleshooting.md
@@ -131,9 +131,9 @@ If you see `django.db.migrations.exceptions.InconsistentMigrationHistory` error,
1. Go to [database-access.md](../database-access.md#access-certain-table-in-the-database) to see the commands on how to access a certain table in the database.
2. In this case, we want to remove the migration "history" from the `django_migrations` table
-3. Once you are in the `cgaws....` table, select the `django_migrations` table with the command `SELECT * FROM django_migrations;`
+3. Once you are in the `cgaws...` table, select the `django_migrations` table with the command `SELECT * FROM django_migrations;`
4. Find the id of the "history" you want to delete. This will be the one in the far left column. For this example, let's pretend the id is 101.
5. Run `DELETE FROM django_migrations WHERE id=101;` where 101 is an example id as seen above.
-6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state. Most likely you will show several unapplied migrations.
+6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state. Most likely you will see several unapplied migrations.
7. If you still have unapplied migrations, run `./manage.py migrate`. If an error occurs saying one has already been applied, fake that particular migration `./manage.py migrate --fake model_name_here migration_number` and then run the normal `./manage.py migrate` command to then apply those migrations that come after the one that threw the error.
From e988ed0856e526e66cd980c6b0ba3cec11322d62 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 14:35:11 -0600
Subject: [PATCH 61/71] Update src/registrar/tests/test_admin.py
Co-authored-by: Rachid Mrad <107004823+rachidatecs@users.noreply.github.com>
---
src/registrar/tests/test_admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index c034b103d..c47d9080e 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -86,7 +86,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
super().setUp()
@less_console_noise_decorator
- def test_contact_fields_have_detail_table(self):
+ def test_contact_fields_on_domain_change_form_have_detail_table(self):
"""Tests if the contact fields have the detail table which displays title, email, and phone"""
# Create fake creator
From 001ac4aa5469bb70dc2ae8bc7b921bae8f74544b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 14:35:19 -0600
Subject: [PATCH 62/71] Update src/registrar/tests/test_admin.py
Co-authored-by: Rachid Mrad <107004823+rachidatecs@users.noreply.github.com>
---
src/registrar/tests/test_admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index c47d9080e..4fc4e4d4a 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -87,7 +87,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
@less_console_noise_decorator
def test_contact_fields_on_domain_change_form_have_detail_table(self):
- """Tests if the contact fields have the detail table which displays title, email, and phone"""
+ """Tests if the contact fields in the inlined Domain information have the detail table which displays title, email, and phone"""
# Create fake creator
_creator = User.objects.create(
From 03a0a60b3a7c762db5de9541287b6c57b3cc08a1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 14:43:27 -0600
Subject: [PATCH 63/71] Add comment
---
src/registrar/models/utility/generic_helper.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py
index 991231841..4b08468a4 100644
--- a/src/registrar/models/utility/generic_helper.py
+++ b/src/registrar/models/utility/generic_helper.py
@@ -64,6 +64,9 @@ class CreateOrUpdateOrganizationTypeHelper:
force_update (bool): If an existing instance has no values to change,
try to update the organization_type field (or related fields) anyway.
This is done by invoking the new instance handler.
+
+ Use to force org type to be updated to the correct value even
+ if no other changes were made (including is_election).
"""
# A new record is added with organization_type not defined.
From 4467571613f5d1c69665a6a2a9a7df723d5cbb91 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 14:51:02 -0600
Subject: [PATCH 64/71] Its the final lintdown
---
src/registrar/models/utility/generic_helper.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py
index 4b08468a4..892298967 100644
--- a/src/registrar/models/utility/generic_helper.py
+++ b/src/registrar/models/utility/generic_helper.py
@@ -65,7 +65,7 @@ class CreateOrUpdateOrganizationTypeHelper:
try to update the organization_type field (or related fields) anyway.
This is done by invoking the new instance handler.
- Use to force org type to be updated to the correct value even
+ Use to force org type to be updated to the correct value even
if no other changes were made (including is_election).
"""
From 39f290424b1276f45743ffeb1dfbc9cd76e0d118 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 15:03:03 -0600
Subject: [PATCH 65/71] Remove refactor of sliced requests
Better provisioned for another PR
---
src/registrar/utility/csv_export.py | 123 +++++++++++++---------------
1 file changed, 59 insertions(+), 64 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 61480f4a0..d9e07b958 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -567,78 +567,73 @@ def get_sliced_domains(filter_condition):
Pass distinct=True when filtering by permissions so we do not to count multiples
when a domain has more that one manager.
"""
- return get_org_type_counts(DomainInformation, filter_condition)
+
+ 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,
+ federal,
+ interstate,
+ state_or_territory,
+ tribal,
+ county,
+ city,
+ special_district,
+ school_district,
+ election_board,
+ ]
def get_sliced_requests(filter_condition):
"""Get filtered requests counts sliced by org type and election office."""
- return get_org_type_counts(DomainRequest, filter_condition)
+
+ 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()
+ )
+ 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()
-
-def get_org_type_counts(model_class, filter_condition):
- """Returns a list of counts for each org type"""
-
- # Count all org types, such as federal
- dynamic_count_dict = {}
- for choice in DomainRequest.OrganizationChoices:
- choice_name = f"{choice}_count"
- dynamic_count_dict[choice_name] = _org_type_count_query_builder(choice)
-
- # Static counts
- static_count_dict = {
- # Count all distinct records
- "total_count": Count("id"),
- # Count all election boards
- "election_board_count": Count(Case(When(is_election_board=True, then=1))),
- }
-
- # Merge static counts with dynamic organization type counts
- merged_count_dict = {**static_count_dict, **dynamic_count_dict}
-
- # Perform a single query with conditional aggregation
- model_queryset = model_class.objects.filter(**filter_condition).distinct()
- aggregates = model_queryset.aggregate(**merged_count_dict)
-
- # This can be automated but for the sake of readability, this is fixed for now.
- # To automate this would also mean the added benefit of
- # auto-updating (automatically adds new org types) charts,
- # but that requires an upstream refactor.
return [
- # Total number of records
- aggregates["total_count"],
- # Number of records with org type FEDERAL
- aggregates["federal_count"],
- # Number of records with org type INTERSTATE
- aggregates["interstate_count"],
- # Number of records with org type STATE_OR_TERRITORY
- aggregates["state_or_territory_count"],
- # Number of records for TRIBAL
- aggregates["tribal_count"],
- # Number of records for COUNTY
- aggregates["county_count"],
- # Number of records for CITY
- aggregates["city_count"],
- # Number of records for SPECIAL_DISTRICT
- aggregates["special_district_count"],
- # Number of records for SCHOOL_DISTRICT
- aggregates["school_district_count"],
- # Number of records for ELECTION_BOARD
- aggregates["election_board_count"],
+ requests_count,
+ federal,
+ interstate,
+ state_or_territory,
+ tribal,
+ county,
+ city,
+ special_district,
+ school_district,
+ election_board,
]
-
-def _org_type_count_query_builder(generic_org_type):
- """
- Returns an expression that counts the number of a given generic_org_type.
- On the backend (the DB), this returns an array of "1" which is then counted by the expression.
- Used within an .aggregate call, but this essentially performs the same as queryset.count()
-
- We use this as opposed to queryset.count() because when this operation is repeated multiple times,
- it is more efficient to do these once in the DB rather than multiple times (as each count consitutes a call)
- """
- return Count(Case(When(generic_org_type=generic_org_type, then=1)))
-
-
def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
"""Get counts for domains that have domain managers for two different dates,
get list of managed domains at end_date."""
From 65c0da6705d5ff39a6b118554777cd096e99ef12 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 15:05:49 -0600
Subject: [PATCH 66/71] Update csv_export.py
---
src/registrar/utility/csv_export.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index d9e07b958..bd7fefb68 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -602,7 +602,6 @@ def get_sliced_domains(filter_condition):
def get_sliced_requests(filter_condition):
"""Get filtered requests counts sliced by org type and election office."""
-
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
requests_count = requests.count()
federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
@@ -634,6 +633,7 @@ def get_sliced_requests(filter_condition):
election_board,
]
+
def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
"""Get counts for domains that have domain managers for two different dates,
get list of managed domains at end_date."""
From 507c8d9bb6167db97d2427e2ce36cc3dbc2a7a14 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 15:08:00 -0600
Subject: [PATCH 67/71] Remove old import
---
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 bd7fefb68..8787f9e74 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -3,7 +3,6 @@ import logging
from datetime import datetime
from registrar.models.domain import Domain
from registrar.models.domain_invitation import DomainInvitation
-from django.db.models import Case, When, Count
from registrar.models.domain_request import DomainRequest
from registrar.models.domain_information import DomainInformation
from django.utils import timezone
From 82c88b2876d88a5f1967822647b6636e5735056a Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 15:10:13 -0600
Subject: [PATCH 68/71] Add top for firefox
---
src/registrar/assets/sass/_theme/_admin.scss | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index 4b69dc8e3..0da5a189e 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -593,6 +593,7 @@ address.dja-address-contact-list {
right: auto;
left: 4px;
height: 100%;
+ top: -1px;
}
button {
font-size: unset !important;
From a51c80f33305222380af3592fcba239a21d4062b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 17 Apr 2024 15:32:13 -0600
Subject: [PATCH 69/71] Fix merge conflict
---
.../admin/includes/detail_table_fieldset.html | 11 +-----
.../admin/includes/user_detail_list.html | 39 ++++++++++---------
2 files changed, 23 insertions(+), 27 deletions(-)
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 eff73f828..fb7303352 100644
--- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
+++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
@@ -69,16 +69,9 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% if field.field.name == "creator" %}
- {% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
-
-
-
- {% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
-
-
-
- {% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
+ {% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
+ {% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
{% elif field.field.name == "submitter" %}
diff --git a/src/registrar/templates/django/admin/includes/user_detail_list.html b/src/registrar/templates/django/admin/includes/user_detail_list.html
index 829af933a..c9ce1c52a 100644
--- a/src/registrar/templates/django/admin/includes/user_detail_list.html
+++ b/src/registrar/templates/django/admin/includes/user_detail_list.html
@@ -5,24 +5,27 @@
{% with rejected_requests_count=user.get_rejected_requests_count %}
{% with ineligible_requests_count=user.get_ineligible_requests_count %}
{% if approved_domains_count|add:active_requests_count|add:rejected_requests_count|add:ineligible_requests_count > 0 %}
-
{% endif %}
{% endwith %}
{% endwith %}
From 3395cf89ba4fe4c59589da281191623a33d9aa05 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 18 Apr 2024 09:28:14 -0600
Subject: [PATCH 70/71] Linting + remove filter horizontal
---
src/registrar/admin.py | 3 ---
src/registrar/tests/test_admin.py | 3 ++-
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index b7c8ddef6..a2e7cd7ca 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1493,9 +1493,6 @@ class DomainInformationInline(admin.StackedInline):
readonly_fields = DomainInformationAdmin.readonly_fields
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
- # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
- # to activate the edit/delete/view buttons
- filter_horizontal = ("other_contacts",)
autocomplete_fields = [
"creator",
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 4fc4e4d4a..e91823db8 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -87,7 +87,8 @@ class TestDomainAdmin(MockEppLib, WebTest):
@less_console_noise_decorator
def test_contact_fields_on_domain_change_form_have_detail_table(self):
- """Tests if the contact fields in the inlined Domain information have the detail table which displays title, email, and phone"""
+ """Tests if the contact fields in the inlined Domain information have the detail table
+ which displays title, email, and phone"""
# Create fake creator
_creator = User.objects.create(
From f0067cf49787348f0709a50bc7aa6c50d15be7ba Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 18 Apr 2024 10:41:04 -0600
Subject: [PATCH 71/71] Add comments
---
.../templates/django/admin/domain_information_change_form.html | 2 ++
.../templates/django/admin/domain_request_change_form.html | 2 ++
2 files changed, 4 insertions(+)
diff --git a/src/registrar/templates/django/admin/domain_information_change_form.html b/src/registrar/templates/django/admin/domain_information_change_form.html
index d20e33151..c5b0d54b8 100644
--- a/src/registrar/templates/django/admin/domain_information_change_form.html
+++ b/src/registrar/templates/django/admin/domain_information_change_form.html
@@ -9,6 +9,8 @@
{% include "django/admin/includes/domain_information_fieldset.html" %}
Use detail_table_fieldset as an example, or just extend it.
+
+ original_object is just a variable name for "DomainInformation" or "DomainRequest"
{% endcomment %}
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
{% endfor %}
diff --git a/src/registrar/templates/django/admin/domain_request_change_form.html b/src/registrar/templates/django/admin/domain_request_change_form.html
index cb1ba38c0..d574bbb7e 100644
--- a/src/registrar/templates/django/admin/domain_request_change_form.html
+++ b/src/registrar/templates/django/admin/domain_request_change_form.html
@@ -13,6 +13,8 @@
{% include "django/admin/includes/domain_information_fieldset.html" %}
Use detail_table_fieldset as an example, or just extend it.
+
+ original_object is just a variable name for "DomainInformation" or "DomainRequest"
{% endcomment %}
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
{% endfor %}