Merge branch 'main' of https://github.com/cisagov/manage.get.gov into rh/2909-new-agency-field

This commit is contained in:
Rebecca Hsieh 2024-04-18 15:44:28 -07:00
commit c997d150e6
No known key found for this signature in database
26 changed files with 1290 additions and 99 deletions

View file

@ -586,3 +586,59 @@ Example: `cf ssh getgov-za`
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **debug** | Increases logging detail. Defaults to False. |
## Populate Organization type
This section outlines how to run the `populate_organization_type` script.
The script is used to update the organization_type field on DomainRequest and DomainInformation when it is None.
That data are synthesized from the generic_org_type field and the is_election_board field by concatenating " - Elections" on the end of generic_org_type string if is_elections_board is True.
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: Get the domain_election_board file
The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
After downloading this file, place it in `src/migrationdata`
#### Step 2: Upload the domain_election_board file to your sandbox
Follow [Step 1: Transfer data to sandboxes](#step-1-transfer-data-to-sandboxes) and [Step 2: Transfer uploaded files to the getgov directory](#step-2-transfer-uploaded-files-to-the-getgov-directory) from the [Set Up Migrations on Sandbox](#set-up-migrations-on-sandbox) portion of this doc.
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 4: Running the script
```./manage.py populate_organization_type {domain_election_board_filename}```
- The domain_election_board_filename file must adhere to this format:
- example.gov\
example2.gov\
example3.gov
Example:
`./manage.py populate_organization_type migrationdata/election-domains.csv`
### Running locally
#### Step 1: Get the domain_election_board file
The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
After downloading this file, place it in `src/migrationdata`
#### Step 2: Running the script
```docker-compose exec app ./manage.py populate_organization_type {domain_election_board_filename}```
Example (assuming that this is being ran from src/):
`docker-compose exec app ./manage.py populate_organization_type migrationdata/election-domains.csv`
### Required parameters
| | Parameter | Description |
|:-:|:------------------------------------|:-------------------------------------------------------------------|
| 1 | **domain_election_board_filename** | A file containing every domain that is an election office.

View file

@ -290,6 +290,13 @@ class CustomLogEntryAdmin(LogEntryAdmin):
# Return the field value without a link
return f"{obj.content_type} - {obj.object_repr}"
# We name the custom prop 'created_at' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
@admin.display(description=_("Created at"))
def created(self, obj):
return obj.timestamp
search_help_text = "Search by resource, changes, or user."
change_form_template = "admin/change_form_no_submit.html"
@ -478,7 +485,7 @@ class MyUserAdmin(BaseUserAdmin):
list_display = (
"username",
"email",
"overridden_email_field",
"first_name",
"last_name",
# Group is a custom property defined within this file,
@ -487,6 +494,18 @@ class MyUserAdmin(BaseUserAdmin):
"status",
)
# Renames inherited AbstractUser label 'email_address to 'email'
def formfield_for_dbfield(self, dbfield, **kwargs):
field = super().formfield_for_dbfield(dbfield, **kwargs)
if dbfield.name == "email":
field.label = "Email"
return field
# Renames inherited AbstractUser column name 'email_address to 'email'
@admin.display(description=_("Email"))
def overridden_email_field(self, obj):
return obj.email
fieldsets = (
(
None,
@ -561,6 +580,7 @@ class MyUserAdmin(BaseUserAdmin):
# this ordering effects the ordering of results
# in autocomplete_fields for user
ordering = ["first_name", "last_name", "email"]
search_help_text = "Search by first name, last name, or email."
change_form_template = "django/admin/email_clipboard_change_form.html"
@ -651,7 +671,7 @@ class MyHostAdmin(AuditedAdmin):
"""Custom host admin class to use our inlines."""
search_fields = ["name", "domain__name"]
search_help_text = "Search by domain or hostname."
search_help_text = "Search by domain or host name."
inlines = [HostIPInline]
@ -659,9 +679,9 @@ class ContactAdmin(ListHeaderAdmin):
"""Custom contact admin class to add search."""
search_fields = ["email", "first_name", "last_name"]
search_help_text = "Search by firstname, lastname or email."
search_help_text = "Search by first name, last name or email."
list_display = [
"contact",
"name",
"email",
"user_exists",
]
@ -690,7 +710,7 @@ class ContactAdmin(ListHeaderAdmin):
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
def contact(self, obj: models.Contact):
def name(self, obj: models.Contact):
"""Duplicate the contact _str_"""
if obj.first_name or obj.last_name:
return obj.get_formatted_name()
@ -701,7 +721,7 @@ class ContactAdmin(ListHeaderAdmin):
else:
return ""
contact.admin_order_field = "first_name" # type: ignore
name.admin_order_field = "first_name" # type: ignore
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
@ -859,7 +879,7 @@ class UserDomainRoleAdmin(ListHeaderAdmin):
"domain__name",
"role",
]
search_help_text = "Search by firstname, lastname, email, domain, or role."
search_help_text = "Search by first name, last name, email, or domain."
autocomplete_fields = ["user", "domain"]
@ -1513,10 +1533,11 @@ class DomainInformationInline(admin.StackedInline):
We had issues inheriting from both StackedInline
and the source DomainInformationAdmin since these
classes conflict, so we'll just pull what we need
from DomainInformationAdmin"""
from DomainInformationAdmin
"""
form = DomainInformationInlineForm
template = "django/admin/includes/domain_info_inline_stacked.html"
model = models.DomainInformation
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
@ -1526,10 +1547,8 @@ class DomainInformationInline(admin.StackedInline):
del fieldsets[index]
break
readonly_fields = DomainInformationAdmin.readonly_fields
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)
autocomplete_fields = [
"creator",
@ -1660,6 +1679,7 @@ class DomainAdmin(ListHeaderAdmin):
city.admin_order_field = "domain_info__city" # type: ignore
@admin.display(description=_("State / territory"))
def state_territory(self, obj):
return obj.domain_info.state_territory if obj.domain_info else None
@ -1695,11 +1715,15 @@ class DomainAdmin(ListHeaderAdmin):
if extra_context is None:
extra_context = {}
# Pass in what the an extended expiration date would be for the expiration date modal
if object_id is not None:
domain = Domain.objects.get(pk=object_id)
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
# Used in the custom contact view
if domain is not None and hasattr(domain, "domain_info"):
extra_context["original_object"] = domain.domain_info
# Pass in what the an extended expiration date would be for the expiration date modal
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
try:
curr_exp_date = domain.registry_expiration_date
except KeyError:
@ -1970,6 +1994,11 @@ class DraftDomainAdmin(ListHeaderAdmin):
# this ordering effects the ordering of results
# in autocomplete_fields for user
ordering = ["name"]
list_display = ["name"]
@admin.display(description=_("Requested domain"))
def name(self, obj):
return obj.name
def get_model_perms(self, request):
"""
@ -2048,13 +2077,36 @@ class FederalAgencyAdmin(ListHeaderAdmin):
ordering = ["agency"]
class UserGroupAdmin(AuditedAdmin):
"""Overwrite the generated UserGroup admin class"""
list_display = ["user_group"]
fieldsets = ((None, {"fields": ("name", "permissions")}),)
def formfield_for_dbfield(self, dbfield, **kwargs):
field = super().formfield_for_dbfield(dbfield, **kwargs)
if dbfield.name == "name":
field.label = "Group name"
if dbfield.name == "permissions":
field.label = "User permissions"
return field
# We name the custom prop 'Group' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
@admin.display(description=_("Group"))
def user_group(self, obj):
return obj.name
admin.site.unregister(LogEntry) # Unregister the default registration
admin.site.register(LogEntry, CustomLogEntryAdmin)
admin.site.register(models.User, MyUserAdmin)
# Unregister the built-in Group model
admin.site.unregister(Group)
# Register UserGroup
admin.site.register(models.UserGroup)
admin.site.register(models.UserGroup, UserGroupAdmin)
admin.site.register(models.UserDomainRole, UserDomainRoleAdmin)
admin.site.register(models.Contact, ContactAdmin)
admin.site.register(models.DomainInvitation, DomainInvitationAdmin)

View file

@ -495,6 +495,8 @@ address.dja-address-contact-list {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: medium;
padding-top: 3px !important;
}
}
@ -505,6 +507,7 @@ address.dja-address-contact-list {
@media screen and (min-width:768px) {
.visible-768 {
display: block;
padding-top: 0;
}
}
@ -593,6 +596,7 @@ address.dja-address-contact-list {
right: auto;
left: 4px;
height: 100%;
top: -1px;
}
button {
font-size: unset !important;

View 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)

View file

@ -49,6 +49,7 @@ class ScriptDataHelper:
Usage:
bulk_update_fields(Domain, page.object_list, ["first_ready"])
"""
logger.info(f"{TerminalColors.YELLOW} Bulk updating fields... {TerminalColors.ENDC}")
# Create a Paginator object. Bulk_update on the full dataset
# is too memory intensive for our current app config, so we can chunk this data instead.
paginator = Paginator(update_list, batch_size)
@ -59,13 +60,16 @@ class ScriptDataHelper:
class TerminalHelper:
@staticmethod
def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool):
def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool, log_header=None):
"""Prints success, failed, and skipped counts, as well as
all affected objects."""
update_success_count = len(to_update)
update_failed_count = len(failed_to_update)
update_skipped_count = len(skipped)
if log_header is None:
log_header = "============= FINISHED ==============="
# Prepare debug messages
debug_messages = {
"success": (f"{TerminalColors.OKCYAN}Updated: {to_update}{TerminalColors.ENDC}\n"),
@ -85,7 +89,7 @@ class TerminalHelper:
if update_failed_count == 0 and update_skipped_count == 0:
logger.info(
f"""{TerminalColors.OKGREEN}
============= FINISHED ===============
{log_header}
Updated {update_success_count} entries
{TerminalColors.ENDC}
"""
@ -93,7 +97,7 @@ class TerminalHelper:
elif update_failed_count == 0:
logger.warning(
f"""{TerminalColors.YELLOW}
============= FINISHED ===============
{log_header}
Updated {update_success_count} entries
----- SOME DATA WAS INVALID (NEEDS MANUAL PATCHING) -----
Skipped updating {update_skipped_count} entries
@ -103,7 +107,7 @@ class TerminalHelper:
else:
logger.error(
f"""{TerminalColors.FAIL}
============= FINISHED ===============
{log_header}
Updated {update_success_count} entries
----- UPDATE FAILED -----
Failed to update {update_failed_count} entries,

View file

@ -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",
),
),
]

View file

@ -18,7 +18,7 @@ class Contact(TimeStampedModel):
first_name = models.CharField(
null=True,
blank=True,
verbose_name="first name / given name",
verbose_name="first name",
db_index=True,
)
middle_name = models.CharField(
@ -28,13 +28,13 @@ class Contact(TimeStampedModel):
last_name = models.CharField(
null=True,
blank=True,
verbose_name="last name / family name",
verbose_name="last name",
db_index=True,
)
title = models.CharField(
null=True,
blank=True,
verbose_name="title or role in your organization",
verbose_name="title / role",
)
email = models.EmailField(
null=True,

View file

@ -992,6 +992,7 @@ class Domain(TimeStampedModel, DomainHelper):
blank=False,
default=None, # prevent saving without a value
unique=True,
verbose_name="domain",
help_text="Fully qualified domain name",
)
@ -1000,6 +1001,7 @@ class Domain(TimeStampedModel, DomainHelper):
choices=State.choices,
default=State.UNKNOWN,
protected=True, # cannot change state directly, particularly in Django admin
verbose_name="domain state",
help_text="Very basic info about the lifecycle of this domain object",
)
@ -1017,12 +1019,14 @@ class Domain(TimeStampedModel, DomainHelper):
deleted = DateField(
null=True,
editable=False,
verbose_name="deleted on",
help_text="Deleted at date",
)
first_ready = DateField(
null=True,
editable=False,
verbose_name="first ready on",
help_text="The last time this domain moved into the READY state",
)

View file

@ -72,6 +72,7 @@ class DomainInformation(TimeStampedModel):
is_election_board = models.BooleanField(
null=True,
blank=True,
verbose_name="election office",
help_text="Is your organization an election office?",
)
@ -118,6 +119,7 @@ class DomainInformation(TimeStampedModel):
is_election_board = models.BooleanField(
null=True,
blank=True,
verbose_name="election office",
help_text="Is your organization an election office?",
)
@ -131,13 +133,13 @@ class DomainInformation(TimeStampedModel):
null=True,
blank=True,
help_text="Street address",
verbose_name="Street address",
verbose_name="address line 1",
)
address_line2 = models.CharField(
null=True,
blank=True,
help_text="Street address line 2 (optional)",
verbose_name="Street address line 2 (optional)",
verbose_name="address line 2",
)
city = models.CharField(
null=True,
@ -149,21 +151,22 @@ class DomainInformation(TimeStampedModel):
choices=StateTerritoryChoices.choices,
null=True,
blank=True,
verbose_name="state / territory",
help_text="State, territory, or military post",
verbose_name="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
help_text="Zip code",
verbose_name="zip code",
db_index=True,
)
urbanization = models.CharField(
null=True,
blank=True,
help_text="Urbanization (required for Puerto Rico only)",
verbose_name="Urbanization (required for Puerto Rico only)",
verbose_name="urbanization",
)
about_your_organization = models.TextField(
@ -246,14 +249,17 @@ class DomainInformation(TimeStampedModel):
except Exception:
return ""
def save(self, *args, **kwargs):
"""Save override for custom properties"""
def sync_organization_type(self):
"""
Updates the organization_type (without saving) to match
the is_election_board and generic_organization_type fields.
"""
# Define mappings between generic org and election org.
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
# For any given organization type, return the "_election" variant.
# For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()
@ -272,6 +278,12 @@ class DomainInformation(TimeStampedModel):
# Actually updates the organization_type field
org_type_helper.create_or_update_organization_type()
return self
def save(self, *args, **kwargs):
"""Save override for custom properties"""
self.sync_organization_type()
super().save(*args, **kwargs)
@classmethod

View file

@ -487,6 +487,7 @@ class DomainRequest(TimeStampedModel):
is_election_board = models.BooleanField(
null=True,
blank=True,
verbose_name="election office",
help_text="Is your organization an election office?",
)
@ -559,12 +560,14 @@ class DomainRequest(TimeStampedModel):
choices=StateTerritoryChoices.choices,
null=True,
blank=True,
verbose_name="state / territory",
help_text="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
verbose_name="zip code",
help_text="Zip code",
db_index=True,
)
@ -666,6 +669,7 @@ class DomainRequest(TimeStampedModel):
null=True,
blank=True,
default=None,
verbose_name="submitted at",
help_text="Date submitted",
)
@ -675,14 +679,16 @@ class DomainRequest(TimeStampedModel):
help_text="Notes about this request",
)
def save(self, *args, **kwargs):
"""Save override for custom properties"""
def sync_organization_type(self):
"""
Updates the organization_type (without saving) to match
the is_election_board and generic_organization_type fields.
"""
# Define mappings between generic org and election org.
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
# For any given organization type, return the "_election" variant.
# For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = self.OrgChoicesElectionOffice.get_org_generic_to_org_election()
@ -701,6 +707,10 @@ class DomainRequest(TimeStampedModel):
# Actually updates the organization_type field
org_type_helper.create_or_update_organization_type()
def save(self, *args, **kwargs):
"""Save override for custom properties"""
self.sync_organization_type()
super().save(*args, **kwargs)
def __str__(self):

View file

@ -18,5 +18,6 @@ class DraftDomain(TimeStampedModel, DomainHelper):
max_length=253,
blank=False,
default=None, # prevent saving without a value
verbose_name="requested domain",
help_text="Fully qualified domain name",
)

View file

@ -21,6 +21,7 @@ class Host(TimeStampedModel):
blank=False,
default=None, # prevent saving without a value
unique=False,
verbose_name="host name",
help_text="Fully qualified domain name",
)

View file

@ -20,6 +20,7 @@ class HostIP(TimeStampedModel):
blank=False,
default=None, # prevent saving without a value
validators=[validate_ipv46_address],
verbose_name="IP address",
help_text="IP address",
)

View file

@ -20,13 +20,13 @@ class TransitionDomain(TimeStampedModel):
username = models.CharField(
null=False,
blank=False,
verbose_name="Username",
verbose_name="username",
help_text="Username - this will be an email address",
)
domain_name = models.CharField(
null=True,
blank=True,
verbose_name="Domain name",
verbose_name="domain",
)
status = models.CharField(
max_length=255,
@ -34,7 +34,7 @@ class TransitionDomain(TimeStampedModel):
blank=True,
default=StatusChoices.READY,
choices=StatusChoices.choices,
verbose_name="Status",
verbose_name="status",
help_text="domain status during the transfer",
)
email_sent = models.BooleanField(
@ -46,7 +46,7 @@ class TransitionDomain(TimeStampedModel):
processed = models.BooleanField(
null=False,
default=True,
verbose_name="Processed",
verbose_name="processed",
help_text="Indicates whether this TransitionDomain was already processed",
)
generic_org_type = models.CharField(
@ -83,8 +83,8 @@ class TransitionDomain(TimeStampedModel):
first_name = models.CharField(
null=True,
blank=True,
help_text="First name",
verbose_name="first name / given name",
help_text="First name / given name",
verbose_name="first name",
db_index=True,
)
middle_name = models.CharField(
@ -100,6 +100,7 @@ class TransitionDomain(TimeStampedModel):
title = models.CharField(
null=True,
blank=True,
verbose_name="title / role",
help_text="Title",
)
email = models.EmailField(
@ -126,12 +127,14 @@ class TransitionDomain(TimeStampedModel):
max_length=2,
null=True,
blank=True,
verbose_name="state / territory",
help_text="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
verbose_name="zip code",
help_text="Zip code",
db_index=True,
)

View file

@ -33,6 +33,7 @@ class User(AbstractUser):
default=None, # Set the default value to None
null=True, # Allow the field to be null
blank=True, # Allow the field to be blank
verbose_name="user status",
)
domains = models.ManyToManyField(

View file

@ -49,7 +49,7 @@ class CreateOrUpdateOrganizationTypeHelper:
self.generic_org_to_org_map = generic_org_to_org_map
self.election_org_to_generic_org_map = election_org_to_generic_org_map
def create_or_update_organization_type(self):
def create_or_update_organization_type(self, force_update=False):
"""The organization_type field on DomainRequest and DomainInformation is consituted from the
generic_org_type and is_election_board fields. To keep the organization_type
field up to date, we need to update it before save based off of those field
@ -59,6 +59,14 @@ class CreateOrUpdateOrganizationTypeHelper:
one of the excluded types (FEDERAL, INTERSTATE, SCHOOL_DISTRICT), the
organization_type is set to a corresponding election variant. Otherwise, it directly
mirrors the generic_org_type value.
args:
force_update (bool): If an existing instance has no values to change,
try to update the organization_type field (or related fields) anyway.
This is done by invoking the new instance handler.
Use to force org type to be updated to the correct value even
if no other changes were made (including is_election).
"""
# A new record is added with organization_type not defined.
@ -67,7 +75,7 @@ class CreateOrUpdateOrganizationTypeHelper:
if is_new_instance:
self._handle_new_instance()
else:
self._handle_existing_instance()
self._handle_existing_instance(force_update)
return self.instance
@ -92,7 +100,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
def _handle_existing_instance(self):
def _handle_existing_instance(self, force_update_when_no_are_changes_found=False):
# == Init variables == #
# Instance is already in the database, fetch its current state
current_instance = self.sender.objects.get(id=self.instance.id)
@ -109,10 +117,12 @@ class CreateOrUpdateOrganizationTypeHelper:
# This will not happen in normal flow as it is not possible otherwise.
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
# No values to update - do nothing
return None
# == Program flow will halt here if there is no reason to update == #
# No changes found
if force_update_when_no_are_changes_found:
# If we want to force an update anyway, we can treat this record like
# its a new one in that we check for "None" values rather than changes.
self._handle_new_instance()
else:
# == Update the linked values == #
# Find out which field needs updating
organization_type_needs_update = generic_org_type_changed or is_election_board_changed

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

View file

@ -9,7 +9,9 @@
{% include "django/admin/includes/domain_information_fieldset.html" %}
Use detail_table_fieldset as an example, or just extend it.
original_object is just a variable name for "DomainInformation" or "DomainRequest"
{% endcomment %}
{% include "django/admin/includes/detail_table_fieldset.html" %}
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
{% endfor %}
{% endblock %}

View file

@ -13,8 +13,10 @@
{% include "django/admin/includes/domain_information_fieldset.html" %}
Use detail_table_fieldset as an example, or just extend it.
original_object is just a variable name for "DomainInformation" or "DomainRequest"
{% endcomment %}
{% include "django/admin/includes/detail_table_fieldset.html" %}
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
{% endfor %}
{% endblock %}
@ -116,8 +118,8 @@
</button>
</span>
<p class="text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
<strong>Requested domain:</strong> {{ original.requested_domain.name }}
<p class="padding-top-05 text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
Requested domain: <strong>{{ original.requested_domain.name }}</strong>
</p>
{{ block.super }}
</div>

View file

@ -5,7 +5,7 @@
This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endcomment %}
{% block field_readonly %}
{% with all_contacts=original.other_contacts.all %}
{% with all_contacts=original_object.other_contacts.all %}
{% if field.field.name == "other_contacts" %}
{% if all_contacts.count > 2 %}
<div class="readonly">
@ -54,7 +54,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% elif field.field.name == "alternative_domains" %}
<div class="readonly">
{% with current_path=request.get_full_path %}
{% for alt_domain in original.alternative_domains.all %}
{% for alt_domain in original_object.alternative_domains.all %}
<a href="{% url 'admin:registrar_website_change' alt_domain.id %}?{{ 'return_path='|add:current_path }}">{{ alt_domain }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endwith %}
@ -69,24 +69,21 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% if field.field.name == "creator" %}
<div class="flex-container">
<label aria-label="Creator contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %}
</div>
<div class="flex-container">
<label aria-label="User summary details"></label>
{% include "django/admin/includes/user_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %}
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
</div>
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
{% elif field.field.name == "submitter" %}
<div class="flex-container">
<label aria-label="Submitter contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original.submitter no_title_top_padding=field.is_readonly %}
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
</div>
{% elif field.field.name == "authorizing_official" %}
<div class="flex-container">
<label aria-label="Authorizing official contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original.authorizing_official no_title_top_padding=field.is_readonly %}
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.authorizing_official no_title_top_padding=field.is_readonly %}
</div>
{% elif field.field.name == "other_contacts" and original.other_contacts.all %}
{% with all_contacts=original.other_contacts.all %}
{% elif field.field.name == "other_contacts" and original_object.other_contacts.all %}
{% with all_contacts=original_object.other_contacts.all %}
{% if all_contacts.count > 2 %}
<details class="margin-top-1 dja-detail-table" aria-role="button" open>
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
@ -104,7 +101,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
<td class="padding-left-1">{{ contact.title }}</td>
<td class="padding-left-1">
{{ contact.email }}
</td>
<td class="padding-left-1">{{ contact.phone }}</td>
<td class="padding-left-1 text-size-small">

View file

@ -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 %}

View file

@ -5,6 +5,8 @@
{% with rejected_requests_count=user.get_rejected_requests_count %}
{% with ineligible_requests_count=user.get_ineligible_requests_count %}
{% if approved_domains_count|add:active_requests_count|add:rejected_requests_count|add:ineligible_requests_count > 0 %}
<div class="flex-container">
<label aria-label="User summary details"></label>
<ul class="dja-status-list">
{% if approved_domains_count > 0 %}
{# Approved domains #}
@ -23,6 +25,7 @@
<li>Ineligible requests: {{ ineligible_requests_count }}</li>
{% endif %}
</ul>
</div>
{% endif %}
{% endwith %}
{% endwith %}

View file

@ -0,0 +1 @@
manualtransmission.gov
1 manualtransmission.gov

View file

@ -85,6 +85,78 @@ class TestDomainAdmin(MockEppLib, WebTest):
)
super().setUp()
@less_console_noise_decorator
def test_contact_fields_on_domain_change_form_have_detail_table(self):
"""Tests if the contact fields in the inlined Domain information have the detail table
which displays title, email, and phone"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Due to the relation between User <==> Contact,
# the underlying contact has to be modified this way.
_creator.contact.email = "meoward.jones@igorville.gov"
_creator.contact.phone = "(555) 123 12345"
_creator.contact.title = "Treat inspector"
_creator.contact.save()
# Create a fake domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
domain_request.approve()
_domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
domain = Domain.objects.filter(domain_info=_domain_info).get()
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Check that the fields have the right values.
# == Check for the creator == #
# Check for the right title, email, and phone number in the response.
# We only need to check for the end tag
# (Otherwise this test will fail if we change classes, etc)
self.assertContains(response, "Treat inspector")
self.assertContains(response, "meoward.jones@igorville.gov")
self.assertContains(response, "(555) 123 12345")
# Check for the field itself
self.assertContains(response, "Meoward Jones")
# == Check for the submitter == #
self.assertContains(response, "mayor@igorville.gov")
self.assertContains(response, "Admin Tester")
self.assertContains(response, "(555) 555 5556")
self.assertContains(response, "Testy2 Tester2")
# == Check for the authorizing_official == #
self.assertContains(response, "testy@town.com")
self.assertContains(response, "Chief Tester")
self.assertContains(response, "(555) 555 5555")
# Includes things like readonly fields
self.assertContains(response, "Testy Tester")
# == Test the other_employees field == #
self.assertContains(response, "testy2@town.com")
self.assertContains(response, "Another Tester")
self.assertContains(response, "(555) 555 5557")
# Test for the copy link
self.assertContains(response, "usa-button__clipboard")
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
def test_extend_expiration_date_button(self, mock_date_today):
"""
@ -1509,7 +1581,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Since we're using client to mock the request, we can only test against
# non-interpolated values
expected_content = "<strong>Requested domain:</strong>"
expected_content = "Requested domain:"
expected_content2 = '<span class="scroll-indicator"></span>'
expected_content3 = '<div class="submit-row-wrapper">'
not_expected_content = "submit-row-wrapper--analyst-view>"
@ -1538,7 +1610,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Since we're using client to mock the request, we can only test against
# non-interpolated values
expected_content = "<strong>Requested domain:</strong>"
expected_content = "Requested domain:"
expected_content2 = '<span class="scroll-indicator"></span>'
expected_content3 = '<div class="submit-row-wrapper submit-row-wrapper--analyst-view">'
self.assertContains(request, expected_content)

View file

@ -7,6 +7,9 @@ from django.test import TestCase
from registrar.models import (
User,
Domain,
DomainRequest,
Contact,
Website,
DomainInvitation,
TransitionDomain,
DomainInformation,
@ -18,7 +21,284 @@ from django.core.management import call_command
from unittest.mock import patch, call
from epplibwrapper import commands, common
from .common import MockEppLib, less_console_noise
from .common import MockEppLib, less_console_noise, completed_domain_request
from api.tests.common import less_console_noise_decorator
class TestPopulateOrganizationType(MockEppLib):
"""Tests for the populate_organization_type script"""
def setUp(self):
"""Creates a fake domain object"""
super().setUp()
# Get the domain requests
self.domain_request_1 = completed_domain_request(
name="lasers.gov",
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
is_election_board=True,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
self.domain_request_2 = completed_domain_request(
name="readysetgo.gov",
generic_org_type=DomainRequest.OrganizationChoices.CITY,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
self.domain_request_3 = completed_domain_request(
name="manualtransmission.gov",
generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
self.domain_request_4 = completed_domain_request(
name="saladandfries.gov",
generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
is_election_board=True,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
# Approve all three requests
self.domain_request_1.approve()
self.domain_request_2.approve()
self.domain_request_3.approve()
self.domain_request_4.approve()
# Get the domains
self.domain_1 = Domain.objects.get(name="lasers.gov")
self.domain_2 = Domain.objects.get(name="readysetgo.gov")
self.domain_3 = Domain.objects.get(name="manualtransmission.gov")
self.domain_4 = Domain.objects.get(name="saladandfries.gov")
# Get the domain infos
self.domain_info_1 = DomainInformation.objects.get(domain=self.domain_1)
self.domain_info_2 = DomainInformation.objects.get(domain=self.domain_2)
self.domain_info_3 = DomainInformation.objects.get(domain=self.domain_3)
self.domain_info_4 = DomainInformation.objects.get(domain=self.domain_4)
def tearDown(self):
"""Deletes all DB objects related to migrations"""
super().tearDown()
# Delete domains and related information
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
User.objects.all().delete()
Contact.objects.all().delete()
Website.objects.all().delete()
@less_console_noise_decorator
def run_populate_organization_type(self):
"""
This method executes the populate_organization_type command.
The 'call_command' function from Django's management framework is then used to
execute the populate_organization_type command with the specified arguments.
"""
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv")
def assert_expected_org_values_on_request_and_info(
self,
domain_request: DomainRequest,
domain_info: DomainInformation,
expected_values: dict,
):
"""
This is a helper function that tests the following conditions:
1. DomainRequest and DomainInformation (on given objects) are equivalent
2. That generic_org_type, is_election_board, and organization_type are equal to passed in values
Args:
domain_request (DomainRequest): The DomainRequest object to test
domain_info (DomainInformation): The DomainInformation object to test
expected_values (dict): Container for what we expect is_electionboard, generic_org_type,
and organization_type to be on DomainRequest and DomainInformation.
Example:
expected_values = {
"is_election_board": False,
"generic_org_type": DomainRequest.OrganizationChoices.CITY,
"organization_type": DomainRequest.OrgChoicesElectionOffice.CITY,
}
"""
# Test domain request
with self.subTest(field="DomainRequest"):
self.assertEqual(domain_request.generic_org_type, expected_values["generic_org_type"])
self.assertEqual(domain_request.is_election_board, expected_values["is_election_board"])
self.assertEqual(domain_request.organization_type, expected_values["organization_type"])
# Test domain info
with self.subTest(field="DomainInformation"):
self.assertEqual(domain_info.generic_org_type, expected_values["generic_org_type"])
self.assertEqual(domain_info.is_election_board, expected_values["is_election_board"])
self.assertEqual(domain_info.organization_type, expected_values["organization_type"])
def do_nothing(self):
"""Does nothing for mocking purposes"""
pass
def test_request_and_info_city_not_in_csv(self):
"""
Tests what happens to a city domain that is not defined in the CSV.
Scenario: A domain request (of type city) is made that is not defined in the CSV file.
When a domain request is made for a city that is not listed in the CSV,
Then the `is_election_board` value should remain False,
and the `generic_org_type` and `organization_type` should both be `city`.
Expected Result: The `is_election_board` and `generic_org_type` attributes should be unchanged.
The `organization_type` field should now be `city`.
"""
city_request = self.domain_request_2
city_info = self.domain_request_2
# Make sure that all data is correct before proceeding.
# Since the presave fixture is in effect, we should expect that
# is_election_board is equal to none, even though we tried to define it as "True"
expected_values = {
"is_election_board": False,
"generic_org_type": DomainRequest.OrganizationChoices.CITY,
"organization_type": DomainRequest.OrgChoicesElectionOffice.CITY,
}
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
# Run the populate script
try:
self.run_populate_organization_type()
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
# All values should be the same
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
def test_request_and_info_federal(self):
"""
Tests what happens to a federal domain after the script is run (should be unchanged).
Scenario: A domain request (of type federal) is processed after running the populate_organization_type script.
When a federal domain request is made,
Then the `is_election_board` value should remain None,
and the `generic_org_type` and `organization_type` fields should both be `federal`.
Expected Result: The `is_election_board` and `generic_org_type` attributes should be unchanged.
The `organization_type` field should now be `federal`.
"""
federal_request = self.domain_request_1
federal_info = self.domain_info_1
# Make sure that all data is correct before proceeding.
# Since the presave fixture is in effect, we should expect that
# is_election_board is equal to none, even though we tried to define it as "True"
expected_values = {
"is_election_board": None,
"generic_org_type": DomainRequest.OrganizationChoices.FEDERAL,
"organization_type": DomainRequest.OrgChoicesElectionOffice.FEDERAL,
}
self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
# Run the populate script
try:
self.run_populate_organization_type()
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
# All values should be the same
self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
def test_request_and_info_tribal_add_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL - ELECTION
for the domain request and the domain info
"""
# Set org type fields to none to mimic an environment without this data
tribal_request = self.domain_request_3
tribal_request.organization_type = None
tribal_info = self.domain_info_3
tribal_info.organization_type = None
with patch.object(DomainRequest, "sync_organization_type", self.do_nothing):
with patch.object(DomainInformation, "sync_organization_type", self.do_nothing):
tribal_request.save()
tribal_info.save()
# Make sure that all data is correct before proceeding.
expected_values = {
"is_election_board": False,
"generic_org_type": DomainRequest.OrganizationChoices.TRIBAL,
"organization_type": None,
}
self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
# Run the populate script
try:
self.run_populate_organization_type()
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
tribal_request.refresh_from_db()
tribal_info.refresh_from_db()
# Because we define this in the "csv", we expect that is election board will switch to True,
# and organization_type will now be tribal_election
expected_values["is_election_board"] = True
expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
def test_request_and_info_tribal_doesnt_remove_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL_ELECTION
when the is_election_board is True, and generic_org_type is Tribal when it is not
present in the CSV.
To avoid overwriting data, the script should not set any domain specified as
an election_office (that doesn't exist in the CSV) to false.
"""
# Set org type fields to none to mimic an environment without this data
tribal_election_request = self.domain_request_4
tribal_election_info = self.domain_info_4
tribal_election_request.organization_type = None
tribal_election_info.organization_type = None
with patch.object(DomainRequest, "sync_organization_type", self.do_nothing):
with patch.object(DomainInformation, "sync_organization_type", self.do_nothing):
tribal_election_request.save()
tribal_election_info.save()
# Make sure that all data is correct before proceeding.
# Because the presave fixture is in place when creating this, we should expect that the
# organization_type variable is already pre-populated. We will test what happens when
# it is not in another test.
expected_values = {
"is_election_board": True,
"generic_org_type": DomainRequest.OrganizationChoices.TRIBAL,
"organization_type": None,
}
self.assert_expected_org_values_on_request_and_info(
tribal_election_request, tribal_election_info, expected_values
)
# Run the populate script
try:
self.run_populate_organization_type()
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
# If we don't define this in the "csv", but the value was already true,
# we expect that is election board will stay True, and the org type will be tribal,
# and organization_type will now be tribal_election
expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
tribal_election_request.refresh_from_db()
tribal_election_info.refresh_from_db()
self.assert_expected_org_values_on_request_and_info(
tribal_election_request, tribal_election_info, expected_values
)
class TestPopulateFirstReady(TestCase):

View file

@ -87,10 +87,10 @@ def parse_row_for_domain(
if security_email.lower() in invalid_emails:
security_email = "(blank)"
if domain_info.federal_type and domain_info.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
domain_type = f"{domain_info.get_generic_org_type_display()} - {domain_info.get_federal_type_display()}"
if domain_info.federal_type and domain_info.organization_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
domain_type = f"{domain_info.get_organization_type_display()} - {domain_info.get_federal_type_display()}"
else:
domain_type = domain_info.get_generic_org_type_display()
domain_type = domain_info.get_organization_type_display()
# create a dictionary of fields which can be included in output
FIELDS = {
@ -319,9 +319,9 @@ def parse_row_for_requests(columns, request: DomainRequest):
requested_domain_name = request.requested_domain.name
if request.federal_type:
request_type = f"{request.get_generic_org_type_display()} - {request.get_federal_type_display()}"
request_type = f"{request.get_organization_type_display()} - {request.get_federal_type_display()}"
else:
request_type = request.get_generic_org_type_display()
request_type = request.get_organization_type_display()
# create a dictionary of fields which can be included in output
FIELDS = {
@ -399,7 +399,7 @@ def export_data_type_to_csv(csv_file):
# Coalesce is used to replace federal_type of None with ZZZZZ
sort_fields = [
"generic_org_type",
"organization_type",
Coalesce("federal_type", Value("ZZZZZ")),
"federal_agency",
"domain__name",
@ -432,7 +432,7 @@ def export_data_full_to_csv(csv_file):
]
# Coalesce is used to replace federal_type of None with ZZZZZ
sort_fields = [
"generic_org_type",
"organization_type",
Coalesce("federal_type", Value("ZZZZZ")),
"federal_agency",
"domain__name",
@ -465,13 +465,13 @@ def export_data_federal_to_csv(csv_file):
]
# Coalesce is used to replace federal_type of None with ZZZZZ
sort_fields = [
"generic_org_type",
"organization_type",
Coalesce("federal_type", Value("ZZZZZ")),
"federal_agency",
"domain__name",
]
filter_condition = {
"generic_org_type__icontains": "federal",
"organization_type__icontains": "federal",
"domain__state__in": [
Domain.State.READY,
Domain.State.DNS_NEEDED,
@ -601,7 +601,6 @@ def get_sliced_domains(filter_condition):
def get_sliced_requests(filter_condition):
"""Get filtered requests counts sliced by org type and election office."""
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
requests_count = requests.count()
federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()