Merge remote-tracking branch 'origin/main' into nl/2300-Senior-Official-Table

This commit is contained in:
CocoByte 2024-07-03 11:34:33 -06:00
commit 269c8d641d
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
6 changed files with 155 additions and 117 deletions

View file

@ -9,6 +9,7 @@ from django.db.models.functions import Concat, Coalesce
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions, FSMField from django_fsm import get_available_FIELD_transitions, FSMField
from waffle.decorators import flag_is_active
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
@ -166,6 +167,9 @@ class DomainRequestAdminForm(forms.ModelForm):
"alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False), "alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False),
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False), "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
} }
labels = {
"action_needed_reason_email": "Auto-generated email",
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -1522,6 +1526,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
custom_election_board.admin_order_field = "is_election_board" # type: ignore custom_election_board.admin_order_field = "is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore custom_election_board.short_description = "Election office" # type: ignore
# This is just a placeholder. This field will be populated in the detail_table_fieldset view.
# This is not a field that exists on the model.
def status_history(self, obj):
return "No changelog to display."
status_history.short_description = "Status History" # type: ignore
# Filters # Filters
list_filter = ( list_filter = (
StatusListFilter, StatusListFilter,
@ -1548,9 +1559,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"fields": [ "fields": [
"portfolio", "portfolio",
"sub_organization", "sub_organization",
"status_history",
"status", "status",
"rejection_reason", "rejection_reason",
"action_needed_reason", "action_needed_reason",
"action_needed_reason_email",
"investigator", "investigator",
"creator", "creator",
"submitter", "submitter",
@ -1630,6 +1643,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"alternative_domains", "alternative_domains",
"is_election_board", "is_election_board",
"federal_agency", "federal_agency",
"status_history",
"action_needed_reason_email",
) )
# Read only that we'll leverage for CISA Analysts # Read only that we'll leverage for CISA Analysts
@ -1948,6 +1963,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
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) extra_context["action_needed_reason_emails"] = self.get_all_action_needed_reason_emails_as_json(obj)
extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
# 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)
@ -1978,9 +1994,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt" template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt"
subject_template = get_template(template_subject_path) subject_template = get_template(template_subject_path)
# Return the content of the rendered views if flag_is_active(None, "profile_feature"): # type: ignore
context = {"domain_request": domain_request} recipient = domain_request.creator
else:
recipient = domain_request.submitter
# Return the content of the rendered views
context = {"domain_request": domain_request, "recipient": recipient}
return { return {
"subject_text": subject_template.render(context=context), "subject_text": subject_template.render(context=context),
"email_body_text": template.render(context=context), "email_body_text": template.render(context=context),

View file

@ -361,9 +361,12 @@ function initializeWidgetOnList(list, parentId) {
*/ */
(function (){ (function (){
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
// This is the "action needed reason" field
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason'); let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
// This is the "auto-generated email" field
let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email')
if (rejectionReasonFormGroup && actionNeededReasonFormGroup) { if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) {
let statusSelect = document.getElementById('id_status') let statusSelect = document.getElementById('id_status')
let isRejected = statusSelect.value == "rejected" let isRejected = statusSelect.value == "rejected"
let isActionNeeded = statusSelect.value == "action needed" let isActionNeeded = statusSelect.value == "action needed"
@ -371,6 +374,7 @@ function initializeWidgetOnList(list, parentId) {
// Initial handling of rejectionReasonFormGroup display // Initial handling of rejectionReasonFormGroup display
showOrHideObject(rejectionReasonFormGroup, show=isRejected) showOrHideObject(rejectionReasonFormGroup, show=isRejected)
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded) showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
statusSelect.addEventListener('change', function() { statusSelect.addEventListener('change', function() {
@ -382,6 +386,7 @@ function initializeWidgetOnList(list, parentId) {
isActionNeeded = statusSelect.value == "action needed" isActionNeeded = statusSelect.value == "action needed"
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded) showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded) addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded)
}); });
@ -398,6 +403,7 @@ function initializeWidgetOnList(list, parentId) {
let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null
showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason) showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason)
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
} }
}); });
}); });
@ -421,42 +427,6 @@ function initializeWidgetOnList(list, parentId) {
sessionStorage.removeItem(name); sessionStorage.removeItem(name);
} }
} }
document.addEventListener('DOMContentLoaded', function() {
let statusSelect = document.getElementById('id_status');
function moveStatusChangelog(actionNeededReasonFormGroup, statusSelect) {
if (!actionNeededReasonFormGroup || !statusSelect) {
return;
}
let flexContainer = actionNeededReasonFormGroup.querySelector('.flex-container');
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") {
flexContainer.parentNode.insertBefore(statusChangelog, flexContainer.nextSibling);
showElement(showReasonEmailContainer);
} else {
// Move the changelog back to its original location
let statusFlexContainer = statusSelect.closest('.flex-container');
statusFlexContainer.parentNode.insertBefore(statusChangelog, statusFlexContainer.nextSibling);
hideElement(showReasonEmailContainer);
}
}
// Call the function on page load
moveStatusChangelog(actionNeededReasonFormGroup, statusSelect);
// Add event listener to handle changes to the selector itself
statusSelect.addEventListener('change', function() {
moveStatusChangelog(actionNeededReasonFormGroup, statusSelect);
})
});
})(); })();
/** An IIFE for toggling the submit bar on domain request forms /** An IIFE for toggling the submit bar on domain request forms
@ -552,13 +522,13 @@ function initializeWidgetOnList(list, parentId) {
})(); })();
/** An IIFE that hooks up to the "show email" button. /** An IIFE that hooks to the show/hide button underneath action needed reason.
* which shows the auto generated email on action needed reason. * This shows the auto generated email on action needed reason.
*/ */
(function () { (function () {
let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason"); let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more"); let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more");
if(actionNeededReasonDropdown && actionNeededEmail && container) { if(actionNeededReasonDropdown && actionNeededEmail) {
// Add a change listener to the action needed reason dropdown // Add a change listener to the action needed reason dropdown
handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail); handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail);
} }

View file

@ -787,10 +787,16 @@ div.dja__model-description{
color: var(--link-fg); color: var(--link-fg);
} }
.textarea-wrapper {
width: 100%;
max-width: 610px;
}
.dja-readonly-textarea-container { .dja-readonly-textarea-container {
width: 100%;
textarea { textarea {
width: 100%; width: 100%;
min-width: 610px; max-width: 610px;
resize: none; resize: none;
cursor: auto; cursor: auto;
@ -827,3 +833,20 @@ div.dja__model-description{
// Many elements in django admin try to override this, so we need !important. // Many elements in django admin try to override this, so we need !important.
display: none !important; display: none !important;
} }
.margin-top-0 {
margin-top: 0 !important;
}
.padding-top-0 {
padding-top: 0 !important;
}
.flex-container {
@media screen and (min-width: 700px) and (max-width: 1150px) {
&.flex-container--mobile-inline {
display: inline !important;
}
}
}

View file

@ -32,7 +32,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
{% for field in line %} {% for field in line %}
<div> <div>
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %} {% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
{% block flex_container_start %}
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}"> <div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
{% endblock flex_container_start %}
{% if field.is_checkbox %} {% if field.is_checkbox %}
{# .gov override #} {# .gov override #}
{% block field_checkbox %} {% block field_checkbox %}
@ -52,7 +54,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
{% endblock field_other%} {% endblock field_other%}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% block flex_container_end %}
</div> </div>
{% endblock flex_container_end %}
{% if field.field.help_text %} {% if field.field.help_text %}
{# .gov override #} {# .gov override #}

View file

@ -6,9 +6,85 @@
This is using a custom implementation fieldset.html (see admin/fieldset.html) This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endcomment %} {% endcomment %}
{% block flex_container_start %}
{% if field.field.name == "status_history" %}
<div class="flex-container flex-container--mobile-inline {% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
{% else %}
{% comment %} Default flex container element {% endcomment %}
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
{% endif %}
{% endblock flex_container_start %}
{% block field_readonly %} {% block field_readonly %}
{% with all_contacts=original_object.other_contacts.all %} {% with all_contacts=original_object.other_contacts.all %}
{% if field.field.name == "other_contacts" %} {% if field.field.name == "status_history" %}
{% if filtered_audit_log_entries %}
<div class="readonly">
<div class="usa-table-container--scrollable collapse--dgsimple collapsed margin-top-0" 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 entry in filtered_audit_log_entries %}
<tr>
<td>
{% if entry.status %}
{{ entry.status|default:"Error" }}
{% else %}
Error
{% endif %}
{% if entry.rejection_reason %}
- {{ entry.rejection_reason|default:"Error" }}
{% endif %}
{% if entry.action_needed_reason %}
- {{ entry.action_needed_reason|default:"Error" }}
{% endif %}
</td>
<td>{{ entry.actor|default:"Error" }}</td>
<td>{{ entry.timestamp|date:"Y-m-d H:i:s" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0">
<span>Show details</span>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>
</button>
</div>
{% else %}
<div class="readonly">
No changelog to display.
</div>
{% endif %}
{% elif field.field.name == "action_needed_reason_email" %}
<div class="readonly textarea-wrapper">
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
<label class="max-full" for="action_needed_reason_email_view_more">
<strong>Sent to {% if has_profile_feature_flag %}creator{%else%}submitter{%endif%}</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>
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0 margin-bottom-1 margin-left-1">
<span>Show details</span>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>
</button>
</div>
{% elif field.field.name == "other_contacts" %}
{% if all_contacts.count > 2 %} {% if all_contacts.count > 2 %}
<div class="readonly"> <div class="readonly">
{% for contact in all_contacts %} {% for contact in all_contacts %}
@ -68,48 +144,7 @@ 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" %} {% if field.field.name == "action_needed_reason_email" %}
<div class="flex-container" id="dja-status-changelog">
<label aria-label="Status changelog"></label>
<div>
<div class="usa-table-container--scrollable collapse--dgsimple collapsed" tabindex="0">
{% if filtered_audit_log_entries %}
<table class="usa-table usa-table--borderless">
<thead>
<tr>
<th>Status</th>
<th>User</th>
<th>Changed at</th>
</tr>
</thead>
<tbody>
{% for entry in filtered_audit_log_entries %}
<tr>
<td>
{% if entry.status %}
{{ entry.status|default:"Error" }}
{% else %}
Error
{% endif %}
{% if entry.rejection_reason %}
- {{ entry.rejection_reason|default:"Error" }}
{% endif %}
{% if entry.action_needed_reason %}
- {{ entry.action_needed_reason|default:"Error" }}
{% endif %}
</td>
<td>{{ entry.actor|default:"Error" }}</td>
<td>{{ entry.timestamp|date:"Y-m-d H:i:s" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No changelog to display.</p>
{% endif %}
{% comment %} {% comment %}
Store the action needed reason emails in a json-based dictionary. 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. This allows us to change the action_needed_reason_email field dynamically, depending on value.
@ -122,26 +157,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{{ action_needed_reason_emails|safe }} {{ action_needed_reason_emails|safe }}
</script> </script>
{% endif %} {% 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>
<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>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>
</button>
</div>
</div>
{% elif field.field.name == "creator" %} {% 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>

View file

@ -2240,6 +2240,8 @@ class TestDomainRequestAdmin(MockEppLib):
"alternative_domains", "alternative_domains",
"is_election_board", "is_election_board",
"federal_agency", "federal_agency",
"status_history",
"action_needed_reason_email",
"id", "id",
"created_at", "created_at",
"updated_at", "updated_at",
@ -2300,6 +2302,8 @@ class TestDomainRequestAdmin(MockEppLib):
"alternative_domains", "alternative_domains",
"is_election_board", "is_election_board",
"federal_agency", "federal_agency",
"status_history",
"action_needed_reason_email",
"creator", "creator",
"about_your_organization", "about_your_organization",
"requested_domain", "requested_domain",
@ -2330,6 +2334,8 @@ class TestDomainRequestAdmin(MockEppLib):
"alternative_domains", "alternative_domains",
"is_election_board", "is_election_board",
"federal_agency", "federal_agency",
"status_history",
"action_needed_reason_email",
] ]
self.assertEqual(readonly_fields, expected_fields) self.assertEqual(readonly_fields, expected_fields)