diff --git a/src/registrar/assets/js/dja-collapse.js b/src/registrar/assets/js/dja-collapse.js new file mode 100644 index 000000000..c33954192 --- /dev/null +++ b/src/registrar/assets/js/dja-collapse.js @@ -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); + }); + }); +} diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 680c7cdf4..b4b590acb 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -112,12 +112,20 @@ html[data-theme="light"] { .change-list .usa-table--borderless thead th, .change-list .usa-table thead td, .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.change-list, body.change-form, .analytics { color: var(--body-fg); } + .usa-table td { + background-color: transparent; + } } // 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 thead td, .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.change-list, - body.change-form { + body.change-form, + .analytics { color: var(--body-fg); } + .usa-table td { + background-color: transparent; + } } #branding h1 a:link, #branding h1 a:visited { diff --git a/src/registrar/management/commands/populate_domain_updated_federal_agency.py b/src/registrar/management/commands/populate_domain_updated_federal_agency.py new file mode 100644 index 000000000..dd8ceb3b2 --- /dev/null +++ b/src/registrar/management/commands/populate_domain_updated_federal_agency.py @@ -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}" + ) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 17bd9a100..75fbadc3e 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -16,12 +16,21 @@ from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError from itertools import chain +from auditlog.models import AuditlogHistoryField # type: ignore + logger = logging.getLogger(__name__) class DomainRequest(TimeStampedModel): """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 class DomainRequestStatus(models.TextChoices): STARTED = "started", "Started" diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index 93babb2c4..1cc450e02 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -67,7 +67,35 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% endblock field_readonly %} {% block after_help_text %} - {% if field.field.name == "creator" %} + {% if field.field.name == "status" and original_object.history.count > 0 %} +
Status | +User | +Changed at | +
---|---|---|
{{ value.1|default:"None" }} | +{{ log_entry.actor|default:"None" }} | +{{ log_entry.timestamp|default:"None" }} | +