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 |
|
| | Parameter | Description |
|
||||||
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
|
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
|
||||||
| 1 | **debug** | Increases logging detail. Defaults to False. |
|
| 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 the field value without a link
|
||||||
return f"{obj.content_type} - {obj.object_repr}"
|
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."
|
search_help_text = "Search by resource, changes, or user."
|
||||||
|
|
||||||
change_form_template = "admin/change_form_no_submit.html"
|
change_form_template = "admin/change_form_no_submit.html"
|
||||||
|
@ -478,7 +485,7 @@ class MyUserAdmin(BaseUserAdmin):
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"username",
|
"username",
|
||||||
"email",
|
"overridden_email_field",
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
# Group is a custom property defined within this file,
|
# Group is a custom property defined within this file,
|
||||||
|
@ -487,6 +494,18 @@ class MyUserAdmin(BaseUserAdmin):
|
||||||
"status",
|
"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 = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
|
@ -561,6 +580,7 @@ class MyUserAdmin(BaseUserAdmin):
|
||||||
# this ordering effects the ordering of results
|
# this ordering effects the ordering of results
|
||||||
# in autocomplete_fields for user
|
# in autocomplete_fields for user
|
||||||
ordering = ["first_name", "last_name", "email"]
|
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"
|
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."""
|
"""Custom host admin class to use our inlines."""
|
||||||
|
|
||||||
search_fields = ["name", "domain__name"]
|
search_fields = ["name", "domain__name"]
|
||||||
search_help_text = "Search by domain or hostname."
|
search_help_text = "Search by domain or host name."
|
||||||
inlines = [HostIPInline]
|
inlines = [HostIPInline]
|
||||||
|
|
||||||
|
|
||||||
|
@ -659,9 +679,9 @@ class ContactAdmin(ListHeaderAdmin):
|
||||||
"""Custom contact admin class to add search."""
|
"""Custom contact admin class to add search."""
|
||||||
|
|
||||||
search_fields = ["email", "first_name", "last_name"]
|
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 = [
|
list_display = [
|
||||||
"contact",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
"user_exists",
|
"user_exists",
|
||||||
]
|
]
|
||||||
|
@ -690,7 +710,7 @@ class ContactAdmin(ListHeaderAdmin):
|
||||||
# We name the custom prop 'contact' because linter
|
# We name the custom prop 'contact' because linter
|
||||||
# is not allowing a short_description attr on it
|
# is not allowing a short_description attr on it
|
||||||
# This gets around the linter limitation, for now.
|
# This gets around the linter limitation, for now.
|
||||||
def contact(self, obj: models.Contact):
|
def name(self, obj: models.Contact):
|
||||||
"""Duplicate the contact _str_"""
|
"""Duplicate the contact _str_"""
|
||||||
if obj.first_name or obj.last_name:
|
if obj.first_name or obj.last_name:
|
||||||
return obj.get_formatted_name()
|
return obj.get_formatted_name()
|
||||||
|
@ -701,7 +721,7 @@ class ContactAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
return ""
|
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
|
# Read only that we'll leverage for CISA Analysts
|
||||||
analyst_readonly_fields = [
|
analyst_readonly_fields = [
|
||||||
|
@ -859,7 +879,7 @@ class UserDomainRoleAdmin(ListHeaderAdmin):
|
||||||
"domain__name",
|
"domain__name",
|
||||||
"role",
|
"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"]
|
autocomplete_fields = ["user", "domain"]
|
||||||
|
|
||||||
|
@ -1513,10 +1533,11 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
We had issues inheriting from both StackedInline
|
We had issues inheriting from both StackedInline
|
||||||
and the source DomainInformationAdmin since these
|
and the source DomainInformationAdmin since these
|
||||||
classes conflict, so we'll just pull what we need
|
classes conflict, so we'll just pull what we need
|
||||||
from DomainInformationAdmin"""
|
from DomainInformationAdmin
|
||||||
|
"""
|
||||||
|
|
||||||
form = DomainInformationInlineForm
|
form = DomainInformationInlineForm
|
||||||
|
template = "django/admin/includes/domain_info_inline_stacked.html"
|
||||||
model = models.DomainInformation
|
model = models.DomainInformation
|
||||||
|
|
||||||
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
|
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
|
||||||
|
@ -1526,10 +1547,8 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
del fieldsets[index]
|
del fieldsets[index]
|
||||||
break
|
break
|
||||||
|
|
||||||
|
readonly_fields = DomainInformationAdmin.readonly_fields
|
||||||
analyst_readonly_fields = DomainInformationAdmin.analyst_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 = [
|
autocomplete_fields = [
|
||||||
"creator",
|
"creator",
|
||||||
|
@ -1660,6 +1679,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
city.admin_order_field = "domain_info__city" # type: ignore
|
city.admin_order_field = "domain_info__city" # type: ignore
|
||||||
|
|
||||||
|
@admin.display(description=_("State / territory"))
|
||||||
def state_territory(self, obj):
|
def state_territory(self, obj):
|
||||||
return obj.domain_info.state_territory if obj.domain_info else None
|
return obj.domain_info.state_territory if obj.domain_info else None
|
||||||
|
|
||||||
|
@ -1695,11 +1715,15 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
if extra_context is None:
|
if extra_context is None:
|
||||||
extra_context = {}
|
extra_context = {}
|
||||||
|
|
||||||
# Pass in what the an extended expiration date would be for the expiration date modal
|
|
||||||
if object_id is not None:
|
if object_id is not None:
|
||||||
domain = Domain.objects.get(pk=object_id)
|
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:
|
try:
|
||||||
curr_exp_date = domain.registry_expiration_date
|
curr_exp_date = domain.registry_expiration_date
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -1970,6 +1994,11 @@ class DraftDomainAdmin(ListHeaderAdmin):
|
||||||
# this ordering effects the ordering of results
|
# this ordering effects the ordering of results
|
||||||
# in autocomplete_fields for user
|
# in autocomplete_fields for user
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
list_display = ["name"]
|
||||||
|
|
||||||
|
@admin.display(description=_("Requested domain"))
|
||||||
|
def name(self, obj):
|
||||||
|
return obj.name
|
||||||
|
|
||||||
def get_model_perms(self, request):
|
def get_model_perms(self, request):
|
||||||
"""
|
"""
|
||||||
|
@ -2048,13 +2077,36 @@ class FederalAgencyAdmin(ListHeaderAdmin):
|
||||||
ordering = ["agency"]
|
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.unregister(LogEntry) # Unregister the default registration
|
||||||
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
||||||
admin.site.register(models.User, MyUserAdmin)
|
admin.site.register(models.User, MyUserAdmin)
|
||||||
# Unregister the built-in Group model
|
# Unregister the built-in Group model
|
||||||
admin.site.unregister(Group)
|
admin.site.unregister(Group)
|
||||||
# Register UserGroup
|
# Register UserGroup
|
||||||
admin.site.register(models.UserGroup)
|
admin.site.register(models.UserGroup, UserGroupAdmin)
|
||||||
admin.site.register(models.UserDomainRole, UserDomainRoleAdmin)
|
admin.site.register(models.UserDomainRole, UserDomainRoleAdmin)
|
||||||
admin.site.register(models.Contact, ContactAdmin)
|
admin.site.register(models.Contact, ContactAdmin)
|
||||||
admin.site.register(models.DomainInvitation, DomainInvitationAdmin)
|
admin.site.register(models.DomainInvitation, DomainInvitationAdmin)
|
||||||
|
|
|
@ -495,6 +495,8 @@ address.dja-address-contact-list {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
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) {
|
@media screen and (min-width:768px) {
|
||||||
.visible-768 {
|
.visible-768 {
|
||||||
display: block;
|
display: block;
|
||||||
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -593,6 +596,7 @@ address.dja-address-contact-list {
|
||||||
right: auto;
|
right: auto;
|
||||||
left: 4px;
|
left: 4px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
top: -1px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
font-size: unset !important;
|
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:
|
Usage:
|
||||||
bulk_update_fields(Domain, page.object_list, ["first_ready"])
|
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
|
# 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.
|
# is too memory intensive for our current app config, so we can chunk this data instead.
|
||||||
paginator = Paginator(update_list, batch_size)
|
paginator = Paginator(update_list, batch_size)
|
||||||
|
@ -59,13 +60,16 @@ class ScriptDataHelper:
|
||||||
|
|
||||||
class TerminalHelper:
|
class TerminalHelper:
|
||||||
@staticmethod
|
@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
|
"""Prints success, failed, and skipped counts, as well as
|
||||||
all affected objects."""
|
all affected objects."""
|
||||||
update_success_count = len(to_update)
|
update_success_count = len(to_update)
|
||||||
update_failed_count = len(failed_to_update)
|
update_failed_count = len(failed_to_update)
|
||||||
update_skipped_count = len(skipped)
|
update_skipped_count = len(skipped)
|
||||||
|
|
||||||
|
if log_header is None:
|
||||||
|
log_header = "============= FINISHED ==============="
|
||||||
|
|
||||||
# Prepare debug messages
|
# Prepare debug messages
|
||||||
debug_messages = {
|
debug_messages = {
|
||||||
"success": (f"{TerminalColors.OKCYAN}Updated: {to_update}{TerminalColors.ENDC}\n"),
|
"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:
|
if update_failed_count == 0 and update_skipped_count == 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"""{TerminalColors.OKGREEN}
|
f"""{TerminalColors.OKGREEN}
|
||||||
============= FINISHED ===============
|
{log_header}
|
||||||
Updated {update_success_count} entries
|
Updated {update_success_count} entries
|
||||||
{TerminalColors.ENDC}
|
{TerminalColors.ENDC}
|
||||||
"""
|
"""
|
||||||
|
@ -93,7 +97,7 @@ class TerminalHelper:
|
||||||
elif update_failed_count == 0:
|
elif update_failed_count == 0:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"""{TerminalColors.YELLOW}
|
f"""{TerminalColors.YELLOW}
|
||||||
============= FINISHED ===============
|
{log_header}
|
||||||
Updated {update_success_count} entries
|
Updated {update_success_count} entries
|
||||||
----- SOME DATA WAS INVALID (NEEDS MANUAL PATCHING) -----
|
----- SOME DATA WAS INVALID (NEEDS MANUAL PATCHING) -----
|
||||||
Skipped updating {update_skipped_count} entries
|
Skipped updating {update_skipped_count} entries
|
||||||
|
@ -103,7 +107,7 @@ class TerminalHelper:
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"""{TerminalColors.FAIL}
|
f"""{TerminalColors.FAIL}
|
||||||
============= FINISHED ===============
|
{log_header}
|
||||||
Updated {update_success_count} entries
|
Updated {update_success_count} entries
|
||||||
----- UPDATE FAILED -----
|
----- UPDATE FAILED -----
|
||||||
Failed to update {update_failed_count} entries,
|
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(
|
first_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="first name / given name",
|
verbose_name="first name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
middle_name = models.CharField(
|
middle_name = models.CharField(
|
||||||
|
@ -28,13 +28,13 @@ class Contact(TimeStampedModel):
|
||||||
last_name = models.CharField(
|
last_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="last name / family name",
|
verbose_name="last name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
title = models.CharField(
|
title = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="title or role in your organization",
|
verbose_name="title / role",
|
||||||
)
|
)
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
null=True,
|
null=True,
|
||||||
|
|
|
@ -992,6 +992,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
blank=False,
|
blank=False,
|
||||||
default=None, # prevent saving without a value
|
default=None, # prevent saving without a value
|
||||||
unique=True,
|
unique=True,
|
||||||
|
verbose_name="domain",
|
||||||
help_text="Fully qualified domain name",
|
help_text="Fully qualified domain name",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1000,6 +1001,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
choices=State.choices,
|
choices=State.choices,
|
||||||
default=State.UNKNOWN,
|
default=State.UNKNOWN,
|
||||||
protected=True, # cannot change state directly, particularly in Django admin
|
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",
|
help_text="Very basic info about the lifecycle of this domain object",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1017,12 +1019,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
deleted = DateField(
|
deleted = DateField(
|
||||||
null=True,
|
null=True,
|
||||||
editable=False,
|
editable=False,
|
||||||
|
verbose_name="deleted on",
|
||||||
help_text="Deleted at date",
|
help_text="Deleted at date",
|
||||||
)
|
)
|
||||||
|
|
||||||
first_ready = DateField(
|
first_ready = DateField(
|
||||||
null=True,
|
null=True,
|
||||||
editable=False,
|
editable=False,
|
||||||
|
verbose_name="first ready on",
|
||||||
help_text="The last time this domain moved into the READY state",
|
help_text="The last time this domain moved into the READY state",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
is_election_board = models.BooleanField(
|
is_election_board = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="election office",
|
||||||
help_text="Is your organization an election office?",
|
help_text="Is your organization an election office?",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -118,6 +119,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
is_election_board = models.BooleanField(
|
is_election_board = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="election office",
|
||||||
help_text="Is your organization an election office?",
|
help_text="Is your organization an election office?",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -131,13 +133,13 @@ class DomainInformation(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address",
|
help_text="Street address",
|
||||||
verbose_name="Street address",
|
verbose_name="address line 1",
|
||||||
)
|
)
|
||||||
address_line2 = models.CharField(
|
address_line2 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address line 2 (optional)",
|
help_text="Street address line 2 (optional)",
|
||||||
verbose_name="Street address line 2 (optional)",
|
verbose_name="address line 2",
|
||||||
)
|
)
|
||||||
city = models.CharField(
|
city = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -149,21 +151,22 @@ class DomainInformation(TimeStampedModel):
|
||||||
choices=StateTerritoryChoices.choices,
|
choices=StateTerritoryChoices.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="state / territory",
|
||||||
help_text="State, territory, or military post",
|
help_text="State, territory, or military post",
|
||||||
verbose_name="State, territory, or military post",
|
|
||||||
)
|
)
|
||||||
zipcode = models.CharField(
|
zipcode = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Zip code",
|
help_text="Zip code",
|
||||||
|
verbose_name="zip code",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
urbanization = models.CharField(
|
urbanization = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Urbanization (required for Puerto Rico only)",
|
help_text="Urbanization (required for Puerto Rico only)",
|
||||||
verbose_name="Urbanization (required for Puerto Rico only)",
|
verbose_name="urbanization",
|
||||||
)
|
)
|
||||||
|
|
||||||
about_your_organization = models.TextField(
|
about_your_organization = models.TextField(
|
||||||
|
@ -246,14 +249,17 @@ class DomainInformation(TimeStampedModel):
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def sync_organization_type(self):
|
||||||
"""Save override for custom properties"""
|
"""
|
||||||
|
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.
|
# Define mappings between generic org and election org.
|
||||||
# These have to be defined here, as you'd get a cyclical import error
|
# These have to be defined here, as you'd get a cyclical import error
|
||||||
# otherwise.
|
# 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
|
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
|
||||||
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_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
|
# Actually updates the organization_type field
|
||||||
org_type_helper.create_or_update_organization_type()
|
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)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -487,6 +487,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
is_election_board = models.BooleanField(
|
is_election_board = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="election office",
|
||||||
help_text="Is your organization an election office?",
|
help_text="Is your organization an election office?",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -559,12 +560,14 @@ class DomainRequest(TimeStampedModel):
|
||||||
choices=StateTerritoryChoices.choices,
|
choices=StateTerritoryChoices.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="state / territory",
|
||||||
help_text="State, territory, or military post",
|
help_text="State, territory, or military post",
|
||||||
)
|
)
|
||||||
zipcode = models.CharField(
|
zipcode = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="zip code",
|
||||||
help_text="Zip code",
|
help_text="Zip code",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
@ -666,6 +669,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
default=None,
|
default=None,
|
||||||
|
verbose_name="submitted at",
|
||||||
help_text="Date submitted",
|
help_text="Date submitted",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -675,14 +679,16 @@ class DomainRequest(TimeStampedModel):
|
||||||
help_text="Notes about this request",
|
help_text="Notes about this request",
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def sync_organization_type(self):
|
||||||
"""Save override for custom properties"""
|
"""
|
||||||
|
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.
|
# Define mappings between generic org and election org.
|
||||||
# These have to be defined here, as you'd get a cyclical import error
|
# These have to be defined here, as you'd get a cyclical import error
|
||||||
# otherwise.
|
# 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
|
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
|
||||||
generic_org_map = self.OrgChoicesElectionOffice.get_org_generic_to_org_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
|
# Actually updates the organization_type field
|
||||||
org_type_helper.create_or_update_organization_type()
|
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)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -18,5 +18,6 @@ class DraftDomain(TimeStampedModel, DomainHelper):
|
||||||
max_length=253,
|
max_length=253,
|
||||||
blank=False,
|
blank=False,
|
||||||
default=None, # prevent saving without a value
|
default=None, # prevent saving without a value
|
||||||
|
verbose_name="requested domain",
|
||||||
help_text="Fully qualified domain name",
|
help_text="Fully qualified domain name",
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,6 +21,7 @@ class Host(TimeStampedModel):
|
||||||
blank=False,
|
blank=False,
|
||||||
default=None, # prevent saving without a value
|
default=None, # prevent saving without a value
|
||||||
unique=False,
|
unique=False,
|
||||||
|
verbose_name="host name",
|
||||||
help_text="Fully qualified domain name",
|
help_text="Fully qualified domain name",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ class HostIP(TimeStampedModel):
|
||||||
blank=False,
|
blank=False,
|
||||||
default=None, # prevent saving without a value
|
default=None, # prevent saving without a value
|
||||||
validators=[validate_ipv46_address],
|
validators=[validate_ipv46_address],
|
||||||
|
verbose_name="IP address",
|
||||||
help_text="IP address",
|
help_text="IP address",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,13 @@ class TransitionDomain(TimeStampedModel):
|
||||||
username = models.CharField(
|
username = models.CharField(
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
verbose_name="Username",
|
verbose_name="username",
|
||||||
help_text="Username - this will be an email address",
|
help_text="Username - this will be an email address",
|
||||||
)
|
)
|
||||||
domain_name = models.CharField(
|
domain_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="Domain name",
|
verbose_name="domain",
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
@ -34,7 +34,7 @@ class TransitionDomain(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
default=StatusChoices.READY,
|
default=StatusChoices.READY,
|
||||||
choices=StatusChoices.choices,
|
choices=StatusChoices.choices,
|
||||||
verbose_name="Status",
|
verbose_name="status",
|
||||||
help_text="domain status during the transfer",
|
help_text="domain status during the transfer",
|
||||||
)
|
)
|
||||||
email_sent = models.BooleanField(
|
email_sent = models.BooleanField(
|
||||||
|
@ -46,7 +46,7 @@ class TransitionDomain(TimeStampedModel):
|
||||||
processed = models.BooleanField(
|
processed = models.BooleanField(
|
||||||
null=False,
|
null=False,
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name="Processed",
|
verbose_name="processed",
|
||||||
help_text="Indicates whether this TransitionDomain was already processed",
|
help_text="Indicates whether this TransitionDomain was already processed",
|
||||||
)
|
)
|
||||||
generic_org_type = models.CharField(
|
generic_org_type = models.CharField(
|
||||||
|
@ -83,8 +83,8 @@ class TransitionDomain(TimeStampedModel):
|
||||||
first_name = models.CharField(
|
first_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="First name",
|
help_text="First name / given name",
|
||||||
verbose_name="first name / given name",
|
verbose_name="first name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
middle_name = models.CharField(
|
middle_name = models.CharField(
|
||||||
|
@ -100,6 +100,7 @@ class TransitionDomain(TimeStampedModel):
|
||||||
title = models.CharField(
|
title = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="title / role",
|
||||||
help_text="Title",
|
help_text="Title",
|
||||||
)
|
)
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
|
@ -126,12 +127,14 @@ class TransitionDomain(TimeStampedModel):
|
||||||
max_length=2,
|
max_length=2,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="state / territory",
|
||||||
help_text="State, territory, or military post",
|
help_text="State, territory, or military post",
|
||||||
)
|
)
|
||||||
zipcode = models.CharField(
|
zipcode = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name="zip code",
|
||||||
help_text="Zip code",
|
help_text="Zip code",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -33,6 +33,7 @@ class User(AbstractUser):
|
||||||
default=None, # Set the default value to None
|
default=None, # Set the default value to None
|
||||||
null=True, # Allow the field to be null
|
null=True, # Allow the field to be null
|
||||||
blank=True, # Allow the field to be blank
|
blank=True, # Allow the field to be blank
|
||||||
|
verbose_name="user status",
|
||||||
)
|
)
|
||||||
|
|
||||||
domains = models.ManyToManyField(
|
domains = models.ManyToManyField(
|
||||||
|
|
|
@ -49,7 +49,7 @@ class CreateOrUpdateOrganizationTypeHelper:
|
||||||
self.generic_org_to_org_map = generic_org_to_org_map
|
self.generic_org_to_org_map = generic_org_to_org_map
|
||||||
self.election_org_to_generic_org_map = election_org_to_generic_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
|
"""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
|
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
|
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
|
one of the excluded types (FEDERAL, INTERSTATE, SCHOOL_DISTRICT), the
|
||||||
organization_type is set to a corresponding election variant. Otherwise, it directly
|
organization_type is set to a corresponding election variant. Otherwise, it directly
|
||||||
mirrors the generic_org_type value.
|
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.
|
# A new record is added with organization_type not defined.
|
||||||
|
@ -67,7 +75,7 @@ class CreateOrUpdateOrganizationTypeHelper:
|
||||||
if is_new_instance:
|
if is_new_instance:
|
||||||
self._handle_new_instance()
|
self._handle_new_instance()
|
||||||
else:
|
else:
|
||||||
self._handle_existing_instance()
|
self._handle_existing_instance(force_update)
|
||||||
|
|
||||||
return self.instance
|
return self.instance
|
||||||
|
|
||||||
|
@ -92,7 +100,7 @@ class CreateOrUpdateOrganizationTypeHelper:
|
||||||
# Update the field
|
# Update the field
|
||||||
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
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 == #
|
# == Init variables == #
|
||||||
# Instance is already in the database, fetch its current state
|
# Instance is already in the database, fetch its current state
|
||||||
current_instance = self.sender.objects.get(id=self.instance.id)
|
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.
|
# This will not happen in normal flow as it is not possible otherwise.
|
||||||
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
|
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):
|
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
|
||||||
# No values to update - do nothing
|
# No changes found
|
||||||
return None
|
if force_update_when_no_are_changes_found:
|
||||||
# == Program flow will halt here if there is no reason to update == #
|
# 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 == #
|
# Update the field
|
||||||
# Find out which field needs updating
|
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
||||||
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)
|
|
||||||
|
|
||||||
def _update_fields(self, 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" %}
|
{% include "django/admin/includes/domain_information_fieldset.html" %}
|
||||||
|
|
||||||
Use detail_table_fieldset as an example, or just extend it.
|
Use detail_table_fieldset as an example, or just extend it.
|
||||||
|
|
||||||
|
original_object is just a variable name for "DomainInformation" or "DomainRequest"
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% include "django/admin/includes/detail_table_fieldset.html" %}
|
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
{% include "django/admin/includes/domain_information_fieldset.html" %}
|
{% include "django/admin/includes/domain_information_fieldset.html" %}
|
||||||
|
|
||||||
Use detail_table_fieldset as an example, or just extend it.
|
Use detail_table_fieldset as an example, or just extend it.
|
||||||
|
|
||||||
|
original_object is just a variable name for "DomainInformation" or "DomainRequest"
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% include "django/admin/includes/detail_table_fieldset.html" %}
|
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -116,8 +118,8 @@
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<p class="text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
|
<p class="padding-top-05 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 }}
|
Requested domain: <strong>{{ original.requested_domain.name }}</strong>
|
||||||
</p>
|
</p>
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% block field_readonly %}
|
{% 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 field.field.name == "other_contacts" %}
|
||||||
{% if all_contacts.count > 2 %}
|
{% if all_contacts.count > 2 %}
|
||||||
<div class="readonly">
|
<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" %}
|
{% elif field.field.name == "alternative_domains" %}
|
||||||
<div class="readonly">
|
<div class="readonly">
|
||||||
{% with current_path=request.get_full_path %}
|
{% 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 %}
|
<a href="{% url 'admin:registrar_website_change' alt_domain.id %}?{{ 'return_path='|add:current_path }}">{{ alt_domain }}</a>{% if not forloop.last %}, {% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -69,24 +69,21 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% if field.field.name == "creator" %}
|
{% if field.field.name == "creator" %}
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<label aria-label="Creator contact details"></label>
|
<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 %}
|
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.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 %}
|
|
||||||
</div>
|
</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" %}
|
{% elif field.field.name == "submitter" %}
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<label aria-label="Submitter contact details"></label>
|
<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>
|
</div>
|
||||||
{% elif field.field.name == "authorizing_official" %}
|
{% elif field.field.name == "authorizing_official" %}
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<label aria-label="Authorizing official contact details"></label>
|
<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>
|
</div>
|
||||||
{% elif field.field.name == "other_contacts" and original.other_contacts.all %}
|
{% elif field.field.name == "other_contacts" and original_object.other_contacts.all %}
|
||||||
{% with all_contacts=original.other_contacts.all %}
|
{% with all_contacts=original_object.other_contacts.all %}
|
||||||
{% if all_contacts.count > 2 %}
|
{% if all_contacts.count > 2 %}
|
||||||
<details class="margin-top-1 dja-detail-table" aria-role="button" open>
|
<details class="margin-top-1 dja-detail-table" aria-role="button" open>
|
||||||
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
|
<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.title }}</td>
|
||||||
<td class="padding-left-1">
|
<td class="padding-left-1">
|
||||||
{{ contact.email }}
|
{{ contact.email }}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td class="padding-left-1">{{ contact.phone }}</td>
|
<td class="padding-left-1">{{ contact.phone }}</td>
|
||||||
<td class="padding-left-1 text-size-small">
|
<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 rejected_requests_count=user.get_rejected_requests_count %}
|
||||||
{% with ineligible_requests_count=user.get_ineligible_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 %}
|
{% if approved_domains_count|add:active_requests_count|add:rejected_requests_count|add:ineligible_requests_count > 0 %}
|
||||||
<ul class="dja-status-list">
|
<div class="flex-container">
|
||||||
{% if approved_domains_count > 0 %}
|
<label aria-label="User summary details"></label>
|
||||||
{# Approved domains #}
|
<ul class="dja-status-list">
|
||||||
<li>Approved domains: {{ approved_domains_count }}</li>
|
{% if approved_domains_count > 0 %}
|
||||||
{% endif %}
|
{# Approved domains #}
|
||||||
{% if active_requests_count > 0 %}
|
<li>Approved domains: {{ approved_domains_count }}</li>
|
||||||
{# Active requests #}
|
{% endif %}
|
||||||
<li>Active requests: {{ active_requests_count }}</li>
|
{% if active_requests_count > 0 %}
|
||||||
{% endif %}
|
{# Active requests #}
|
||||||
{% if rejected_requests_count > 0 %}
|
<li>Active requests: {{ active_requests_count }}</li>
|
||||||
{# Rejected requests #}
|
{% endif %}
|
||||||
<li>Rejected requests: {{ rejected_requests_count }}</li>
|
{% if rejected_requests_count > 0 %}
|
||||||
{% endif %}
|
{# Rejected requests #}
|
||||||
{% if ineligible_requests_count > 0 %}
|
<li>Rejected requests: {{ rejected_requests_count }}</li>
|
||||||
{# Ineligible requests #}
|
{% endif %}
|
||||||
<li>Ineligible requests: {{ ineligible_requests_count }}</li>
|
{% if ineligible_requests_count > 0 %}
|
||||||
{% endif %}
|
{# Ineligible requests #}
|
||||||
</ul>
|
<li>Ineligible requests: {{ ineligible_requests_count }}</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% 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()
|
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))
|
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
|
||||||
def test_extend_expiration_date_button(self, mock_date_today):
|
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
|
# Since we're using client to mock the request, we can only test against
|
||||||
# non-interpolated values
|
# non-interpolated values
|
||||||
expected_content = "<strong>Requested domain:</strong>"
|
expected_content = "Requested domain:"
|
||||||
expected_content2 = '<span class="scroll-indicator"></span>'
|
expected_content2 = '<span class="scroll-indicator"></span>'
|
||||||
expected_content3 = '<div class="submit-row-wrapper">'
|
expected_content3 = '<div class="submit-row-wrapper">'
|
||||||
not_expected_content = "submit-row-wrapper--analyst-view>"
|
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
|
# Since we're using client to mock the request, we can only test against
|
||||||
# non-interpolated values
|
# non-interpolated values
|
||||||
expected_content = "<strong>Requested domain:</strong>"
|
expected_content = "Requested domain:"
|
||||||
expected_content2 = '<span class="scroll-indicator"></span>'
|
expected_content2 = '<span class="scroll-indicator"></span>'
|
||||||
expected_content3 = '<div class="submit-row-wrapper submit-row-wrapper--analyst-view">'
|
expected_content3 = '<div class="submit-row-wrapper submit-row-wrapper--analyst-view">'
|
||||||
self.assertContains(request, expected_content)
|
self.assertContains(request, expected_content)
|
||||||
|
|
|
@ -7,6 +7,9 @@ from django.test import TestCase
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
User,
|
User,
|
||||||
Domain,
|
Domain,
|
||||||
|
DomainRequest,
|
||||||
|
Contact,
|
||||||
|
Website,
|
||||||
DomainInvitation,
|
DomainInvitation,
|
||||||
TransitionDomain,
|
TransitionDomain,
|
||||||
DomainInformation,
|
DomainInformation,
|
||||||
|
@ -18,7 +21,284 @@ from django.core.management import call_command
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
from epplibwrapper import commands, common
|
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):
|
class TestPopulateFirstReady(TestCase):
|
||||||
|
|
|
@ -87,10 +87,10 @@ def parse_row_for_domain(
|
||||||
if security_email.lower() in invalid_emails:
|
if security_email.lower() in invalid_emails:
|
||||||
security_email = "(blank)"
|
security_email = "(blank)"
|
||||||
|
|
||||||
if domain_info.federal_type and domain_info.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
|
if domain_info.federal_type and domain_info.organization_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
|
||||||
domain_type = f"{domain_info.get_generic_org_type_display()} - {domain_info.get_federal_type_display()}"
|
domain_type = f"{domain_info.get_organization_type_display()} - {domain_info.get_federal_type_display()}"
|
||||||
else:
|
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
|
# create a dictionary of fields which can be included in output
|
||||||
FIELDS = {
|
FIELDS = {
|
||||||
|
@ -319,9 +319,9 @@ def parse_row_for_requests(columns, request: DomainRequest):
|
||||||
requested_domain_name = request.requested_domain.name
|
requested_domain_name = request.requested_domain.name
|
||||||
|
|
||||||
if request.federal_type:
|
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:
|
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
|
# create a dictionary of fields which can be included in output
|
||||||
FIELDS = {
|
FIELDS = {
|
||||||
|
@ -399,7 +399,7 @@ def export_data_type_to_csv(csv_file):
|
||||||
|
|
||||||
# Coalesce is used to replace federal_type of None with ZZZZZ
|
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||||
sort_fields = [
|
sort_fields = [
|
||||||
"generic_org_type",
|
"organization_type",
|
||||||
Coalesce("federal_type", Value("ZZZZZ")),
|
Coalesce("federal_type", Value("ZZZZZ")),
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
"domain__name",
|
"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
|
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||||
sort_fields = [
|
sort_fields = [
|
||||||
"generic_org_type",
|
"organization_type",
|
||||||
Coalesce("federal_type", Value("ZZZZZ")),
|
Coalesce("federal_type", Value("ZZZZZ")),
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
"domain__name",
|
"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
|
# Coalesce is used to replace federal_type of None with ZZZZZ
|
||||||
sort_fields = [
|
sort_fields = [
|
||||||
"generic_org_type",
|
"organization_type",
|
||||||
Coalesce("federal_type", Value("ZZZZZ")),
|
Coalesce("federal_type", Value("ZZZZZ")),
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
]
|
]
|
||||||
filter_condition = {
|
filter_condition = {
|
||||||
"generic_org_type__icontains": "federal",
|
"organization_type__icontains": "federal",
|
||||||
"domain__state__in": [
|
"domain__state__in": [
|
||||||
Domain.State.READY,
|
Domain.State.READY,
|
||||||
Domain.State.DNS_NEEDED,
|
Domain.State.DNS_NEEDED,
|
||||||
|
@ -601,7 +601,6 @@ def get_sliced_domains(filter_condition):
|
||||||
|
|
||||||
def get_sliced_requests(filter_condition):
|
def get_sliced_requests(filter_condition):
|
||||||
"""Get filtered requests counts sliced by org type and election office."""
|
"""Get filtered requests counts sliced by org type and election office."""
|
||||||
|
|
||||||
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
|
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
|
||||||
requests_count = requests.count()
|
requests_count = requests.count()
|
||||||
federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
|
federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue