mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 18:09:25 +02:00
Merge branch 'main' of https://github.com/cisagov/manage.get.gov into rh/2909-new-agency-field
This commit is contained in:
commit
c997d150e6
26 changed files with 1290 additions and 99 deletions
|
@ -586,3 +586,59 @@ Example: `cf ssh getgov-za`
|
|||
| | Parameter | Description |
|
||||
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
|
||||
| 1 | **debug** | Increases logging detail. Defaults to False. |
|
||||
|
||||
|
||||
## 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 by concatenating " - Elections" on the end of generic_org_type string if is_elections_board is True.
|
||||
|
||||
### 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}```
|
||||
|
||||
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_board_filename}```
|
||||
|
||||
- The domain_election_board_filename file must adhere to this format:
|
||||
- example.gov\
|
||||
example2.gov\
|
||||
example3.gov
|
||||
|
||||
Example:
|
||||
`./manage.py populate_organization_type migrationdata/election-domains.csv`
|
||||
|
||||
### Running locally
|
||||
|
||||
#### 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}```
|
||||
|
||||
Example (assuming that this is being ran from src/):
|
||||
`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.
|
||||
|
|
|
@ -290,6 +290,13 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
|||
# Return the field value without a link
|
||||
return f"{obj.content_type} - {obj.object_repr}"
|
||||
|
||||
# We name the custom prop 'created_at' because linter
|
||||
# is not allowing a short_description attr on it
|
||||
# This gets around the linter limitation, for now.
|
||||
@admin.display(description=_("Created at"))
|
||||
def created(self, obj):
|
||||
return obj.timestamp
|
||||
|
||||
search_help_text = "Search by resource, changes, or user."
|
||||
|
||||
change_form_template = "admin/change_form_no_submit.html"
|
||||
|
@ -478,7 +485,7 @@ class MyUserAdmin(BaseUserAdmin):
|
|||
|
||||
list_display = (
|
||||
"username",
|
||||
"email",
|
||||
"overridden_email_field",
|
||||
"first_name",
|
||||
"last_name",
|
||||
# Group is a custom property defined within this file,
|
||||
|
@ -487,6 +494,18 @@ class MyUserAdmin(BaseUserAdmin):
|
|||
"status",
|
||||
)
|
||||
|
||||
# Renames inherited AbstractUser label 'email_address to 'email'
|
||||
def formfield_for_dbfield(self, dbfield, **kwargs):
|
||||
field = super().formfield_for_dbfield(dbfield, **kwargs)
|
||||
if dbfield.name == "email":
|
||||
field.label = "Email"
|
||||
return field
|
||||
|
||||
# Renames inherited AbstractUser column name 'email_address to 'email'
|
||||
@admin.display(description=_("Email"))
|
||||
def overridden_email_field(self, obj):
|
||||
return obj.email
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
|
@ -561,6 +580,7 @@ class MyUserAdmin(BaseUserAdmin):
|
|||
# this ordering effects the ordering of results
|
||||
# in autocomplete_fields for user
|
||||
ordering = ["first_name", "last_name", "email"]
|
||||
search_help_text = "Search by first name, last name, or email."
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
|
||||
|
@ -651,7 +671,7 @@ class MyHostAdmin(AuditedAdmin):
|
|||
"""Custom host admin class to use our inlines."""
|
||||
|
||||
search_fields = ["name", "domain__name"]
|
||||
search_help_text = "Search by domain or hostname."
|
||||
search_help_text = "Search by domain or host name."
|
||||
inlines = [HostIPInline]
|
||||
|
||||
|
||||
|
@ -659,9 +679,9 @@ class ContactAdmin(ListHeaderAdmin):
|
|||
"""Custom contact admin class to add search."""
|
||||
|
||||
search_fields = ["email", "first_name", "last_name"]
|
||||
search_help_text = "Search by firstname, lastname or email."
|
||||
search_help_text = "Search by first name, last name or email."
|
||||
list_display = [
|
||||
"contact",
|
||||
"name",
|
||||
"email",
|
||||
"user_exists",
|
||||
]
|
||||
|
@ -690,7 +710,7 @@ class ContactAdmin(ListHeaderAdmin):
|
|||
# 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.
|
||||
def contact(self, obj: models.Contact):
|
||||
def name(self, obj: models.Contact):
|
||||
"""Duplicate the contact _str_"""
|
||||
if obj.first_name or obj.last_name:
|
||||
return obj.get_formatted_name()
|
||||
|
@ -701,7 +721,7 @@ class ContactAdmin(ListHeaderAdmin):
|
|||
else:
|
||||
return ""
|
||||
|
||||
contact.admin_order_field = "first_name" # type: ignore
|
||||
name.admin_order_field = "first_name" # type: ignore
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [
|
||||
|
@ -859,7 +879,7 @@ class UserDomainRoleAdmin(ListHeaderAdmin):
|
|||
"domain__name",
|
||||
"role",
|
||||
]
|
||||
search_help_text = "Search by firstname, lastname, email, domain, or role."
|
||||
search_help_text = "Search by first name, last name, email, or domain."
|
||||
|
||||
autocomplete_fields = ["user", "domain"]
|
||||
|
||||
|
@ -1513,10 +1533,11 @@ 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
|
||||
"""
|
||||
|
||||
form = DomainInformationInlineForm
|
||||
|
||||
template = "django/admin/includes/domain_info_inline_stacked.html"
|
||||
model = models.DomainInformation
|
||||
|
||||
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
|
||||
|
@ -1526,10 +1547,8 @@ class DomainInformationInline(admin.StackedInline):
|
|||
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
|
||||
filter_horizontal = ("other_contacts",)
|
||||
|
||||
autocomplete_fields = [
|
||||
"creator",
|
||||
|
@ -1660,6 +1679,7 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
|
||||
city.admin_order_field = "domain_info__city" # type: ignore
|
||||
|
||||
@admin.display(description=_("State / territory"))
|
||||
def state_territory(self, obj):
|
||||
return obj.domain_info.state_territory if obj.domain_info else None
|
||||
|
||||
|
@ -1695,11 +1715,15 @@ 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)
|
||||
|
||||
# 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)
|
||||
try:
|
||||
curr_exp_date = domain.registry_expiration_date
|
||||
except KeyError:
|
||||
|
@ -1970,6 +1994,11 @@ class DraftDomainAdmin(ListHeaderAdmin):
|
|||
# this ordering effects the ordering of results
|
||||
# in autocomplete_fields for user
|
||||
ordering = ["name"]
|
||||
list_display = ["name"]
|
||||
|
||||
@admin.display(description=_("Requested domain"))
|
||||
def name(self, obj):
|
||||
return obj.name
|
||||
|
||||
def get_model_perms(self, request):
|
||||
"""
|
||||
|
@ -2048,13 +2077,36 @@ class FederalAgencyAdmin(ListHeaderAdmin):
|
|||
ordering = ["agency"]
|
||||
|
||||
|
||||
class UserGroupAdmin(AuditedAdmin):
|
||||
"""Overwrite the generated UserGroup admin class"""
|
||||
|
||||
list_display = ["user_group"]
|
||||
|
||||
fieldsets = ((None, {"fields": ("name", "permissions")}),)
|
||||
|
||||
def formfield_for_dbfield(self, dbfield, **kwargs):
|
||||
field = super().formfield_for_dbfield(dbfield, **kwargs)
|
||||
if dbfield.name == "name":
|
||||
field.label = "Group name"
|
||||
if dbfield.name == "permissions":
|
||||
field.label = "User permissions"
|
||||
return field
|
||||
|
||||
# We name the custom prop 'Group' because linter
|
||||
# is not allowing a short_description attr on it
|
||||
# This gets around the linter limitation, for now.
|
||||
@admin.display(description=_("Group"))
|
||||
def user_group(self, obj):
|
||||
return obj.name
|
||||
|
||||
|
||||
admin.site.unregister(LogEntry) # Unregister the default registration
|
||||
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
||||
admin.site.register(models.User, MyUserAdmin)
|
||||
# Unregister the built-in Group model
|
||||
admin.site.unregister(Group)
|
||||
# Register UserGroup
|
||||
admin.site.register(models.UserGroup)
|
||||
admin.site.register(models.UserGroup, UserGroupAdmin)
|
||||
admin.site.register(models.UserDomainRole, UserDomainRoleAdmin)
|
||||
admin.site.register(models.Contact, ContactAdmin)
|
||||
admin.site.register(models.DomainInvitation, DomainInvitationAdmin)
|
||||
|
|
|
@ -495,6 +495,8 @@ address.dja-address-contact-list {
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: medium;
|
||||
padding-top: 3px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -505,6 +507,7 @@ address.dja-address-contact-list {
|
|||
@media screen and (min-width:768px) {
|
||||
.visible-768 {
|
||||
display: block;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -593,6 +596,7 @@ address.dja-address-contact-list {
|
|||
right: auto;
|
||||
left: 4px;
|
||||
height: 100%;
|
||||
top: -1px;
|
||||
}
|
||||
button {
|
||||
font-size: unset !important;
|
||||
|
|
237
src/registrar/management/commands/populate_organization_type.py
Normal file
237
src/registrar/management/commands/populate_organization_type.py
Normal file
|
@ -0,0 +1,237 @@
|
|||
import argparse
|
||||
import logging
|
||||
import os
|
||||
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__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
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__()
|
||||
# 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] = []
|
||||
|
||||
# Define a global variable for all domains with election offices
|
||||
self.domains_with_election_boards_set = set()
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Adds command line arguments"""
|
||||
parser.add_argument(
|
||||
"domain_election_board_filename",
|
||||
help=("A file that contains" " all the domains that are election offices."),
|
||||
)
|
||||
|
||||
def handle(self, domain_election_board_filename, **kwargs):
|
||||
"""Loops through each valid Domain object and updates its first_created value"""
|
||||
|
||||
# Check if the provided file path is valid
|
||||
if not os.path.isfile(domain_election_board_filename):
|
||||
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_board_filename}'")
|
||||
|
||||
# Read the election office csv
|
||||
self.read_election_board_file(domain_election_board_filename)
|
||||
|
||||
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)
|
||||
|
||||
# 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,
|
||||
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)
|
||||
|
||||
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):
|
||||
"""
|
||||
Updates the organization_type for a list of DomainRequest objects using the `sync_organization_type` function.
|
||||
Results are then logged.
|
||||
|
||||
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 = 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.status == DomainRequest.DomainRequestStatus.APPROVED
|
||||
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:
|
||||
self.request_skipped.append(request)
|
||||
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", "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, True, log_header
|
||||
)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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)
|
||||
logger.info(f"Updating {info} => {info.organization_type}")
|
||||
else:
|
||||
self.di_skipped.append(info)
|
||||
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", "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, 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
|
||||
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" enum equivalent.
|
||||
# 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_board" 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,
|
||||
)
|
||||
|
||||
org_type_helper.create_or_update_organization_type(force_update=True)
|
|
@ -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)
|
||||
|
@ -59,13 +60,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 +89,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 +97,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 +107,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,
|
||||
|
|
|
@ -0,0 +1,382 @@
|
|||
# Generated by Django 4.2.10 on 2024-04-18 18:01
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django_fsm
|
||||
import registrar.models.utility.domain_field
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0084_create_groups_v11"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="first_name",
|
||||
field=models.CharField(blank=True, db_index=True, null=True, verbose_name="first name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="last_name",
|
||||
field=models.CharField(blank=True, db_index=True, null=True, verbose_name="last name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="title",
|
||||
field=models.CharField(blank=True, null=True, verbose_name="title / role"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domain",
|
||||
name="deleted",
|
||||
field=models.DateField(editable=False, help_text="Deleted at date", null=True, verbose_name="deleted on"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domain",
|
||||
name="first_ready",
|
||||
field=models.DateField(
|
||||
editable=False,
|
||||
help_text="The last time this domain moved into the READY state",
|
||||
null=True,
|
||||
verbose_name="first ready on",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domain",
|
||||
name="name",
|
||||
field=registrar.models.utility.domain_field.DomainField(
|
||||
default=None,
|
||||
help_text="Fully qualified domain name",
|
||||
max_length=253,
|
||||
unique=True,
|
||||
verbose_name="domain",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domain",
|
||||
name="state",
|
||||
field=django_fsm.FSMField(
|
||||
choices=[
|
||||
("unknown", "Unknown"),
|
||||
("dns needed", "Dns needed"),
|
||||
("ready", "Ready"),
|
||||
("on hold", "On hold"),
|
||||
("deleted", "Deleted"),
|
||||
],
|
||||
default="unknown",
|
||||
help_text="Very basic info about the lifecycle of this domain object",
|
||||
max_length=21,
|
||||
protected=True,
|
||||
verbose_name="domain state",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="address_line1",
|
||||
field=models.CharField(blank=True, help_text="Street address", null=True, verbose_name="address line 1"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="address_line2",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Street address line 2 (optional)", null=True, verbose_name="address line 2"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="is_election_board",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
help_text="Is your organization an election office?",
|
||||
null=True,
|
||||
verbose_name="election office",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
help_text="State, territory, or military post",
|
||||
max_length=2,
|
||||
null=True,
|
||||
verbose_name="state / territory",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="urbanization",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Urbanization (required for Puerto Rico only)",
|
||||
null=True,
|
||||
verbose_name="urbanization",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="zipcode",
|
||||
field=models.CharField(
|
||||
blank=True, db_index=True, help_text="Zip code", max_length=10, null=True, verbose_name="zip code"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="is_election_board",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
help_text="Is your organization an election office?",
|
||||
null=True,
|
||||
verbose_name="election office",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
help_text="State, territory, or military post",
|
||||
max_length=2,
|
||||
null=True,
|
||||
verbose_name="state / territory",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="submission_date",
|
||||
field=models.DateField(
|
||||
blank=True, default=None, help_text="Date submitted", null=True, verbose_name="submitted at"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="zipcode",
|
||||
field=models.CharField(
|
||||
blank=True, db_index=True, help_text="Zip code", max_length=10, null=True, verbose_name="zip code"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="draftdomain",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
default=None, help_text="Fully qualified domain name", max_length=253, verbose_name="requested domain"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="host",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
default=None, help_text="Fully qualified domain name", max_length=253, verbose_name="host name"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="hostip",
|
||||
name="address",
|
||||
field=models.CharField(
|
||||
default=None,
|
||||
help_text="IP address",
|
||||
max_length=46,
|
||||
validators=[django.core.validators.validate_ipv46_address],
|
||||
verbose_name="IP address",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="domain_name",
|
||||
field=models.CharField(blank=True, null=True, verbose_name="domain"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="first_name",
|
||||
field=models.CharField(
|
||||
blank=True, db_index=True, help_text="First name / given name", null=True, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="processed",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="Indicates whether this TransitionDomain was already processed",
|
||||
verbose_name="processed",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="State, territory, or military post",
|
||||
max_length=2,
|
||||
null=True,
|
||||
verbose_name="state / territory",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[("ready", "Ready"), ("on hold", "On hold"), ("unknown", "Unknown")],
|
||||
default="ready",
|
||||
help_text="domain status during the transfer",
|
||||
max_length=255,
|
||||
verbose_name="status",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="title",
|
||||
field=models.CharField(blank=True, help_text="Title", null=True, verbose_name="title / role"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="username",
|
||||
field=models.CharField(help_text="Username - this will be an email address", verbose_name="username"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="zipcode",
|
||||
field=models.CharField(
|
||||
blank=True, db_index=True, help_text="Zip code", max_length=10, null=True, verbose_name="zip code"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[("restricted", "restricted")],
|
||||
default=None,
|
||||
max_length=10,
|
||||
null=True,
|
||||
verbose_name="user status",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -18,7 +18,7 @@ class Contact(TimeStampedModel):
|
|||
first_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="first name / given name",
|
||||
verbose_name="first name",
|
||||
db_index=True,
|
||||
)
|
||||
middle_name = models.CharField(
|
||||
|
@ -28,13 +28,13 @@ class Contact(TimeStampedModel):
|
|||
last_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="last name / family name",
|
||||
verbose_name="last name",
|
||||
db_index=True,
|
||||
)
|
||||
title = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="title or role in your organization",
|
||||
verbose_name="title / role",
|
||||
)
|
||||
email = models.EmailField(
|
||||
null=True,
|
||||
|
|
|
@ -992,6 +992,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
blank=False,
|
||||
default=None, # prevent saving without a value
|
||||
unique=True,
|
||||
verbose_name="domain",
|
||||
help_text="Fully qualified domain name",
|
||||
)
|
||||
|
||||
|
@ -1000,6 +1001,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
choices=State.choices,
|
||||
default=State.UNKNOWN,
|
||||
protected=True, # cannot change state directly, particularly in Django admin
|
||||
verbose_name="domain state",
|
||||
help_text="Very basic info about the lifecycle of this domain object",
|
||||
)
|
||||
|
||||
|
@ -1017,12 +1019,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
deleted = DateField(
|
||||
null=True,
|
||||
editable=False,
|
||||
verbose_name="deleted on",
|
||||
help_text="Deleted at date",
|
||||
)
|
||||
|
||||
first_ready = DateField(
|
||||
null=True,
|
||||
editable=False,
|
||||
verbose_name="first ready on",
|
||||
help_text="The last time this domain moved into the READY state",
|
||||
)
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ class DomainInformation(TimeStampedModel):
|
|||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="election office",
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
|
@ -118,6 +119,7 @@ class DomainInformation(TimeStampedModel):
|
|||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="election office",
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
|
@ -131,13 +133,13 @@ class DomainInformation(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
help_text="Street address",
|
||||
verbose_name="Street address",
|
||||
verbose_name="address line 1",
|
||||
)
|
||||
address_line2 = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Street address line 2 (optional)",
|
||||
verbose_name="Street address line 2 (optional)",
|
||||
verbose_name="address line 2",
|
||||
)
|
||||
city = models.CharField(
|
||||
null=True,
|
||||
|
@ -149,21 +151,22 @@ class DomainInformation(TimeStampedModel):
|
|||
choices=StateTerritoryChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="state / territory",
|
||||
help_text="State, territory, or military post",
|
||||
verbose_name="State, territory, or military post",
|
||||
)
|
||||
zipcode = models.CharField(
|
||||
max_length=10,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Zip code",
|
||||
verbose_name="zip code",
|
||||
db_index=True,
|
||||
)
|
||||
urbanization = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Urbanization (required for Puerto Rico only)",
|
||||
verbose_name="Urbanization (required for Puerto Rico only)",
|
||||
verbose_name="urbanization",
|
||||
)
|
||||
|
||||
about_your_organization = models.TextField(
|
||||
|
@ -246,14 +249,17 @@ 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
|
||||
# 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()
|
||||
|
||||
|
@ -272,6 +278,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
|
||||
|
|
|
@ -487,6 +487,7 @@ class DomainRequest(TimeStampedModel):
|
|||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="election office",
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
|
@ -559,12 +560,14 @@ class DomainRequest(TimeStampedModel):
|
|||
choices=StateTerritoryChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="state / territory",
|
||||
help_text="State, territory, or military post",
|
||||
)
|
||||
zipcode = models.CharField(
|
||||
max_length=10,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="zip code",
|
||||
help_text="Zip code",
|
||||
db_index=True,
|
||||
)
|
||||
|
@ -666,6 +669,7 @@ class DomainRequest(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name="submitted at",
|
||||
help_text="Date submitted",
|
||||
)
|
||||
|
||||
|
@ -675,14 +679,16 @@ 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.
|
||||
|
||||
# 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()
|
||||
|
||||
|
@ -701,6 +707,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):
|
||||
|
|
|
@ -18,5 +18,6 @@ class DraftDomain(TimeStampedModel, DomainHelper):
|
|||
max_length=253,
|
||||
blank=False,
|
||||
default=None, # prevent saving without a value
|
||||
verbose_name="requested domain",
|
||||
help_text="Fully qualified domain name",
|
||||
)
|
||||
|
|
|
@ -21,6 +21,7 @@ class Host(TimeStampedModel):
|
|||
blank=False,
|
||||
default=None, # prevent saving without a value
|
||||
unique=False,
|
||||
verbose_name="host name",
|
||||
help_text="Fully qualified domain name",
|
||||
)
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ class HostIP(TimeStampedModel):
|
|||
blank=False,
|
||||
default=None, # prevent saving without a value
|
||||
validators=[validate_ipv46_address],
|
||||
verbose_name="IP address",
|
||||
help_text="IP address",
|
||||
)
|
||||
|
||||
|
|
|
@ -20,13 +20,13 @@ class TransitionDomain(TimeStampedModel):
|
|||
username = models.CharField(
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name="Username",
|
||||
verbose_name="username",
|
||||
help_text="Username - this will be an email address",
|
||||
)
|
||||
domain_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Domain name",
|
||||
verbose_name="domain",
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=255,
|
||||
|
@ -34,7 +34,7 @@ class TransitionDomain(TimeStampedModel):
|
|||
blank=True,
|
||||
default=StatusChoices.READY,
|
||||
choices=StatusChoices.choices,
|
||||
verbose_name="Status",
|
||||
verbose_name="status",
|
||||
help_text="domain status during the transfer",
|
||||
)
|
||||
email_sent = models.BooleanField(
|
||||
|
@ -46,7 +46,7 @@ class TransitionDomain(TimeStampedModel):
|
|||
processed = models.BooleanField(
|
||||
null=False,
|
||||
default=True,
|
||||
verbose_name="Processed",
|
||||
verbose_name="processed",
|
||||
help_text="Indicates whether this TransitionDomain was already processed",
|
||||
)
|
||||
generic_org_type = models.CharField(
|
||||
|
@ -83,8 +83,8 @@ class TransitionDomain(TimeStampedModel):
|
|||
first_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="First name",
|
||||
verbose_name="first name / given name",
|
||||
help_text="First name / given name",
|
||||
verbose_name="first name",
|
||||
db_index=True,
|
||||
)
|
||||
middle_name = models.CharField(
|
||||
|
@ -100,6 +100,7 @@ class TransitionDomain(TimeStampedModel):
|
|||
title = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="title / role",
|
||||
help_text="Title",
|
||||
)
|
||||
email = models.EmailField(
|
||||
|
@ -126,12 +127,14 @@ class TransitionDomain(TimeStampedModel):
|
|||
max_length=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="state / territory",
|
||||
help_text="State, territory, or military post",
|
||||
)
|
||||
zipcode = models.CharField(
|
||||
max_length=10,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="zip code",
|
||||
help_text="Zip code",
|
||||
db_index=True,
|
||||
)
|
||||
|
|
|
@ -33,6 +33,7 @@ class User(AbstractUser):
|
|||
default=None, # Set the default value to None
|
||||
null=True, # Allow the field to be null
|
||||
blank=True, # Allow the field to be blank
|
||||
verbose_name="user status",
|
||||
)
|
||||
|
||||
domains = models.ManyToManyField(
|
||||
|
|
|
@ -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,14 @@ 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.
|
||||
|
||||
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.
|
||||
|
@ -67,7 +75,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 +100,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 +117,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):
|
||||
"""
|
||||
|
|
52
src/registrar/templates/admin/stacked.html
Normal file
52
src/registrar/templates/admin/stacked.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% 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 %}
|
||||
|
||||
<div class="js-inline-admin-formset inline-group"
|
||||
id="{{ inline_admin_formset.formset.prefix }}-group"
|
||||
data-inline-type="stacked"
|
||||
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
|
||||
|
||||
<fieldset class="module {{ inline_admin_formset.classes }}">
|
||||
{% if inline_admin_formset.formset.max_num == 1 %}
|
||||
<h2>{{ inline_admin_formset.opts.verbose_name|capfirst }}</h2>
|
||||
{% else %}
|
||||
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
|
||||
{% endif %}
|
||||
{{ inline_admin_formset.formset.management_form }}
|
||||
{{ inline_admin_formset.formset.non_form_errors }}
|
||||
|
||||
{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if forloop.last and inline_admin_formset.has_add_permission %}empty{% else %}{{ forloop.counter0 }}{% endif %}">
|
||||
<h3><b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b> <span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{{ inline_admin_formset.has_change_permission|yesno:'inlinechangelink,inlineviewlink' }}">{% if inline_admin_formset.has_change_permission %}{% translate "Change" %}{% else %}{% translate "View" %}{% endif %}</a>{% endif %}
|
||||
{% else %}#{{ forloop.counter }}{% endif %}</span>
|
||||
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% translate "View on site" %}</a>{% endif %}
|
||||
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
|
||||
</h3>
|
||||
{% if inline_admin_form.form.non_field_errors %}
|
||||
{{ inline_admin_form.form.non_field_errors }}
|
||||
{% endif %}
|
||||
|
||||
{% for fieldset in inline_admin_form %}
|
||||
{# .gov override #}
|
||||
{% block fieldset %}
|
||||
{% include "admin/includes/fieldset.html" %}
|
||||
{% endblock fieldset%}
|
||||
{# End of .gov override #}
|
||||
{% endfor %}
|
||||
|
||||
{% if inline_admin_form.needs_explicit_pk_field %}
|
||||
{{ inline_admin_form.pk_field.field }}
|
||||
{% endif %}
|
||||
{% if inline_admin_form.fk_field %}
|
||||
{{ inline_admin_form.fk_field.field }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
</div>
|
|
@ -9,7 +9,9 @@
|
|||
{% 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" %}
|
||||
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,8 +13,10 @@
|
|||
{% 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" %}
|
||||
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -116,8 +118,8 @@
|
|||
</button>
|
||||
</span>
|
||||
|
||||
<p class="text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
|
||||
<strong>Requested domain:</strong> {{ original.requested_domain.name }}
|
||||
<p class="padding-top-05 text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
|
||||
Requested domain: <strong>{{ original.requested_domain.name }}</strong>
|
||||
</p>
|
||||
{{ block.super }}
|
||||
</div>
|
||||
|
|
|
@ -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 %}
|
||||
<div class="readonly">
|
||||
|
@ -54,7 +54,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% elif field.field.name == "alternative_domains" %}
|
||||
<div class="readonly">
|
||||
{% with current_path=request.get_full_path %}
|
||||
{% for alt_domain in original.alternative_domains.all %}
|
||||
{% for alt_domain in original_object.alternative_domains.all %}
|
||||
<a href="{% url 'admin:registrar_website_change' alt_domain.id %}?{{ 'return_path='|add:current_path }}">{{ alt_domain }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
@ -69,24 +69,21 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% if field.field.name == "creator" %}
|
||||
<div class="flex-container">
|
||||
<label aria-label="Creator contact details"></label>
|
||||
{% include "django/admin/includes/contact_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %}
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<label aria-label="User summary details"></label>
|
||||
{% include "django/admin/includes/user_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 %}
|
||||
</div>
|
||||
{% 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" %}
|
||||
<div class="flex-container">
|
||||
<label aria-label="Submitter contact details"></label>
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% elif field.field.name == "authorizing_official" %}
|
||||
<div class="flex-container">
|
||||
<label aria-label="Authorizing official contact details"></label>
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% 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 class="margin-top-1 dja-detail-table" aria-role="button" open>
|
||||
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
|
||||
|
@ -104,7 +101,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
<td class="padding-left-1">{{ contact.title }}</td>
|
||||
<td class="padding-left-1">
|
||||
{{ contact.email }}
|
||||
|
||||
</td>
|
||||
<td class="padding-left-1">{{ contact.phone }}</td>
|
||||
<td class="padding-left-1 text-size-small">
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{% extends 'admin/stacked.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block fieldset %}
|
||||
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original_object %}
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
<ul class="dja-status-list">
|
||||
{% if approved_domains_count > 0 %}
|
||||
{# Approved domains #}
|
||||
<li>Approved domains: {{ approved_domains_count }}</li>
|
||||
{% endif %}
|
||||
{% if active_requests_count > 0 %}
|
||||
{# Active requests #}
|
||||
<li>Active requests: {{ active_requests_count }}</li>
|
||||
{% endif %}
|
||||
{% if rejected_requests_count > 0 %}
|
||||
{# Rejected requests #}
|
||||
<li>Rejected requests: {{ rejected_requests_count }}</li>
|
||||
{% endif %}
|
||||
{% if ineligible_requests_count > 0 %}
|
||||
{# Ineligible requests #}
|
||||
<li>Ineligible requests: {{ ineligible_requests_count }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="flex-container">
|
||||
<label aria-label="User summary details"></label>
|
||||
<ul class="dja-status-list">
|
||||
{% if approved_domains_count > 0 %}
|
||||
{# Approved domains #}
|
||||
<li>Approved domains: {{ approved_domains_count }}</li>
|
||||
{% endif %}
|
||||
{% if active_requests_count > 0 %}
|
||||
{# Active requests #}
|
||||
<li>Active requests: {{ active_requests_count }}</li>
|
||||
{% endif %}
|
||||
{% if rejected_requests_count > 0 %}
|
||||
{# Rejected requests #}
|
||||
<li>Rejected requests: {{ rejected_requests_count }}</li>
|
||||
{% endif %}
|
||||
{% if ineligible_requests_count > 0 %}
|
||||
{# Ineligible requests #}
|
||||
<li>Ineligible requests: {{ ineligible_requests_count }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
|
1
src/registrar/tests/data/fake_election_domains.csv
Normal file
1
src/registrar/tests/data/fake_election_domains.csv
Normal file
|
@ -0,0 +1 @@
|
|||
manualtransmission.gov
|
|
|
@ -85,6 +85,78 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
|||
)
|
||||
super().setUp()
|
||||
|
||||
@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"""
|
||||
|
||||
# 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")
|
||||
|
||||
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
|
||||
def test_extend_expiration_date_button(self, mock_date_today):
|
||||
"""
|
||||
|
@ -1509,7 +1581,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
# Since we're using client to mock the request, we can only test against
|
||||
# non-interpolated values
|
||||
expected_content = "<strong>Requested domain:</strong>"
|
||||
expected_content = "Requested domain:"
|
||||
expected_content2 = '<span class="scroll-indicator"></span>'
|
||||
expected_content3 = '<div class="submit-row-wrapper">'
|
||||
not_expected_content = "submit-row-wrapper--analyst-view>"
|
||||
|
@ -1538,7 +1610,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
# Since we're using client to mock the request, we can only test against
|
||||
# non-interpolated values
|
||||
expected_content = "<strong>Requested domain:</strong>"
|
||||
expected_content = "Requested domain:"
|
||||
expected_content2 = '<span class="scroll-indicator"></span>'
|
||||
expected_content3 = '<div class="submit-row-wrapper submit-row-wrapper--analyst-view">'
|
||||
self.assertContains(request, expected_content)
|
||||
|
|
|
@ -7,6 +7,9 @@ from django.test import TestCase
|
|||
from registrar.models import (
|
||||
User,
|
||||
Domain,
|
||||
DomainRequest,
|
||||
Contact,
|
||||
Website,
|
||||
DomainInvitation,
|
||||
TransitionDomain,
|
||||
DomainInformation,
|
||||
|
@ -18,7 +21,284 @@ 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,
|
||||
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
|
||||
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")
|
||||
|
||||
def assert_expected_org_values_on_request_and_info(
|
||||
self,
|
||||
domain_request: DomainRequest,
|
||||
domain_info: DomainInformation,
|
||||
expected_values: dict,
|
||||
):
|
||||
"""
|
||||
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
|
||||
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
|
||||
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 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.
|
||||
|
||||
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
|
||||
|
||||
# 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 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
|
||||
|
||||
# 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": 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
|
||||
"""
|
||||
|
||||
# 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_info = self.domain_info_3
|
||||
tribal_info.organization_type = None
|
||||
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,
|
||||
}
|
||||
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}")
|
||||
|
||||
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
|
||||
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_doesnt_remove_election_office(self):
|
||||
"""
|
||||
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
|
||||
tribal_election_request = self.domain_request_4
|
||||
tribal_election_info = self.domain_info_4
|
||||
tribal_election_request.organization_type = None
|
||||
tribal_election_info.organization_type = None
|
||||
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,
|
||||
}
|
||||
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}")
|
||||
|
||||
# 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(
|
||||
tribal_election_request, tribal_election_info, expected_values
|
||||
)
|
||||
|
||||
|
||||
class TestPopulateFirstReady(TestCase):
|
||||
|
|
|
@ -87,10 +87,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.get_organization_type_display()} - {domain_info.get_federal_type_display()}"
|
||||
else:
|
||||
domain_type = domain_info.get_generic_org_type_display()
|
||||
domain_type = domain_info.get_organization_type_display()
|
||||
|
||||
# create a dictionary of fields which can be included in output
|
||||
FIELDS = {
|
||||
|
@ -319,9 +319,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.get_organization_type_display()} - {request.get_federal_type_display()}"
|
||||
else:
|
||||
request_type = request.get_generic_org_type_display()
|
||||
request_type = request.get_organization_type_display()
|
||||
|
||||
# create a dictionary of fields which can be included in output
|
||||
FIELDS = {
|
||||
|
@ -399,7 +399,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 +432,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 +465,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,
|
||||
|
@ -601,7 +601,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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue