Update migration and conflict

This commit is contained in:
Rebecca Hsieh 2024-06-26 14:09:30 -07:00
commit 7d85dced0d
No known key found for this signature in database
10 changed files with 313 additions and 10 deletions

View file

@ -1,7 +1,8 @@
from datetime import date
import logging
import copy
import json
from django.template.loader import get_template
from django import forms
from django.db.models import Value, CharField, Q
from django.db.models.functions import Concat, Coalesce
@ -1689,6 +1690,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return super().save_model(request, obj, form, change)
# == 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.
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
@ -1697,14 +1702,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return super().save_model(request, obj, form, change)
# == Handle status changes == #
# Run some checks on the current object for invalid status changes
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:
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):
"""
Checks for various conditions when a status change is triggered.
@ -1898,10 +1906,45 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Initialize extra_context and add filtered entries
extra_context = extra_context or {}
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
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):
"""Process a log entry and return filtered entry dictionary if applicable."""
changes = log_entry.changes

View file

@ -8,6 +8,25 @@
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// 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 */
function openInNewTab(el, removeAttribute = false){
if(removeAttribute){
@ -57,6 +76,7 @@ function openInNewTab(el, removeAttribute = false){
createPhantomModalFormButtons();
})();
/** An IIFE for DomainRequest to hook a modal to a dropdown option.
* This intentionally does not interact with createPhantomModalFormButtons()
*/
@ -408,13 +428,21 @@ function initializeWidgetOnList(list, parentId) {
function moveStatusChangelog(actionNeededReasonFormGroup, statusSelect) {
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
@ -518,3 +546,60 @@ function initializeWidgetOnList(list, parentId) {
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);
}
})();

View file

@ -786,3 +786,44 @@ div.dja__model-description{
.usa-button--dja-link-color {
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;
}

View file

@ -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),
),
]

View file

@ -0,0 +1,59 @@
# Generated by Django 4.2.10 on 2024-06-24 19:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0107_domainrequest_action_needed_reason_email"),
]
operations = [
migrations.RemoveField(
model_name="domaininformation",
name="authorizing_official",
),
migrations.RemoveField(
model_name="domainrequest",
name="authorizing_official",
),
migrations.AddField(
model_name="domaininformation",
name="senior_official",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="information_senior_official",
to="registrar.contact",
),
),
migrations.AddField(
model_name="domainrequest",
name="senior_official",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="senior_official",
to="registrar.contact",
),
),
migrations.AlterField(
model_name="domainrequest",
name="action_needed_reason",
field=models.TextField(
blank=True,
choices=[
("eligibility_unclear", "Unclear organization eligibility"),
("questionable_senior_official", "Questionable senior official"),
("already_has_domains", "Already has domains"),
("bad_name", "Doesnt meet naming requirements"),
("other", "Other (no auto-email sent)"),
],
null=True,
),
),
]

View file

@ -296,6 +296,11 @@ class DomainRequest(TimeStampedModel):
blank=True,
)
action_needed_reason_email = models.TextField(
null=True,
blank=True,
)
federal_agency = models.ForeignKey(
"registrar.FederalAgency",
on_delete=models.PROTECT,

View file

@ -5,7 +5,9 @@
{% block field_sets %}
{# 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>
{# 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 %}
{% comment %}
TODO: this will eventually need to be changed to something like this

View file

@ -62,17 +62,18 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endwith %}
</div>
{% else %}
<div class="readonly">{{ field.contents }}</div>
<div class="readonly">{{ field.contents }}</div>
{% endif %}
{% endwith %}
{% endblock field_readonly %}
{% 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">
<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>
@ -105,7 +106,34 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endfor %}
</tbody>
</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>
<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">

View file

@ -858,6 +858,7 @@ def completed_domain_request( # noqa
is_election_board=False,
organization_type=None,
federal_agency=None,
action_needed_reason=None,
):
"""A completed domain request."""
if not user:
@ -922,6 +923,9 @@ def completed_domain_request( # noqa
if organization_type:
domain_request_kwargs["organization_type"] = organization_type
if action_needed_reason:
domain_request_kwargs["action_needed_reason"] = action_needed_reason
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
if has_other_contacts:

View file

@ -1596,6 +1596,24 @@ class TestDomainRequestAdmin(MockEppLib):
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
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)
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
"""When transitioning to submitted from started or withdrawn on a domain request,
@ -2161,15 +2179,14 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, "testy@town.com", count=2)
expected_so_fields = [
# Field, expected value
("title", "Chief Tester"),
("phone", "(555) 555 5555"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_so_fields)
self.assertContains(response, "Testy Tester", count=10)
self.test_helper.assert_response_contains_distinct_values(response, expected_so_fields)
self.assertContains(response, "Chief Tester")
# == Test the other_employees field == #
self.assertContains(response, "testy2@town.com", count=2)
self.assertContains(response, "testy2@town.com")
expected_other_employees_fields = [
# Field, expected value
("title", "Another Tester"),
@ -2290,6 +2307,7 @@ class TestDomainRequestAdmin(MockEppLib):
"status",
"rejection_reason",
"action_needed_reason",
"action_needed_reason_email",
"federal_agency",
"portfolio",
"creator",