mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 18:09:25 +02:00
Merge branch 'main' into gd/2365-add-suborg-to-domain-info
This commit is contained in:
commit
77acdc18e0
15 changed files with 641 additions and 50 deletions
65
docs/developer/management_script_helpers.md
Normal file
65
docs/developer/management_script_helpers.md
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# Terminal Helper Functions
|
||||||
|
`terminal_helper.py` contains utility functions to assist with common terminal and script operations.
|
||||||
|
This file documents what they do and provides guidance on their usage.
|
||||||
|
|
||||||
|
## TerminalColors
|
||||||
|
`TerminalColors` provides ANSI color codes as variables to style terminal output.
|
||||||
|
|
||||||
|
## ScriptDataHelper
|
||||||
|
### bulk_update_fields
|
||||||
|
|
||||||
|
`bulk_update_fields` performs a memory-efficient bulk update on a Django model in batches using a Paginator.
|
||||||
|
|
||||||
|
## TerminalHelper
|
||||||
|
### log_script_run_summary
|
||||||
|
|
||||||
|
`log_script_run_summary` logs a summary of a script run, including counts of updated, skipped, and failed records.
|
||||||
|
|
||||||
|
### print_conditional
|
||||||
|
|
||||||
|
`print_conditional` conditionally logs a statement at a specified severity if a condition is met.
|
||||||
|
|
||||||
|
### prompt_for_execution
|
||||||
|
|
||||||
|
`prompt_for_execution` prompts the user to inspect a string and confirm if they wish to proceed. Returns True if proceeding, False if skipping, or exits the script.
|
||||||
|
|
||||||
|
### query_yes_no
|
||||||
|
|
||||||
|
`query_yes_no` prompts the user with a yes/no question and returns True for "yes" or False for "no".
|
||||||
|
|
||||||
|
### query_yes_no_exit
|
||||||
|
|
||||||
|
`query_yes_no_exit` is similar to `query_yes_no` but includes an "exit" option to terminate the script.
|
||||||
|
|
||||||
|
## PopulateScriptTemplate
|
||||||
|
|
||||||
|
`PopulateScriptTemplate` is an abstract base class that provides a template for creating generic populate scripts. It handles logging and bulk updating for repetitive scripts that update a few fields.
|
||||||
|
|
||||||
|
### **Disclaimer**
|
||||||
|
This template is intended as a shorthand for simple scripts. It is not recommended for complex operations. See `transfer_federal_agency.py` for a straightforward example of how to use this template.
|
||||||
|
|
||||||
|
### Step-by-step usage guide
|
||||||
|
To create a script using `PopulateScriptTemplate`:
|
||||||
|
1. Create a new class that inherits from `PopulateScriptTemplate`
|
||||||
|
2. Implement the `update_record` method to define how each record should be updated
|
||||||
|
3. Optionally, override the configuration variables and helper methods as needed
|
||||||
|
4. Call `mass_update_records` within `handle` and run the script
|
||||||
|
|
||||||
|
#### Template explanation
|
||||||
|
|
||||||
|
The main method provided by `PopulateScriptTemplate` is `mass_update_records`. This method loops through each valid object (specified by `filter_conditions`) and updates the fields defined in `fields_to_update` using the `update_record` method.
|
||||||
|
|
||||||
|
Before updating, `mass_update_records` prompts the user to confirm the proposed changes. If the user does not proceed, the script will exit.
|
||||||
|
|
||||||
|
After processing the records, `mass_update_records` performs a bulk update on the specified fields using `ScriptDataHelper.bulk_update_fields` and logs a summary of the script run using `TerminalHelper.log_script_run_summary`.
|
||||||
|
|
||||||
|
#### Config options
|
||||||
|
The class provides the following optional configuration variables:
|
||||||
|
- `prompt_title`: The header displayed by `prompt_for_execution` when the script starts (default: "Do you wish to proceed?")
|
||||||
|
- `display_run_summary_items_as_str`: If True, runs `str(item)` on each item when printing the run summary for prettier output (default: False)
|
||||||
|
- `run_summary_header`: The header for the script run summary printed after the script finishes (default: None)
|
||||||
|
|
||||||
|
The class also provides helper methods:
|
||||||
|
- `get_class_name`: Returns a display-friendly class name for the terminal prompt
|
||||||
|
- `get_failure_message`: Returns the message to display if a record fails to update
|
||||||
|
- `should_skip_record`: Defines the condition for skipping a record (by default, no records are skipped)
|
|
@ -698,6 +698,33 @@ Example: `cf ssh getgov-za`
|
||||||
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
|
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
|
||||||
| 1 | **debug** | Increases logging detail. Defaults to False. |
|
| 1 | **debug** | Increases logging detail. Defaults to False. |
|
||||||
|
|
||||||
|
## Transfer federal agency script
|
||||||
|
The transfer federal agency script adds the "federal_type" field on each associated DomainRequest, and uses that to populate the "federal_type" field on each FederalAgency.
|
||||||
|
|
||||||
|
**Important:** When running this script, note that data generated by our fixtures will be inaccurate (since we assign random data to them). Use real data on this script.
|
||||||
|
Do note that there is a check on record uniqueness. If two or more records do NOT have the same value for federal_type for any given federal agency, then the record is skipped. This protects against fixtures data when loaded with real data.
|
||||||
|
|
||||||
|
### Running on sandboxes
|
||||||
|
|
||||||
|
#### Step 1: Login to CloudFoundry
|
||||||
|
```cf login -a api.fr.cloud.gov --sso```
|
||||||
|
|
||||||
|
#### Step 2: SSH into your environment
|
||||||
|
```cf ssh getgov-{space}```
|
||||||
|
|
||||||
|
Example: `cf ssh getgov-za`
|
||||||
|
|
||||||
|
#### Step 3: Create a shell instance
|
||||||
|
```/tmp/lifecycle/shell```
|
||||||
|
|
||||||
|
#### Step 4: Running the script
|
||||||
|
```./manage.py transfer_federal_agency_type```
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
|
||||||
|
#### Step 1: Running the script
|
||||||
|
```docker-compose exec app ./manage.py transfer_federal_agency_type```
|
||||||
|
|
||||||
## Email current metadata report
|
## Email current metadata report
|
||||||
|
|
||||||
### Running on sandboxes
|
### Running on sandboxes
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import logging
|
import logging
|
||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
|
from django.template.loader import get_template
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Value, CharField, Q
|
from django.db.models import Value, CharField, Q
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
|
@ -1696,6 +1697,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
return super().save_model(request, obj, form, change)
|
return super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
# == Handle non-status changes == #
|
# == Handle non-status changes == #
|
||||||
|
# Change this in #1901. Add a check on "not self.action_needed_reason_email"
|
||||||
|
if obj.action_needed_reason:
|
||||||
|
self._handle_action_needed_reason_email(obj)
|
||||||
|
should_save = True
|
||||||
|
|
||||||
# Get the original domain request from the database.
|
# Get the original domain request from the database.
|
||||||
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
||||||
|
@ -1704,14 +1709,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
return super().save_model(request, obj, form, change)
|
return super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
# == Handle status changes == #
|
# == Handle status changes == #
|
||||||
|
|
||||||
# Run some checks on the current object for invalid status changes
|
# Run some checks on the current object for invalid status changes
|
||||||
obj, should_save = self._handle_status_change(request, obj, original_obj)
|
obj, should_save = self._handle_status_change(request, obj, original_obj)
|
||||||
|
|
||||||
# We should only save if we don't display any errors in the step above.
|
# We should only save if we don't display any errors in the steps above.
|
||||||
if should_save:
|
if should_save:
|
||||||
return super().save_model(request, obj, form, change)
|
return super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
def _handle_action_needed_reason_email(self, obj):
|
||||||
|
text = self._get_action_needed_reason_default_email_text(obj, obj.action_needed_reason)
|
||||||
|
obj.action_needed_reason_email = text.get("email_body_text")
|
||||||
|
|
||||||
def _handle_status_change(self, request, obj, original_obj):
|
def _handle_status_change(self, request, obj, original_obj):
|
||||||
"""
|
"""
|
||||||
Checks for various conditions when a status change is triggered.
|
Checks for various conditions when a status change is triggered.
|
||||||
|
@ -1905,10 +1913,45 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
# Initialize extra_context and add filtered entries
|
# Initialize extra_context and add filtered entries
|
||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
||||||
|
extra_context["action_needed_reason_emails"] = self.get_all_action_needed_reason_emails_as_json(obj)
|
||||||
|
|
||||||
# Call the superclass method with updated extra_context
|
# Call the superclass method with updated extra_context
|
||||||
return super().change_view(request, object_id, form_url, extra_context)
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
def get_all_action_needed_reason_emails_as_json(self, domain_request):
|
||||||
|
"""Returns a json dictionary of every action needed reason and its associated email
|
||||||
|
for this particular domain request."""
|
||||||
|
emails = {}
|
||||||
|
for action_needed_reason in domain_request.ActionNeededReasons:
|
||||||
|
enum_value = action_needed_reason.value
|
||||||
|
# Change this in #1901. Just add a check for the current value.
|
||||||
|
emails[enum_value] = self._get_action_needed_reason_default_email_text(domain_request, enum_value)
|
||||||
|
return json.dumps(emails)
|
||||||
|
|
||||||
|
def _get_action_needed_reason_default_email_text(self, domain_request, action_needed_reason: str):
|
||||||
|
"""Returns the default email associated with the given action needed reason"""
|
||||||
|
if action_needed_reason is None or action_needed_reason == domain_request.ActionNeededReasons.OTHER:
|
||||||
|
return {
|
||||||
|
"subject_text": None,
|
||||||
|
"email_body_text": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the email body
|
||||||
|
template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt"
|
||||||
|
template = get_template(template_path)
|
||||||
|
|
||||||
|
# Get the email subject
|
||||||
|
template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt"
|
||||||
|
subject_template = get_template(template_subject_path)
|
||||||
|
|
||||||
|
# Return the content of the rendered views
|
||||||
|
context = {"domain_request": domain_request}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"subject_text": subject_template.render(context=context),
|
||||||
|
"email_body_text": template.render(context=context),
|
||||||
|
}
|
||||||
|
|
||||||
def process_log_entry(self, log_entry):
|
def process_log_entry(self, log_entry):
|
||||||
"""Process a log entry and return filtered entry dictionary if applicable."""
|
"""Process a log entry and return filtered entry dictionary if applicable."""
|
||||||
changes = log_entry.changes
|
changes = log_entry.changes
|
||||||
|
|
|
@ -8,6 +8,25 @@
|
||||||
|
|
||||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
// Helper functions.
|
// Helper functions.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide element
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const hideElement = (element) => {
|
||||||
|
if (element && !element.classList.contains("display-none"))
|
||||||
|
element.classList.add('display-none');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show element
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const showElement = (element) => {
|
||||||
|
if (element && element.classList.contains("display-none"))
|
||||||
|
element.classList.remove('display-none');
|
||||||
|
};
|
||||||
|
|
||||||
/** Either sets attribute target="_blank" to a given element, or removes it */
|
/** Either sets attribute target="_blank" to a given element, or removes it */
|
||||||
function openInNewTab(el, removeAttribute = false){
|
function openInNewTab(el, removeAttribute = false){
|
||||||
if(removeAttribute){
|
if(removeAttribute){
|
||||||
|
@ -57,6 +76,7 @@ function openInNewTab(el, removeAttribute = false){
|
||||||
createPhantomModalFormButtons();
|
createPhantomModalFormButtons();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
/** An IIFE for DomainRequest to hook a modal to a dropdown option.
|
/** An IIFE for DomainRequest to hook a modal to a dropdown option.
|
||||||
* This intentionally does not interact with createPhantomModalFormButtons()
|
* This intentionally does not interact with createPhantomModalFormButtons()
|
||||||
*/
|
*/
|
||||||
|
@ -408,13 +428,21 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
function moveStatusChangelog(actionNeededReasonFormGroup, statusSelect) {
|
function moveStatusChangelog(actionNeededReasonFormGroup, statusSelect) {
|
||||||
let flexContainer = actionNeededReasonFormGroup.querySelector('.flex-container');
|
let flexContainer = actionNeededReasonFormGroup.querySelector('.flex-container');
|
||||||
let statusChangelog = document.getElementById('dja-status-changelog');
|
let statusChangelog = document.getElementById('dja-status-changelog');
|
||||||
|
|
||||||
|
// On action needed, show the email that will be sent out
|
||||||
|
let showReasonEmailContainer = document.querySelector("#action_needed_reason_email_readonly")
|
||||||
|
|
||||||
|
// Prepopulate values on page load.
|
||||||
if (statusSelect.value === "action needed") {
|
if (statusSelect.value === "action needed") {
|
||||||
flexContainer.parentNode.insertBefore(statusChangelog, flexContainer.nextSibling);
|
flexContainer.parentNode.insertBefore(statusChangelog, flexContainer.nextSibling);
|
||||||
|
showElement(showReasonEmailContainer);
|
||||||
} else {
|
} else {
|
||||||
// Move the changelog back to its original location
|
// Move the changelog back to its original location
|
||||||
let statusFlexContainer = statusSelect.closest('.flex-container');
|
let statusFlexContainer = statusSelect.closest('.flex-container');
|
||||||
statusFlexContainer.parentNode.insertBefore(statusChangelog, statusFlexContainer.nextSibling);
|
statusFlexContainer.parentNode.insertBefore(statusChangelog, statusFlexContainer.nextSibling);
|
||||||
|
hideElement(showReasonEmailContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the function on page load
|
// Call the function on page load
|
||||||
|
@ -518,3 +546,60 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
handleShowMoreButton(toggleButton, descriptionDiv)
|
handleShowMoreButton(toggleButton, descriptionDiv)
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
/** An IIFE that hooks up to the "show email" button.
|
||||||
|
* which shows the auto generated email on action needed reason.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
|
||||||
|
let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more");
|
||||||
|
if(actionNeededReasonDropdown && actionNeededEmail && container) {
|
||||||
|
// Add a change listener to the action needed reason dropdown
|
||||||
|
handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail) {
|
||||||
|
actionNeededReasonDropdown.addEventListener("change", function() {
|
||||||
|
let reason = actionNeededReasonDropdown.value;
|
||||||
|
|
||||||
|
// If a reason isn't specified, no email will be sent.
|
||||||
|
// You also cannot save the model in this state.
|
||||||
|
// This flow occurs if you switch back to the empty picker state.
|
||||||
|
if(!reason) {
|
||||||
|
showNoEmailMessage(actionNeededEmail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionNeededEmails = JSON.parse(document.getElementById('action-needed-emails-data').textContent)
|
||||||
|
let emailData = actionNeededEmails[reason];
|
||||||
|
if (emailData) {
|
||||||
|
let emailBody = emailData.email_body_text
|
||||||
|
if (emailBody) {
|
||||||
|
actionNeededEmail.value = emailBody
|
||||||
|
showActionNeededEmail(actionNeededEmail);
|
||||||
|
}else {
|
||||||
|
showNoEmailMessage(actionNeededEmail);
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
showNoEmailMessage(actionNeededEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the text field. Hide the "no email" message.
|
||||||
|
function showActionNeededEmail(actionNeededEmail){
|
||||||
|
let noEmailMessage = document.getElementById("no-email-message");
|
||||||
|
showElement(actionNeededEmail);
|
||||||
|
hideElement(noEmailMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the text field. Show the "no email" message.
|
||||||
|
function showNoEmailMessage(actionNeededEmail) {
|
||||||
|
let noEmailMessage = document.getElementById("no-email-message");
|
||||||
|
hideElement(actionNeededEmail);
|
||||||
|
showElement(noEmailMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
|
@ -786,3 +786,44 @@ div.dja__model-description{
|
||||||
.usa-button--dja-link-color {
|
.usa-button--dja-link-color {
|
||||||
color: var(--link-fg);
|
color: var(--link-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dja-readonly-textarea-container {
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 610px;
|
||||||
|
resize: none;
|
||||||
|
cursor: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style the scroll bar handle
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--body-fg);
|
||||||
|
border-radius: 99px;
|
||||||
|
background-clip: content-box;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-full {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-border {
|
||||||
|
background-color: var(--selected-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
label {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-none {
|
||||||
|
// Many elements in django admin try to override this, so we need !important.
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
|
@ -12,12 +12,11 @@ class Command(BaseCommand, PopulateScriptTemplate):
|
||||||
def handle(self, **kwargs):
|
def handle(self, **kwargs):
|
||||||
"""Loops through each valid User object and updates its verification_type value"""
|
"""Loops through each valid User object and updates its verification_type value"""
|
||||||
filter_condition = {"verification_type__isnull": True}
|
filter_condition = {"verification_type__isnull": True}
|
||||||
self.mass_populate_field(User, filter_condition, ["verification_type"])
|
self.mass_update_records(User, filter_condition, ["verification_type"])
|
||||||
|
|
||||||
def populate_field(self, field_to_update):
|
def update_record(self, record: User):
|
||||||
"""Defines how we update the verification_type field"""
|
"""Defines how we update the verification_type field"""
|
||||||
field_to_update.set_user_verification_type()
|
record.set_user_verification_type()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{TerminalColors.OKCYAN}Updating {field_to_update} => "
|
f"{TerminalColors.OKCYAN}Updating {record} => " f"{record.verification_type}{TerminalColors.OKCYAN}"
|
||||||
f"{field_to_update.verification_type}{TerminalColors.OKCYAN}"
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import logging
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||||
|
from registrar.models import FederalAgency, DomainInformation
|
||||||
|
from registrar.utility.constants import BranchChoices
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand, PopulateScriptTemplate):
|
||||||
|
"""
|
||||||
|
This command uses the PopulateScriptTemplate,
|
||||||
|
which provides reusable logging and bulk updating functions for mass-updating fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
help = "Loops through each valid User object and updates its verification_type value"
|
||||||
|
prompt_title = "Do you wish to update all Federal Agencies?"
|
||||||
|
|
||||||
|
def handle(self, **kwargs):
|
||||||
|
"""Loops through each valid User object and updates the value of its verification_type field"""
|
||||||
|
|
||||||
|
# These are federal agencies that we don't have any data on.
|
||||||
|
# Independent agencies are considered "EXECUTIVE" here.
|
||||||
|
self.missing_records = {
|
||||||
|
"Christopher Columbus Fellowship Foundation": BranchChoices.EXECUTIVE,
|
||||||
|
"Commission for the Preservation of America's Heritage Abroad": BranchChoices.EXECUTIVE,
|
||||||
|
"Commission of Fine Arts": BranchChoices.EXECUTIVE,
|
||||||
|
"Committee for Purchase From People Who Are Blind or Severely Disabled": BranchChoices.EXECUTIVE,
|
||||||
|
"DC Court Services and Offender Supervision Agency": BranchChoices.EXECUTIVE,
|
||||||
|
"DC Pre-trial Services": BranchChoices.EXECUTIVE,
|
||||||
|
"Department of Agriculture": BranchChoices.EXECUTIVE,
|
||||||
|
"Dwight D. Eisenhower Memorial Commission": BranchChoices.LEGISLATIVE,
|
||||||
|
"Farm Credit System Insurance Corporation": BranchChoices.EXECUTIVE,
|
||||||
|
"Federal Financial Institutions Examination Council": BranchChoices.EXECUTIVE,
|
||||||
|
"Federal Judiciary": BranchChoices.JUDICIAL,
|
||||||
|
"Institute of Peace": BranchChoices.EXECUTIVE,
|
||||||
|
"International Boundary and Water Commission: United States and Mexico": BranchChoices.EXECUTIVE,
|
||||||
|
"International Boundary Commission: United States and Canada": BranchChoices.EXECUTIVE,
|
||||||
|
"International Joint Commission: United States and Canada": BranchChoices.EXECUTIVE,
|
||||||
|
"Legislative Branch": BranchChoices.LEGISLATIVE,
|
||||||
|
"National Foundation on the Arts and the Humanities": BranchChoices.EXECUTIVE,
|
||||||
|
"Nuclear Safety Oversight Committee": BranchChoices.EXECUTIVE,
|
||||||
|
"Office of Compliance": BranchChoices.LEGISLATIVE,
|
||||||
|
"Overseas Private Investment Corporation": BranchChoices.EXECUTIVE,
|
||||||
|
"Public Defender Service for the District of Columbia": BranchChoices.EXECUTIVE,
|
||||||
|
"The Executive Office of the President": BranchChoices.EXECUTIVE,
|
||||||
|
"U.S. Access Board": BranchChoices.EXECUTIVE,
|
||||||
|
"U.S. Agency for Global Media": BranchChoices.EXECUTIVE,
|
||||||
|
"U.S. China Economic and Security Review Commission": BranchChoices.LEGISLATIVE,
|
||||||
|
"U.S. Interagency Council on Homelessness": BranchChoices.EXECUTIVE,
|
||||||
|
"U.S. International Trade Commission": BranchChoices.EXECUTIVE,
|
||||||
|
"U.S. Postal Service": BranchChoices.EXECUTIVE,
|
||||||
|
"U.S. Trade and Development Agency": BranchChoices.EXECUTIVE,
|
||||||
|
"Udall Foundation": BranchChoices.EXECUTIVE,
|
||||||
|
"United States Arctic Research Commission": BranchChoices.EXECUTIVE,
|
||||||
|
"Utah Reclamation Mitigation and Conservation Commission": BranchChoices.EXECUTIVE,
|
||||||
|
"Vietnam Education Foundation": BranchChoices.EXECUTIVE,
|
||||||
|
"Woodrow Wilson International Center for Scholars": BranchChoices.EXECUTIVE,
|
||||||
|
"World War I Centennial Commission": BranchChoices.EXECUTIVE,
|
||||||
|
}
|
||||||
|
# Get all existing domain requests. Select_related allows us to skip doing db queries.
|
||||||
|
self.all_domain_infos = DomainInformation.objects.select_related("federal_agency")
|
||||||
|
self.mass_update_records(
|
||||||
|
FederalAgency, filter_conditions={"agency__isnull": False}, fields_to_update=["federal_type"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_record(self, record: FederalAgency):
|
||||||
|
"""Defines how we update the federal_type field on each record."""
|
||||||
|
request = self.all_domain_infos.filter(federal_agency__agency=record.agency).first()
|
||||||
|
if request:
|
||||||
|
record.federal_type = request.federal_type
|
||||||
|
elif not request and record.agency in self.missing_records:
|
||||||
|
record.federal_type = self.missing_records.get(record.agency)
|
||||||
|
logger.info(f"{TerminalColors.OKCYAN}Updating {str(record)} => {record.federal_type}{TerminalColors.ENDC}")
|
||||||
|
|
||||||
|
def should_skip_record(self, record) -> bool: # noqa
|
||||||
|
"""Defines the conditions in which we should skip updating a record."""
|
||||||
|
requests = self.all_domain_infos.filter(federal_agency__agency=record.agency, federal_type__isnull=False)
|
||||||
|
# Check if all federal_type values are the same. Skip the record otherwise.
|
||||||
|
distinct_federal_types = requests.values("federal_type").distinct()
|
||||||
|
should_skip = distinct_federal_types.count() != 1
|
||||||
|
if should_skip and record.agency not in self.missing_records:
|
||||||
|
logger.info(
|
||||||
|
f"{TerminalColors.YELLOW}Skipping update for {str(record)} => count is "
|
||||||
|
f"{distinct_federal_types.count()} and records are {distinct_federal_types}{TerminalColors.ENDC}"
|
||||||
|
)
|
||||||
|
elif record.agency in self.missing_records:
|
||||||
|
logger.info(
|
||||||
|
f"{TerminalColors.MAGENTA}Missing data on {str(record)} - "
|
||||||
|
f"swapping to manual mapping{TerminalColors.ENDC}"
|
||||||
|
)
|
||||||
|
should_skip = False
|
||||||
|
return should_skip
|
|
@ -61,56 +61,96 @@ class ScriptDataHelper:
|
||||||
|
|
||||||
class PopulateScriptTemplate(ABC):
|
class PopulateScriptTemplate(ABC):
|
||||||
"""
|
"""
|
||||||
Contains an ABC for generic populate scripts
|
Contains an ABC for generic populate scripts.
|
||||||
|
|
||||||
|
This template provides reusable logging and bulk updating functions for
|
||||||
|
mass-updating fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def mass_populate_field(self, sender, filter_conditions, fields_to_update):
|
# Optional script-global config variables. For the most part, you can leave these untouched.
|
||||||
"""Loops through each valid "sender" object - specified by filter_conditions - and
|
# Defines what prompt_for_execution displays as its header when you first start the script
|
||||||
updates fields defined by fields_to_update using populate_function.
|
prompt_title: str = "Do you wish to proceed?"
|
||||||
|
|
||||||
You must define populate_field before you can use this function.
|
# The header when printing the script run summary (after the script finishes)
|
||||||
|
run_summary_header = None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_record(self, record):
|
||||||
|
"""Defines how we update each field. Must be defined before using mass_update_records."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def mass_update_records(self, object_class, filter_conditions, fields_to_update, debug=True):
|
||||||
|
"""Loops through each valid "object_class" object - specified by filter_conditions - and
|
||||||
|
updates fields defined by fields_to_update using update_record.
|
||||||
|
|
||||||
|
You must define update_record before you can use this function.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
objects = sender.objects.filter(**filter_conditions)
|
records = object_class.objects.filter(**filter_conditions)
|
||||||
|
readable_class_name = self.get_class_name(object_class)
|
||||||
|
|
||||||
# Code execution will stop here if the user prompts "N"
|
# Code execution will stop here if the user prompts "N"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
info_to_inspect=f"""
|
||||||
==Proposed Changes==
|
==Proposed Changes==
|
||||||
Number of {sender} objects to change: {len(objects)}
|
Number of {readable_class_name} objects to change: {len(records)}
|
||||||
These fields will be updated on each record: {fields_to_update}
|
These fields will be updated on each record: {fields_to_update}
|
||||||
""",
|
""",
|
||||||
prompt_title="Do you wish to patch this data?",
|
prompt_title=self.prompt_title,
|
||||||
)
|
)
|
||||||
logger.info("Updating...")
|
logger.info("Updating...")
|
||||||
|
|
||||||
to_update: List[sender] = []
|
to_update: List[object_class] = []
|
||||||
failed_to_update: List[sender] = []
|
to_skip: List[object_class] = []
|
||||||
for updated_object in objects:
|
failed_to_update: List[object_class] = []
|
||||||
|
for record in records:
|
||||||
try:
|
try:
|
||||||
self.populate_field(updated_object)
|
if not self.should_skip_record(record):
|
||||||
to_update.append(updated_object)
|
self.update_record(record)
|
||||||
|
to_update.append(record)
|
||||||
|
else:
|
||||||
|
to_skip.append(record)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
failed_to_update.append(updated_object)
|
fail_message = self.get_failure_message(record)
|
||||||
|
failed_to_update.append(record)
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
logger.error(f"{TerminalColors.FAIL}" f"Failed to update {updated_object}" f"{TerminalColors.ENDC}")
|
logger.error(fail_message)
|
||||||
|
|
||||||
# Do a bulk update on the first_ready field
|
# Do a bulk update on the desired field
|
||||||
ScriptDataHelper.bulk_update_fields(sender, to_update, fields_to_update)
|
ScriptDataHelper.bulk_update_fields(object_class, to_update, fields_to_update)
|
||||||
|
|
||||||
# Log what happened
|
# Log what happened
|
||||||
TerminalHelper.log_script_run_summary(to_update, failed_to_update, skipped=[], debug=True)
|
TerminalHelper.log_script_run_summary(
|
||||||
|
to_update,
|
||||||
|
failed_to_update,
|
||||||
|
to_skip,
|
||||||
|
debug=debug,
|
||||||
|
log_header=self.run_summary_header,
|
||||||
|
display_as_str=True,
|
||||||
|
)
|
||||||
|
|
||||||
@abstractmethod
|
def get_class_name(self, sender) -> str:
|
||||||
def populate_field(self, field_to_update):
|
"""Returns the class name that we want to display for the terminal prompt.
|
||||||
"""Defines how we update each field. Must be defined before using mass_populate_field."""
|
Example: DomainRequest => "Domain Request"
|
||||||
raise NotImplementedError
|
"""
|
||||||
|
return sender._meta.verbose_name if getattr(sender, "_meta") else sender
|
||||||
|
|
||||||
|
def get_failure_message(self, record) -> str:
|
||||||
|
"""Returns the message that we will display if a record fails to update"""
|
||||||
|
return f"{TerminalColors.FAIL}" f"Failed to update {record}" f"{TerminalColors.ENDC}"
|
||||||
|
|
||||||
|
def should_skip_record(self, record) -> bool: # noqa
|
||||||
|
"""Defines the condition in which we should skip updating a record. Override as needed."""
|
||||||
|
# By default - don't skip
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class TerminalHelper:
|
class TerminalHelper:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool, log_header=None):
|
def log_script_run_summary(
|
||||||
|
to_update, failed_to_update, skipped, debug: bool, log_header=None, display_as_str=False
|
||||||
|
):
|
||||||
"""Prints success, failed, and skipped counts, as well as
|
"""Prints success, failed, and skipped counts, as well as
|
||||||
all affected objects."""
|
all affected objects."""
|
||||||
update_success_count = len(to_update)
|
update_success_count = len(to_update)
|
||||||
|
@ -121,10 +161,14 @@ class TerminalHelper:
|
||||||
log_header = "============= FINISHED ==============="
|
log_header = "============= FINISHED ==============="
|
||||||
|
|
||||||
# Prepare debug messages
|
# Prepare debug messages
|
||||||
|
if debug:
|
||||||
|
updated_display = [str(u) for u in to_update] if display_as_str else to_update
|
||||||
|
skipped_display = [str(s) for s in skipped] if display_as_str else skipped
|
||||||
|
failed_display = [str(f) for f in failed_to_update] if display_as_str else failed_to_update
|
||||||
debug_messages = {
|
debug_messages = {
|
||||||
"success": (f"{TerminalColors.OKCYAN}Updated: {to_update}{TerminalColors.ENDC}\n"),
|
"success": (f"{TerminalColors.OKCYAN}Updated: {updated_display}{TerminalColors.ENDC}\n"),
|
||||||
"skipped": (f"{TerminalColors.YELLOW}Skipped: {skipped}{TerminalColors.ENDC}\n"),
|
"skipped": (f"{TerminalColors.YELLOW}Skipped: {skipped_display}{TerminalColors.ENDC}\n"),
|
||||||
"failed": (f"{TerminalColors.FAIL}Failed: {failed_to_update}{TerminalColors.ENDC}\n"),
|
"failed": (f"{TerminalColors.FAIL}Failed: {failed_display}{TerminalColors.ENDC}\n"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Print out a list of everything that was changed, if we have any changes to log.
|
# Print out a list of everything that was changed, if we have any changes to log.
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-06-26 14:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0106_create_groups_v14"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="action_needed_reason_email",
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -296,6 +296,11 @@ class DomainRequest(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
action_needed_reason_email = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
federal_agency = models.ForeignKey(
|
federal_agency = models.ForeignKey(
|
||||||
"registrar.FederalAgency",
|
"registrar.FederalAgency",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
{% block field_sets %}
|
{% block field_sets %}
|
||||||
{# Create an invisible <a> tag so that we can use a click event to toggle the modal. #}
|
{# Create an invisible <a> tag so that we can use a click event to toggle the modal. #}
|
||||||
<a id="invisible-ineligible-modal-toggler" class="display-none" href="#toggle-set-ineligible" aria-controls="toggle-set-ineligible" data-open-modal></a>
|
<a id="invisible-ineligible-modal-toggler" class="display-none" href="#toggle-set-ineligible" aria-controls="toggle-set-ineligible" data-open-modal></a>
|
||||||
|
{# Store the current object id so we can access it easier #}
|
||||||
|
<input id="domain_request_id" class="display-none" value="{{original.id}}" />
|
||||||
|
<input id="has_audit_logs" class="display-none" value="{%if filtered_audit_log_entries %}true{% else %}false{% endif %}"/>
|
||||||
{% for fieldset in adminform %}
|
{% for fieldset in adminform %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
TODO: this will eventually need to be changed to something like this
|
TODO: this will eventually need to be changed to something like this
|
||||||
|
|
|
@ -68,11 +68,12 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endblock field_readonly %}
|
{% endblock field_readonly %}
|
||||||
|
|
||||||
{% block after_help_text %}
|
{% block after_help_text %}
|
||||||
{% if field.field.name == "status" and filtered_audit_log_entries %}
|
{% if field.field.name == "status" %}
|
||||||
<div class="flex-container" id="dja-status-changelog">
|
<div class="flex-container" id="dja-status-changelog">
|
||||||
<label aria-label="Status changelog"></label>
|
<label aria-label="Status changelog"></label>
|
||||||
<div>
|
<div>
|
||||||
<div class="usa-table-container--scrollable collapse--dgsimple collapsed" tabindex="0">
|
<div class="usa-table-container--scrollable collapse--dgsimple collapsed" tabindex="0">
|
||||||
|
{% if filtered_audit_log_entries %}
|
||||||
<table class="usa-table usa-table--borderless">
|
<table class="usa-table usa-table--borderless">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -105,7 +106,34 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>No changelog to display.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
Store the action needed reason emails in a json-based dictionary.
|
||||||
|
This allows us to change the action_needed_reason_email field dynamically, depending on value.
|
||||||
|
The alternative to this is an API endpoint.
|
||||||
|
|
||||||
|
Given that we have a limited number of emails, doing it this way makes sense.
|
||||||
|
{% endcomment %}
|
||||||
|
{% if action_needed_reason_emails %}
|
||||||
|
<script id="action-needed-emails-data" type="application/json">
|
||||||
|
{{ action_needed_reason_emails|safe }}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-2 thin-border display-none">
|
||||||
|
<label class="max-full" for="action_needed_reason_email_view_more">
|
||||||
|
<strong>Auto-generated email (sent to submitter)</strong>
|
||||||
|
</label>
|
||||||
|
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
||||||
|
{{ original_object.action_needed_reason_email }}
|
||||||
|
</textarea>
|
||||||
|
<p id="no-email-message" class="{% if original_object.action_needed_reason %}display-none{% endif %}">No email will be sent.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-2 margin-bottom-1 margin-left-1">
|
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-2 margin-bottom-1 margin-left-1">
|
||||||
<span>Show details</span>
|
<span>Show details</span>
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
|
|
@ -858,6 +858,8 @@ def completed_domain_request( # noqa
|
||||||
is_election_board=False,
|
is_election_board=False,
|
||||||
organization_type=None,
|
organization_type=None,
|
||||||
federal_agency=None,
|
federal_agency=None,
|
||||||
|
federal_type=None,
|
||||||
|
action_needed_reason=None,
|
||||||
):
|
):
|
||||||
"""A completed domain request."""
|
"""A completed domain request."""
|
||||||
if not user:
|
if not user:
|
||||||
|
@ -922,6 +924,12 @@ def completed_domain_request( # noqa
|
||||||
if organization_type:
|
if organization_type:
|
||||||
domain_request_kwargs["organization_type"] = organization_type
|
domain_request_kwargs["organization_type"] = organization_type
|
||||||
|
|
||||||
|
if federal_type:
|
||||||
|
domain_request_kwargs["federal_type"] = federal_type
|
||||||
|
|
||||||
|
if action_needed_reason:
|
||||||
|
domain_request_kwargs["action_needed_reason"] = action_needed_reason
|
||||||
|
|
||||||
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
||||||
|
|
||||||
if has_other_contacts:
|
if has_other_contacts:
|
||||||
|
|
|
@ -1596,6 +1596,24 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_model_displays_action_needed_email(self):
|
||||||
|
"""Tests if the action needed email is visible for Domain Requests"""
|
||||||
|
|
||||||
|
_domain_request = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||||
|
action_needed_reason=DomainRequest.ActionNeededReasons.BAD_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
p = "userpass"
|
||||||
|
self.client.login(username="staffuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(response, "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS")
|
||||||
|
|
||||||
@override_settings(IS_PRODUCTION=True)
|
@override_settings(IS_PRODUCTION=True)
|
||||||
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
|
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
|
||||||
"""When transitioning to submitted from started or withdrawn on a domain request,
|
"""When transitioning to submitted from started or withdrawn on a domain request,
|
||||||
|
@ -2161,15 +2179,15 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.assertContains(response, "testy@town.com", count=2)
|
self.assertContains(response, "testy@town.com", count=2)
|
||||||
expected_ao_fields = [
|
expected_ao_fields = [
|
||||||
# Field, expected value
|
# Field, expected value
|
||||||
("title", "Chief Tester"),
|
|
||||||
("phone", "(555) 555 5555"),
|
("phone", "(555) 555 5555"),
|
||||||
]
|
]
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_ao_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_ao_fields)
|
||||||
|
self.assertContains(response, "Chief Tester")
|
||||||
|
|
||||||
self.assertContains(response, "Testy Tester", count=10)
|
self.assertContains(response, "Testy Tester")
|
||||||
|
|
||||||
# == Test the other_employees field == #
|
# == Test the other_employees field == #
|
||||||
self.assertContains(response, "testy2@town.com", count=2)
|
self.assertContains(response, "testy2@town.com")
|
||||||
expected_other_employees_fields = [
|
expected_other_employees_fields = [
|
||||||
# Field, expected value
|
# Field, expected value
|
||||||
("title", "Another Tester"),
|
("title", "Another Tester"),
|
||||||
|
@ -2290,6 +2308,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
|
"action_needed_reason_email",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
"portfolio",
|
"portfolio",
|
||||||
"sub_organization",
|
"sub_organization",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import copy
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
from registrar.utility.constants import BranchChoices
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
import logging
|
import logging
|
||||||
|
@ -1112,3 +1113,115 @@ class TestImportTables(TestCase):
|
||||||
|
|
||||||
# Check that logger.error was called with the correct message
|
# Check that logger.error was called with the correct message
|
||||||
mock_logger.error.assert_called_once_with("Zip file tmp/exported_tables.zip does not exist.")
|
mock_logger.error.assert_called_once_with("Zip file tmp/exported_tables.zip does not exist.")
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransferFederalAgencyType(TestCase):
|
||||||
|
"""Tests for the transfer_federal_agency_type script"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Creates a fake domain object"""
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.amtrak, _ = FederalAgency.objects.get_or_create(agency="AMTRAK")
|
||||||
|
self.legislative_branch, _ = FederalAgency.objects.get_or_create(agency="Legislative Branch")
|
||||||
|
self.library_of_congress, _ = FederalAgency.objects.get_or_create(agency="Library of Congress")
|
||||||
|
self.gov_admin, _ = FederalAgency.objects.get_or_create(agency="gov Administration")
|
||||||
|
|
||||||
|
self.domain_request_1 = completed_domain_request(
|
||||||
|
name="testgov.gov",
|
||||||
|
federal_agency=self.amtrak,
|
||||||
|
federal_type=BranchChoices.EXECUTIVE,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
)
|
||||||
|
self.domain_request_2 = completed_domain_request(
|
||||||
|
name="cheesefactory.gov",
|
||||||
|
federal_agency=self.legislative_branch,
|
||||||
|
federal_type=BranchChoices.LEGISLATIVE,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
)
|
||||||
|
self.domain_request_3 = completed_domain_request(
|
||||||
|
name="meowardslaw.gov",
|
||||||
|
federal_agency=self.library_of_congress,
|
||||||
|
federal_type=BranchChoices.JUDICIAL,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Duplicate fields with invalid data - we expect to skip updating these
|
||||||
|
self.domain_request_4 = completed_domain_request(
|
||||||
|
name="baddata.gov",
|
||||||
|
federal_agency=self.gov_admin,
|
||||||
|
federal_type=BranchChoices.EXECUTIVE,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
)
|
||||||
|
self.domain_request_5 = completed_domain_request(
|
||||||
|
name="worsedata.gov",
|
||||||
|
federal_agency=self.gov_admin,
|
||||||
|
federal_type=BranchChoices.JUDICIAL,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.domain_request_1.approve()
|
||||||
|
self.domain_request_2.approve()
|
||||||
|
self.domain_request_3.approve()
|
||||||
|
self.domain_request_4.approve()
|
||||||
|
self.domain_request_5.approve()
|
||||||
|
|
||||||
|
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()
|
||||||
|
FederalAgency.objects.all().delete()
|
||||||
|
|
||||||
|
def run_transfer_federal_agency_type(self):
|
||||||
|
"""
|
||||||
|
This method executes the transfer_federal_agency_type command.
|
||||||
|
|
||||||
|
The 'call_command' function from Django's management framework is then used to
|
||||||
|
execute the populate_first_ready command with the specified arguments.
|
||||||
|
"""
|
||||||
|
with less_console_noise():
|
||||||
|
with patch(
|
||||||
|
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
call_command("transfer_federal_agency_type")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_federal_agency_type_script(self):
|
||||||
|
"""
|
||||||
|
Tests that the transfer_federal_agency_type script updates what we expect, and skips what we expect
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Before proceeding, make sure we don't have any data contamination
|
||||||
|
tested_agencies = [
|
||||||
|
self.amtrak,
|
||||||
|
self.legislative_branch,
|
||||||
|
self.library_of_congress,
|
||||||
|
self.gov_admin,
|
||||||
|
]
|
||||||
|
for agency in tested_agencies:
|
||||||
|
self.assertEqual(agency.federal_type, None)
|
||||||
|
|
||||||
|
# Run the script
|
||||||
|
self.run_transfer_federal_agency_type()
|
||||||
|
|
||||||
|
# Refresh the local db instance to reflect changes
|
||||||
|
self.amtrak.refresh_from_db()
|
||||||
|
self.legislative_branch.refresh_from_db()
|
||||||
|
self.library_of_congress.refresh_from_db()
|
||||||
|
self.gov_admin.refresh_from_db()
|
||||||
|
|
||||||
|
# Test the values that we expect to be updated
|
||||||
|
self.assertEqual(self.amtrak.federal_type, BranchChoices.EXECUTIVE)
|
||||||
|
self.assertEqual(self.legislative_branch.federal_type, BranchChoices.LEGISLATIVE)
|
||||||
|
self.assertEqual(self.library_of_congress.federal_type, BranchChoices.JUDICIAL)
|
||||||
|
|
||||||
|
# We don't expect this field to be updated (as it has duplicate data)
|
||||||
|
self.assertEqual(self.gov_admin.federal_type, None)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue