Merge branch 'main' into nl/1895-CISA-regions-questions-domain-req

This commit is contained in:
zandercymatics 2024-04-18 11:07:37 -06:00
commit a735b0376e
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
24 changed files with 1087 additions and 94 deletions

View file

@ -56,6 +56,13 @@ cf ssh getgov-ENVIRONMENT
./manage.py dumpdata
```
## Access certain table in the database
1. `cf connect-to-service getgov-ENVIRONMENT getgov-ENVIRONMENT-database` gets you into whichever environments database you'd like
2. `\c [table name here that starts cgaws...etc];` connects to the [cgaws...etc] table
3. `\dt` retrieves information about that table and displays it
4. Make sure the table you are looking for exists. For this example, we are looking for `django_migrations`
5. Run `SELECT * FROM django_migrations;` to see everything that's in it!
## Dropping and re-creating the database
For your sandbox environment, it might be necessary to start the database over from scratch.

View file

@ -121,3 +121,19 @@ https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1697810600723069
2. `./manage.py migrate model_name_here file_name_WITH_create` (run the last data creation migration AND ONLY THAT ONE)
3. `./manage.py migrate --fake model_name_here most_recent_file_name` (fake migrate the last migration in the migration list)
4. `./manage.py load` (rerun fixtures)
### Scenario 9: Inconsistent Migration History
If you see `django.db.migrations.exceptions.InconsistentMigrationHistory` error, or when you run `./manage.py showmigrations` it looks like:
[x] 0056_example_migration
[ ] 0057_other_migration
[x] 0058_some_other_migration
1. Go to [database-access.md](../database-access.md#access-certain-table-in-the-database) to see the commands on how to access a certain table in the database.
2. In this case, we want to remove the migration "history" from the `django_migrations` table
3. Once you are in the `cgaws...` table, select the `django_migrations` table with the command `SELECT * FROM django_migrations;`
4. Find the id of the "history" you want to delete. This will be the one in the far left column. For this example, let's pretend the id is 101.
5. Run `DELETE FROM django_migrations WHERE id=101;` where 101 is an example id as seen above.
6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state. Most likely you will see several unapplied migrations.
7. If you still have unapplied migrations, run `./manage.py migrate`. If an error occurs saying one has already been applied, fake that particular migration `./manage.py migrate --fake model_name_here migration_number` and then run the normal `./manage.py migrate` command to then apply those migrations that come after the one that threw the error.

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

@ -663,6 +663,7 @@ class ContactAdmin(ListHeaderAdmin):
list_display = [
"contact",
"email",
"user_exists",
]
# this ordering effects the ordering of results
# in autocomplete_fields for user
@ -679,6 +680,13 @@ class ContactAdmin(ListHeaderAdmin):
change_form_template = "django/admin/email_clipboard_change_form.html"
def user_exists(self, obj):
"""Check if the Contact has a related User"""
return "Yes" if obj.user is not None else "No"
user_exists.short_description = "Is user" # type: ignore
user_exists.admin_order_field = "user" # type: ignore
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
@ -1445,12 +1453,36 @@ class DomainRequestAdmin(ListHeaderAdmin):
"""
Override changelist_view to set the selected value of status filter.
"""
# there are two conditions which should set the default selected filter:
# 1 - there are no query parameters in the request and the request is the
# initial request for this view
# 2 - there are no query parameters in the request and the referring url is
# the change view for a domain request
should_apply_default_filter = False
# use http_referer in order to distinguish between request as a link from another page
# and request as a removal of all filters
http_referer = request.META.get("HTTP_REFERER", "")
# if there are no query parameters in the request
# and the request is the initial request for this view
if not bool(request.GET) and request.path not in http_referer:
if not bool(request.GET):
# if the request is the initial request for this view
if request.path not in http_referer:
should_apply_default_filter = True
# elif the request is a referral from changelist view or from
# domain request change view
elif request.path in http_referer:
# find the index to determine the referring url after the path
index = http_referer.find(request.path)
# Check if there is a character following the path in http_referer
next_char_index = index + len(request.path)
if index + next_char_index < len(http_referer):
next_char = http_referer[next_char_index]
# Check if the next character is a digit, if so, this indicates
# a change view for domain request
if next_char.isdigit():
should_apply_default_filter = True
if should_apply_default_filter:
# modify the GET of the request to set the selected filter
modified_get = copy.deepcopy(request.GET)
modified_get["status__in"] = "submitted,in review,action needed"
@ -1487,10 +1519,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)
@ -1500,10 +1533,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",
@ -1669,11 +1700,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:

View file

@ -589,7 +589,7 @@ function hideDeletedForms() {
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
// The Nameservers formset features 2 required and 11 optionals
if (isNameserversForm) {
cloneIndex = 2;
// cloneIndex = 2;
formLabel = "Name server";
// DNSSEC: DS Data
} else if (isDsDataForm) {
@ -789,6 +789,43 @@ function hideDeletedForms() {
HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null)
})();
/**
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
*
*/
(function nameserversFormListener() {
let isNameserversForm = document.querySelector(".nameservers-form");
if (isNameserversForm) {
let forms = document.querySelectorAll(".repeatable-form");
if (forms.length < 3) {
// Hide the delete buttons on the 2 nameservers
forms.forEach((form) => {
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
});
}
}
})();
/**
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
*
*/
(function nameserversFormListener() {
let isNameserversForm = document.querySelector(".nameservers-form");
if (isNameserversForm) {
let forms = document.querySelectorAll(".repeatable-form");
if (forms.length < 3) {
// Hide the delete buttons on the 2 nameservers
forms.forEach((form) => {
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
});
}
}
})();
/**
* An IIFE that listens to the yes/no radio buttons on the CISA representatives form and toggles form field visibility accordingly

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

@ -83,25 +83,34 @@ class DomainNameserverForm(forms.Form):
# after clean_fields. it is used to determine form level errors.
# is_valid is typically called from view during a post
cleaned_data = super().clean()
self.clean_empty_strings(cleaned_data)
server = cleaned_data.get("server", "")
# remove ANY spaces in the server field
server = server.replace(" ", "")
# lowercase the server
server = server.lower()
server = server.replace(" ", "").lower()
cleaned_data["server"] = server
ip = cleaned_data.get("ip", None)
# remove ANY spaces in the ip field
ip = cleaned_data.get("ip", "")
ip = ip.replace(" ", "")
cleaned_data["ip"] = ip
domain = cleaned_data.get("domain", "")
ip_list = self.extract_ip_list(ip)
# validate if the form has a server or an ip
# Capture the server_value
server_value = self.cleaned_data.get("server")
# Validate if the form has a server or an ip
if (ip and ip_list) or server:
self.validate_nameserver_ip_combo(domain, server, ip_list)
# Re-set the server value:
# add_error which is called on validate_nameserver_ip_combo will clean-up (delete) any invalid data.
# We need that data because we need to know the total server entries (even if invalid) in the formset
# clean method where we determine whether a blank first and/or second entry should throw a required error.
self.cleaned_data["server"] = server_value
return cleaned_data
def clean_empty_strings(self, cleaned_data):
@ -149,6 +158,19 @@ class BaseNameserverFormset(forms.BaseFormSet):
"""
Check for duplicate entries in the formset.
"""
# Check if there are at least two valid servers
valid_servers_count = sum(
1 for form in self.forms if form.cleaned_data.get("server") and form.cleaned_data.get("server").strip()
)
if valid_servers_count >= 2:
# If there are, remove the "At least two name servers are required" error from each form
# This will allow for successful submissions when the first or second entries are blanked
# but there are enough entries total
for form in self.forms:
if form.errors.get("server") == ["At least two name servers are required."]:
form.errors.pop("server")
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own
return
@ -156,10 +178,13 @@ class BaseNameserverFormset(forms.BaseFormSet):
data = []
duplicates = []
for form in self.forms:
for index, form in enumerate(self.forms):
if form.cleaned_data:
value = form.cleaned_data["server"]
if value in data:
# We need to make sure not to trigger the duplicate error in case the first and second nameservers
# are empty. If there are enough records in the formset, that error is an unecessary blocker.
# If there aren't, the required error will block the submit.
if value in data and not (form.cleaned_data.get("server", "").strip() == "" and index == 1):
form.add_error(
"server",
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),

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

@ -245,14 +245,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()
@ -271,6 +274,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

@ -675,14 +675,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 +703,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

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

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

View file

@ -1152,6 +1152,18 @@ class MockEppLib(TestCase):
],
)
infoDomainFourHosts = fakedEppObject(
"fournameserversDomain.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
contacts=[],
hosts=[
"ns1.my-nameserver-1.com",
"ns1.my-nameserver-2.com",
"ns1.cats-are-superior3.com",
"ns1.explosive-chicken-nuggets.com",
],
)
infoDomainNoHost = fakedEppObject(
"my-nameserver.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
@ -1452,7 +1464,9 @@ class MockEppLib(TestCase):
)
def mockInfoDomainCommands(self, _request, cleaned):
request_name = getattr(_request, "name", None)
request_name = getattr(_request, "name", None).lower()
print(request_name)
# Define a dictionary to map request names to data and extension values
request_mappings = {
@ -1474,7 +1488,8 @@ class MockEppLib(TestCase):
"nameserverwithip.gov": (self.infoDomainHasIP, None),
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
"threenameserversdomain.gov": (self.infoDomainThreeHosts, None),
"fournameserversdomain.gov": (self.infoDomainFourHosts, None),
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),

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

@ -5,7 +5,7 @@ from django.conf import settings
from django.urls import reverse
from django.contrib.auth import get_user_model
from .common import MockSESClient, create_user # type: ignore
from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -71,11 +71,14 @@ class TestWithDomainPermissions(TestWithUser):
# that inherit this setUp
self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov")
self.domain_with_four_nameservers, _ = Domain.objects.get_or_create(name="fournameserversDomain.gov")
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_four_nameservers)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold)
@ -98,6 +101,11 @@ class TestWithDomainPermissions(TestWithUser):
domain=self.domain_dnssec_none,
role=UserDomainRole.Roles.MANAGER,
)
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_with_four_nameservers,
role=UserDomainRole.Roles.MANAGER,
)
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_with_ip,
@ -727,7 +735,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertContains(home_page, self.domain.name)
class TestDomainNameservers(TestDomainOverview):
class TestDomainNameservers(TestDomainOverview, MockEppLib):
def test_domain_nameservers(self):
"""Can load domain's nameservers page."""
page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
@ -974,6 +982,117 @@ class TestDomainNameservers(TestDomainOverview):
page = result.follow()
self.assertContains(page, "The name servers for this domain have been updated")
def test_domain_nameservers_can_blank_out_first_or_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first or
second entries are blanked out.
Uses self.app WebTest because we need to interact with forms.
"""
nameserver1 = ""
nameserver2 = "ns2.igorville.gov"
nameserver3 = "ns3.igorville.gov"
valid_ip = ""
valid_ip_2 = "128.0.0.2"
valid_ip_3 = "128.0.0.3"
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
nameserver1 = "ns1.igorville.gov"
nameserver2 = ""
nameserver3 = "ns3.igorville.gov"
valid_ip = "128.0.0.1"
valid_ip_2 = ""
valid_ip_3 = "128.0.0.3"
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
def test_domain_nameservers_can_blank_out_first_and_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first and
second entries are blanked out.
Uses self.app WebTest because we need to interact with forms.
"""
# We need to start with a domain with 4 nameservers otherwise the formset in the test environment
# will only have 3 forms
nameserver1 = ""
nameserver2 = ""
nameserver3 = "ns3.igorville.gov"
nameserver4 = "ns4.igorville.gov"
valid_ip = ""
valid_ip_2 = ""
valid_ip_3 = ""
valid_ip_4 = ""
nameservers_page = self.app.get(
reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Minimal check to ensure the form is loaded correctly
self.assertEqual(nameservers_page.form["form-0-server"].value, "ns1.my-nameserver-1.com")
self.assertEqual(nameservers_page.form["form-3-server"].value, "ns1.explosive-chicken-nuggets.com")
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
nameservers_page.form["form-3-server"] = nameserver4
nameservers_page.form["form-3-ip"] = valid_ip_4
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
def test_domain_nameservers_form_invalid(self):
"""Nameserver form does not submit with invalid data.

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