Merge branch 'main' into za/2014-current-websites-new-tab

This commit is contained in:
zandercymatics 2024-05-03 08:39:19 -06:00
commit d24c4347fe
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
8 changed files with 435 additions and 4 deletions

View file

@ -0,0 +1,45 @@
/*
* We will run our own version of
* https://github.com/django/django/blob/195d885ca01b14e3ce9a1881c3b8f7074f953736/django/contrib/admin/static/admin/js/collapse.js
* Works with our fieldset override
*/
/*global gettext*/
'use strict';
{
window.addEventListener('load', function() {
// Add anchor tag for Show/Hide link
const fieldsets = document.querySelectorAll('fieldset.collapse--dotgov');
for (const [i, elem] of fieldsets.entries()) {
// Don't hide if fields in this fieldset have errors
if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
elem.classList.add('collapsed');
const button = elem.querySelector('button');
button.id = 'fieldsetcollapser' + i;
button.className = 'collapse-toggle--dotgov usa-button usa-button--unstyled';
}
}
// Add toggle to hide/show anchor tag
const toggleFuncDotgov = function(e) {
e.preventDefault();
e.stopPropagation();
const fieldset = this.closest('fieldset');
const spanElement = this.querySelector('span');
const useElement = this.querySelector('use');
if (fieldset.classList.contains('collapsed')) {
// Show
spanElement.textContent = 'Hide details';
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
fieldset.classList.remove('collapsed');
} else {
// Hide
spanElement.textContent = 'Show details';
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
fieldset.classList.add('collapsed');
}
};
document.querySelectorAll('.collapse-toggle--dotgov').forEach(function(el) {
el.addEventListener('click', toggleFuncDotgov);
});
});
}

View file

@ -112,12 +112,20 @@ html[data-theme="light"] {
.change-list .usa-table--borderless thead th, .change-list .usa-table--borderless thead th,
.change-list .usa-table thead td, .change-list .usa-table thead td,
.change-list .usa-table thead th, .change-list .usa-table thead th,
.change-form .usa-table,
.change-form .usa-table--striped tbody tr:nth-child(odd) td,
.change-form .usa-table--borderless thead th,
.change-form .usa-table thead td,
.change-form .usa-table thead th,
body.dashboard, body.dashboard,
body.change-list, body.change-list,
body.change-form, body.change-form,
.analytics { .analytics {
color: var(--body-fg); color: var(--body-fg);
} }
.usa-table td {
background-color: transparent;
}
} }
// Firefox needs this to be specifically set // Firefox needs this to be specifically set
@ -127,11 +135,20 @@ html[data-theme="dark"] {
.change-list .usa-table--borderless thead th, .change-list .usa-table--borderless thead th,
.change-list .usa-table thead td, .change-list .usa-table thead td,
.change-list .usa-table thead th, .change-list .usa-table thead th,
.change-form .usa-table,
.change-form .usa-table--striped tbody tr:nth-child(odd) td,
.change-form .usa-table--borderless thead th,
.change-form .usa-table thead td,
.change-form .usa-table thead th,
body.dashboard, body.dashboard,
body.change-list, body.change-list,
body.change-form { body.change-form,
.analytics {
color: var(--body-fg); color: var(--body-fg);
} }
.usa-table td {
background-color: transparent;
}
} }
#branding h1 a:link, #branding h1 a:visited { #branding h1 a:link, #branding h1 a:visited {

View file

@ -0,0 +1,121 @@
""""
Data migration: Renaming deprecated Federal Agencies to
their new updated names ie (U.S. Peace Corps to Peace Corps)
within Domain Information and Domain Requests
"""
import logging
from django.core.management import BaseCommand
from registrar.models import DomainInformation, DomainRequest, FederalAgency
from registrar.management.commands.utility.terminal_helper import ScriptDataHelper
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Transfers Domain Request and Domain Information federal agency field from string to FederalAgency object"
# Deprecated federal agency names mapped to designated replacements {old_value, new value}
rename_deprecated_federal_agency = {
"Appraisal Subcommittee": "Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
"Barry Goldwater Scholarship and Excellence in Education Program": "Barry Goldwater Scholarship and Excellence in Education Foundation", # noqa
"Federal Reserve System": "Federal Reserve Board of Governors",
"Harry S Truman Scholarship Foundation": "Harry S. Truman Scholarship Foundation",
"Japan-US Friendship Commission": "Japan-U.S. Friendship Commission",
"Japan-United States Friendship Commission": "Japan-U.S. Friendship Commission",
"John F. Kennedy Center for Performing Arts": "John F. Kennedy Center for the Performing Arts",
"Occupational Safety & Health Review Commission": "Occupational Safety and Health Review Commission",
"Corporation for National & Community Service": "Corporation for National and Community Service",
"Export/Import Bank of the U.S.": "Export-Import Bank of the United States",
"Medical Payment Advisory Commission": "Medicare Payment Advisory Commission",
"U.S. Peace Corps": "Peace Corps",
"Chemical Safety Board": "U.S. Chemical Safety Board",
"Nuclear Waste Technical Review Board": "U.S. Nuclear Waste Technical Review Board",
"State, Local, and Tribal Government": "Non-Federal Agency",
# "U.S. China Economic and Security Review Commission": "U.S.-China Economic and Security Review Commission",
}
def find_federal_agency_row(self, domain_object):
federal_agency = domain_object.federal_agency
# Domain Information objects without a federal agency default to Non-Federal Agency
if (federal_agency is None) or (federal_agency == ""):
federal_agency = "Non-Federal Agency"
if federal_agency in self.rename_deprecated_federal_agency.keys():
federal_agency = self.rename_deprecated_federal_agency[federal_agency]
return FederalAgency.objects.filter(agency=federal_agency).get()
def handle(self, **options):
"""
Renames the Federal Agency to the correct new naming
for both Domain Information and Domain Requests objects.
NOTE: If it's None for a domain request, we skip it as
a user most likely hasn't gotten to it yet.
"""
logger.info("Transferring federal agencies to FederalAgency object")
# DomainInformation object we populate with updated_federal_agency which are then bulk updated
domain_infos_to_update = []
domain_requests_to_update = []
# Domain Requests with null federal_agency that are not populated with updated_federal_agency
domain_requests_skipped = []
domain_infos_with_errors = []
domain_requests_with_errors = []
domain_infos = DomainInformation.objects.all()
domain_requests = DomainRequest.objects.all()
logger.info(f"Found {len(domain_infos)} DomainInfo objects with federal agency.")
logger.info(f"Found {len(domain_requests)} Domain Request objects with federal agency.")
for domain_info in domain_infos:
try:
federal_agency_row = self.find_federal_agency_row(domain_info)
domain_info.updated_federal_agency = federal_agency_row
domain_infos_to_update.append(domain_info)
logger.info(
f"DomainInformation {domain_info} => updated_federal_agency set to: \
{domain_info.updated_federal_agency}"
)
except Exception as err:
domain_infos_with_errors.append(domain_info)
logger.info(
f"DomainInformation {domain_info} failed to update updated_federal_agency \
from federal_agency {domain_info.federal_agency}. Error: {err}"
)
ScriptDataHelper.bulk_update_fields(DomainInformation, domain_infos_to_update, ["updated_federal_agency"])
for domain_request in domain_requests:
try:
if (domain_request.federal_agency is None) or (domain_request.federal_agency == ""):
domain_requests_skipped.append(domain_request)
else:
federal_agency_row = self.find_federal_agency_row(domain_request)
domain_request.updated_federal_agency = federal_agency_row
domain_requests_to_update.append(domain_request)
logger.info(
f"DomainRequest {domain_request} => updated_federal_agency set to: \
{domain_request.updated_federal_agency}"
)
except Exception as err:
domain_requests_with_errors.append(domain_request)
logger.info(
f"DomainRequest {domain_request} failed to update updated_federal_agency \
from federal_agency {domain_request.federal_agency}. Error: {err}"
)
ScriptDataHelper.bulk_update_fields(DomainRequest, domain_requests_to_update, ["updated_federal_agency"])
logger.info(f"{len(domain_infos_to_update)} DomainInformation rows updated update_federal_agency.")
logger.info(
f"{len(domain_infos_with_errors)} DomainInformation rows errored when updating update_federal_agency. \
{domain_infos_with_errors}"
)
logger.info(f"{len(domain_requests_to_update)} DomainRequest rows updated update_federal_agency.")
logger.info(f"{len(domain_requests_skipped)} DomainRequest rows with null federal_agency skipped.")
logger.info(
f"{len(domain_requests_with_errors)} DomainRequest rows errored when updating update_federal_agency. \
{domain_requests_with_errors}"
)

View file

@ -16,12 +16,21 @@ from .utility.time_stamped_model import TimeStampedModel
from ..utility.email import send_templated_email, EmailSendingError from ..utility.email import send_templated_email, EmailSendingError
from itertools import chain from itertools import chain
from auditlog.models import AuditlogHistoryField # type: ignore
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DomainRequest(TimeStampedModel): class DomainRequest(TimeStampedModel):
"""A registrant's domain request for a new domain.""" """A registrant's domain request for a new domain."""
# https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history
# If we note any performace degradation due to this addition,
# we can query the auditlogs table in admin.py and add the results to
# extra_context in the change_view method for DomainRequestAdmin.
# This is the more straightforward way so trying it first.
history = AuditlogHistoryField()
# Constants for choice fields # Constants for choice fields
class DomainRequestStatus(models.TextChoices): class DomainRequestStatus(models.TextChoices):
STARTED = "started", "Started" STARTED = "started", "Started"

View file

@ -67,7 +67,35 @@ 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 == "creator" %} {% if field.field.name == "status" and original_object.history.count > 0 %}
<div class="flex-container">
<label aria-label="Submitter contact details"></label>
<div class="usa-table-container--scrollable" tabindex="0">
<table class="usa-table usa-table--borderless">
<thead>
<tr>
<th>Status</th>
<th>User</th>
<th>Changed at</th>
</tr>
</thead>
<tbody>
{% for log_entry in original_object.history.all %}
{% for key, value in log_entry.changes_display_dict.items %}
{% if key == "status" %}
<tr>
<td>{{ value.1|default:"None" }}</td>
<td>{{ log_entry.actor|default:"None" }}</td>
<td>{{ log_entry.timestamp|default:"None" }}</td>
</tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% elif field.field.name == "creator" %}
<div class="flex-container tablet:margin-top-2"> <div class="flex-container tablet:margin-top-2">
<label aria-label="Creator contact details"></label> <label aria-label="Creator contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%} {% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%}

View file

@ -789,6 +789,7 @@ def create_ready_domain():
return domain return domain
# TODO in 1793: Remove the federal agency/updated federal agency fields
def completed_domain_request( def completed_domain_request(
has_other_contacts=True, has_other_contacts=True,
has_current_website=True, has_current_website=True,
@ -803,6 +804,8 @@ def completed_domain_request(
generic_org_type="federal", generic_org_type="federal",
is_election_board=False, is_election_board=False,
organization_type=None, organization_type=None,
federal_agency=None,
updated_federal_agency=None,
): ):
"""A completed domain request.""" """A completed domain request."""
if not user: if not user:
@ -839,6 +842,7 @@ def completed_domain_request(
last_name="Bob", last_name="Bob",
is_staff=True, is_staff=True,
) )
domain_request_kwargs = dict( domain_request_kwargs = dict(
generic_org_type=generic_org_type, generic_org_type=generic_org_type,
is_election_board=is_election_board, is_election_board=is_election_board,
@ -856,6 +860,8 @@ def completed_domain_request(
creator=user, creator=user,
status=status, status=status,
investigator=investigator, investigator=investigator,
federal_agency=federal_agency,
updated_federal_agency=updated_federal_agency,
) )
if has_about_your_organization: if has_about_your_organization:
domain_request_kwargs["about_your_organization"] = "e-Government" domain_request_kwargs["about_your_organization"] = "e-Government"
@ -864,7 +870,6 @@ def completed_domain_request(
if organization_type: if organization_type:
domain_request_kwargs["organization_type"] = organization_type domain_request_kwargs["organization_type"] = organization_type
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:

View file

@ -888,6 +888,96 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_response_contains_distinct_values(response, expected_values) self.test_helper.assert_response_contains_distinct_values(response, expected_values)
@less_console_noise_decorator @less_console_noise_decorator
def test_status_logs(self):
"""
Tests that the status changes are shown in a table on the domain request change form,
accurately and in chronological order.
"""
# Create a fake domain request and domain
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED)
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.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_request.requested_domain.name)
# Table will contain one row for Started
self.assertContains(response, "<td>Started</td>", count=1)
self.assertNotContains(response, "<td>Submitted</td>")
domain_request.submit()
domain_request.save()
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Table will contain and extra row for Submitted
self.assertContains(response, "<td>Started</td>", count=1)
self.assertContains(response, "<td>Submitted</td>", count=1)
domain_request.in_review()
domain_request.save()
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Table will contain and extra row for In review
self.assertContains(response, "<td>Started</td>", count=1)
self.assertContains(response, "<td>Submitted</td>", count=1)
self.assertContains(response, "<td>In review</td>", count=1)
domain_request.action_needed()
domain_request.save()
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Table will contain and extra row for Action needed
self.assertContains(response, "<td>Started</td>", count=1)
self.assertContains(response, "<td>Submitted</td>", count=1)
self.assertContains(response, "<td>In review</td>", count=1)
self.assertContains(response, "<td>Action needed</td>", count=1)
domain_request.in_review()
domain_request.save()
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Define the expected sequence of status changes
expected_status_changes = [
"<td>In review</td>",
"<td>Action needed</td>",
"<td>In review</td>",
"<td>Submitted</td>",
"<td>Started</td>",
]
# Test for the order of status changes
for status_change in expected_status_changes:
self.assertContains(response, status_change, html=True)
# Table now contains 2 rows for Approved
self.assertContains(response, "<td>Started</td>", count=1)
self.assertContains(response, "<td>Submitted</td>", count=1)
self.assertContains(response, "<td>In review</td>", count=2)
self.assertContains(response, "<td>Action needed</td>", count=1)
def test_collaspe_toggle_button_markup(self): def test_collaspe_toggle_button_markup(self):
""" """
Tests for the correct collapse toggle button markup Tests for the correct collapse toggle button markup
@ -906,7 +996,6 @@ class TestDomainRequestAdmin(MockEppLib):
# Make sure the page loaded, and that we're on the right page # Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name) self.assertContains(response, domain_request.requested_domain.name)
self.test_helper.assertContains(response, "<span>Show details</span>") self.test_helper.assertContains(response, "<span>Show details</span>")
@less_console_noise_decorator @less_console_noise_decorator

View file

@ -841,3 +841,120 @@ class TestDiscloseEmails(MockEppLib):
) )
] ]
) )
# TODO in #1793: Remove this whole test class
class TestPopulateDomainUpdatedFederalAgency(TestCase):
def setUp(self):
super().setUp()
# Get the domain requests
self.domain_request_1 = completed_domain_request(
name="stitches.gov",
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
is_election_board=True,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
federal_agency="U.S. Peace Corps",
)
self.domain_request_2 = completed_domain_request(
name="fadoesntexist.gov",
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
is_election_board=True,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
federal_agency="MEOWARDRULES",
)
self.domain_request_3 = completed_domain_request(
name="nullfederalagency.gov",
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
is_election_board=True,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
federal_agency=None,
)
# Approve all three requests
self.domain_request_1.approve()
self.domain_request_2.approve()
self.domain_request_3.approve()
# Get the domains
self.domain_1 = Domain.objects.get(name="stitches.gov")
self.domain_2 = Domain.objects.get(name="fadoesntexist.gov")
self.domain_3 = Domain.objects.get(name="nullfederalagency.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)
def tearDown(self):
super().tearDown()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
Domain.objects.all().delete()
def run_populate_domain_updated_federal_agency(self):
"""
This method executes the populate_domain_updated_federal_agency command.
The 'call_command' function from Django's management framework is then used to
execute the populate_domain_updated_federal_agency command.
"""
with less_console_noise():
call_command("populate_domain_updated_federal_agency")
def test_domain_information_renaming_federal_agency_success(self):
"""
Domain Information updates successfully for an "outdated" Federal Agency
"""
self.run_populate_domain_updated_federal_agency()
self.domain_info_1.refresh_from_db()
previous_federal_agency_name = self.domain_info_1.federal_agency
updated_federal_agency_name = self.domain_info_1.updated_federal_agency.agency
self.assertEqual(previous_federal_agency_name, "U.S. Peace Corps")
self.assertEqual(updated_federal_agency_name, "Peace Corps")
def test_domain_information_does_not_exist(self):
"""
Update a Federal Agency that doesn't exist
(should return None bc the Federal Agency didn't exist before)
"""
self.run_populate_domain_updated_federal_agency()
self.domain_info_2.refresh_from_db()
self.assertEqual(self.domain_info_2.updated_federal_agency, None)
def test_domain_request_is_skipped(self):
"""
Update a Domain Request that doesn't exist
(should return None bc the Federal Agency didn't exist before)
"""
# Test case #2
self.run_populate_domain_updated_federal_agency()
self.domain_request_2.refresh_from_db()
self.assertEqual(self.domain_request_2.updated_federal_agency, None)
def test_domain_information_updating_null_federal_agency_to_non_federal_agency(self):
"""
Updating a Domain Information that was previously None
to Non-Federal Agency
"""
self.run_populate_domain_updated_federal_agency()
self.domain_info_3.refresh_from_db()
previous_federal_agency_name = self.domain_info_3.federal_agency
updated_federal_agency_name = self.domain_info_3.updated_federal_agency.agency
self.assertEqual(previous_federal_agency_name, None)
self.assertEqual(updated_federal_agency_name, "Non-Federal Agency")