Skip to content

Commit

Permalink
Merge pull request #2713 from cisagov/rjm/2651-action-needed-email
Browse files Browse the repository at this point in the history
#2651: Refactor action needed email - [RJM]
  • Loading branch information
zandercymatics authored Sep 23, 2024
2 parents 3614680 + 272f68c commit f9e60ac
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 355 deletions.
42 changes: 3 additions & 39 deletions src/registrar/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
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
Expand All @@ -23,6 +21,7 @@
from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch
from registrar.utility.admin_helpers import get_all_action_needed_reason_emails, get_action_needed_reason_default_email
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
from registrar.utility.constants import BranchChoices
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
Expand Down Expand Up @@ -1948,9 +1947,9 @@ def save_model(self, request, obj, form, change):
# Set the action_needed_reason_email to the default if nothing exists.
# Since this check occurs after save, if the user enters a value then we won't update.

default_email = self._get_action_needed_reason_default_email(obj, obj.action_needed_reason)
default_email = get_action_needed_reason_default_email(request, obj, obj.action_needed_reason)
if obj.action_needed_reason_email:
emails = self.get_all_action_needed_reason_emails(obj)
emails = get_all_action_needed_reason_emails(request, obj)
is_custom_email = obj.action_needed_reason_email not in emails.values()
if not is_custom_email:
obj.action_needed_reason_email = default_email
Expand Down Expand Up @@ -2180,8 +2179,6 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
# Initialize extra_context and add filtered entries
extra_context = extra_context or {}
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
emails = self.get_all_action_needed_reason_emails(obj)
extra_context["action_needed_reason_emails"] = json.dumps(emails)

# Denote if an action needed email was sent or not
email_sent = request.session.get("action_needed_email_sent", False)
Expand All @@ -2192,39 +2189,6 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
# 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(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:
# Map the action_needed_reason to its default email
emails[action_needed_reason.value] = self._get_action_needed_reason_default_email(
domain_request, action_needed_reason.value
)

return emails

def _get_action_needed_reason_default_email(self, domain_request, action_needed_reason):
"""Returns the default email associated with the given action needed reason"""
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
return None

recipient = domain_request.creator

# Return the context of the rendered views
context = {"domain_request": domain_request, "recipient": recipient}

# Get the email body
template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt"

email_body_text = get_template(template_path).render(context=context)
email_body_text_cleaned = None
if email_body_text:
email_body_text_cleaned = email_body_text.strip().lstrip("\n")

return email_body_text_cleaned

def process_log_entry(self, log_entry):
"""Process a log entry and return filtered entry dictionary if applicable."""
changes = log_entry.changes
Expand Down
246 changes: 95 additions & 151 deletions src/registrar/assets/js/get-gov-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,167 +504,111 @@ function initializeWidgetOnList(list, parentId) {
/** An IIFE that hooks to the show/hide button underneath action needed reason.
* This shows the auto generated email on action needed reason.
*/
(function () {
// Since this is an iife, these vars will be removed from memory afterwards
var actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");

// Placeholder text (for certain "action needed" reasons that do not involve e=mails)
var placeholderText = document.querySelector("#action-needed-reason-email-placeholder-text")

// E-mail divs and textarea components
var actionNeededEmail = document.querySelector("#id_action_needed_reason_email")
var actionNeededEmailReadonly = document.querySelector("#action-needed-reason-email-readonly")
var actionNeededEmailReadonlyTextarea = document.querySelector("#action-needed-reason-email-readonly-textarea")

// Edit e-mail modal (and its confirmation button)
var confirmEditEmailButton = document.querySelector("#email-already-sent-modal_continue-editing-button")

// Headers and footers (which change depending on if the e-mail was sent or not)
var actionNeededEmailHeader = document.querySelector("#action-needed-email-header")
var actionNeededEmailHeaderOnSave = document.querySelector("#action-needed-email-header-email-sent")
var actionNeededEmailFooter = document.querySelector("#action-needed-email-footer")

let emailWasSent = document.getElementById("action-needed-email-sent");
let lastSentEmailText = document.getElementById("action-needed-email-last-sent-text");

// Get the list of e-mails associated with each action-needed dropdown value
let emailData = document.getElementById('action-needed-emails-data');
if (!emailData) {
return;
}
let actionNeededEmailData = emailData.textContent;
if(!actionNeededEmailData) {
return;
}
let actionNeededEmailsJson = JSON.parse(actionNeededEmailData);

const domainRequestId = actionNeededReasonDropdown ? document.querySelector("#domain_request_id").value : null
const emailSentSessionVariableName = `actionNeededEmailSent-${domainRequestId}`;
const oldDropdownValue = actionNeededReasonDropdown ? actionNeededReasonDropdown.value : null;
const oldEmailValue = actionNeededEmailData ? actionNeededEmailData.value : null;

if(actionNeededReasonDropdown && actionNeededEmail && domainRequestId) {
// Add a change listener to dom load
document.addEventListener('DOMContentLoaded', function() {
let reason = actionNeededReasonDropdown.value;

// Handle the session boolean (to enable/disable editing)
if (emailWasSent && emailWasSent.value === "True") {
// An email was sent out - store that information in a session variable
addOrRemoveSessionBoolean(emailSentSessionVariableName, add=true);
}

// Show an editable email field or a readonly one
updateActionNeededEmailDisplay(reason)
});

// editEmailButton.addEventListener("click", function() {
// if (!checkEmailAlreadySent()) {
// showEmail(canEdit=true)
// }
// });

confirmEditEmailButton.addEventListener("click", function() {
// Show editable view
showEmail(canEdit=true)
});


// Add a change listener to the action needed reason dropdown
actionNeededReasonDropdown.addEventListener("change", function() {
let reason = actionNeededReasonDropdown.value;
let emailBody = reason in actionNeededEmailsJson ? actionNeededEmailsJson[reason] : null;

if (reason && emailBody) {
// Reset the session object on change since change refreshes the email content.
if (oldDropdownValue !== actionNeededReasonDropdown.value || oldEmailValue !== actionNeededEmail.value) {
// Replace the email content
actionNeededEmail.value = emailBody;
actionNeededEmailReadonlyTextarea.value = emailBody;
hideEmailAlreadySentView();
}
}

// Show either a preview of the email or some text describing no email will be sent
updateActionNeededEmailDisplay(reason)
});
document.addEventListener('DOMContentLoaded', function() {
const dropdown = document.getElementById("id_action_needed_reason");
const textarea = document.getElementById("id_action_needed_reason_email")
const domainRequestId = dropdown ? document.getElementById("domain_request_id").value : null
const textareaPlaceholder = document.querySelector(".field-action_needed_reason_email__placeholder");
const directEditButton = document.querySelector('.field-action_needed_reason_email__edit');
const modalTrigger = document.querySelector('.field-action_needed_reason_email__modal-trigger');
const modalConfirm = document.getElementById('confirm-edit-email');
const formLabel = document.querySelector('label[for="id_action_needed_reason_email"]');
let lastSentEmailContent = document.getElementById("last-sent-email-content");
const initialDropdownValue = dropdown ? dropdown.value : null;
const initialEmailValue = textarea.value;

// We will use the const to control the modal
let isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
// We will use the function to control the label and help
function isEmailAlreadySent() {
return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
}

function checkEmailAlreadySent()
{
lastEmailSent = lastSentEmailText.value.replace(/\s+/g, '')
currentEmailInTextArea = actionNeededEmail.value.replace(/\s+/g, '')
return lastEmailSent === currentEmailInTextArea
}

// Shows a readonly preview of the email with updated messaging to indicate this email was sent
function showEmailAlreadySentView()
{
hideElement(actionNeededEmailHeader)
showElement(actionNeededEmailHeaderOnSave)
actionNeededEmailFooter.innerHTML = "This email has been sent to the creator of this request";
}

// Shows a readonly preview of the email with updated messaging to indicate this email was sent
function hideEmailAlreadySentView()
{
showElement(actionNeededEmailHeader)
hideElement(actionNeededEmailHeaderOnSave)
actionNeededEmailFooter.innerHTML = "This email will be sent to the creator of this request after saving";
}

// Shows either a preview of the email or some text describing no email will be sent.
// If the email doesn't exist or if we're of reason "other", display that no email was sent.
function updateActionNeededEmailDisplay(reason) {
hideElement(actionNeededEmail.parentElement)

if (reason) {
if (reason === "other") {
// Hide email preview and show this text instead
showPlaceholderText("No email will be sent");
if (!dropdown || !textarea || !domainRequestId || !formLabel || !modalConfirm) return;
const apiUrl = document.getElementById("get-action-needed-email-for-user-json").value;

function updateUserInterface(reason) {
if (!reason) {
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
formLabel.innerHTML = "Email:";
textareaPlaceholder.innerHTML = "Select an action needed reason to see email";
showElement(textareaPlaceholder);
hideElement(directEditButton);
hideElement(modalTrigger);
hideElement(textarea);
} else if (reason === 'other') {
// 'Other' selected, we will set the label to "Email", show the "No email will be sent" placeholder, hide the trigger, textarea, hide the help text
formLabel.innerHTML = "Email:";
textareaPlaceholder.innerHTML = "No email will be sent";
showElement(textareaPlaceholder);
hideElement(directEditButton);
hideElement(modalTrigger);
hideElement(textarea);
} else {
// A triggering selection is selected, all hands on board:
textarea.setAttribute('readonly', true);
showElement(textarea);
hideElement(textareaPlaceholder);

if (isEmailAlreadySentConst) {
hideElement(directEditButton);
showElement(modalTrigger);
} else {
showElement(directEditButton);
hideElement(modalTrigger);
}
else {
// Always show readonly view of email to start
showEmail(canEdit=false)
if(checkEmailAlreadySent())
{
showEmailAlreadySentView();
}
if (isEmailAlreadySent()) {
formLabel.innerHTML = "Email sent to creator:";
} else {
formLabel.innerHTML = "Email:";
}
} else {
// Hide email preview and show this text instead
showPlaceholderText("Select an action needed reason to see email");
}
}

// Shows either a readonly view (canEdit=false) or editable view (canEdit=true) of the action needed email
function showEmail(canEdit)
{
if(!canEdit)
{
showElement(actionNeededEmailReadonly)
hideElement(actionNeededEmail.parentElement)
}
else
{
hideElement(actionNeededEmailReadonly)
showElement(actionNeededEmail.parentElement)
// Initialize UI
updateUserInterface(dropdown.value);

dropdown.addEventListener("change", function() {
const reason = dropdown.value;
// Update the UI
updateUserInterface(reason);
if (reason && reason !== "other") {
// If it's not the initial value
if (initialDropdownValue !== dropdown.value || initialEmailValue !== textarea.value) {
// Replace the email content
fetch(`${apiUrl}?reason=${reason}&domain_request_id=${domainRequestId}`)
.then(response => {
return response.json().then(data => data);
})
.then(data => {
if (data.error) {
console.error("Error in AJAX call: " + data.error);
}else {
textarea.value = data.action_needed_email;
}
updateUserInterface(reason);
})
.catch(error => {
console.error("Error action needed email: ", error)
});
}
}
showElement(actionNeededEmailFooter) // this is the same for both views, so it was separated out
hideElement(placeholderText)
}

// Hides preview of action needed email and instead displays the given text (innerHTML)
function showPlaceholderText(innerHTML)
{
hideElement(actionNeededEmail.parentElement)
hideElement(actionNeededEmailReadonly)
hideElement(actionNeededEmailFooter)
});

placeholderText.innerHTML = innerHTML;
showElement(placeholderText)
}
})();
modalConfirm.addEventListener("click", () => {
textarea.removeAttribute('readonly');
textarea.focus();
hideElement(directEditButton);
hideElement(modalTrigger);
});
directEditButton.addEventListener("click", () => {
textarea.removeAttribute('readonly');
textarea.focus();
hideElement(directEditButton);
hideElement(modalTrigger);
});
});


/** An IIFE for copy summary button (appears in DomainRegistry models)
Expand Down
17 changes: 0 additions & 17 deletions src/registrar/assets/sass/_theme/_admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -893,23 +893,6 @@ div.dja__model-description{
}
}

.vertical-separator {
min-height: 20px;
height: 100%;
width: 1px;
background-color: #d1d2d2;
vertical-align: middle
}

.usa-summary-box_admin {
color: var(--body-fg);
border-color: var(--summary-box-border);
background-color: var(--summary-box-bg);
min-width: fit-content;
padding: .5rem;
border-radius: .25rem;
}

.text-faded {
color: #{$dhs-gray-60};
}
Expand Down
6 changes: 6 additions & 0 deletions src/registrar/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from registrar.views.utility.api_views import (
get_senior_official_from_federal_agency_json,
get_federal_and_portfolio_types_from_federal_agency_json,
get_action_needed_email_for_user_json,
)
from registrar.views.domains_json import get_domains_json
from registrar.views.utility import always_404
Expand Down Expand Up @@ -153,6 +154,11 @@
get_federal_and_portfolio_types_from_federal_agency_json,
name="get-federal-and-portfolio-types-from-federal-agency-json",
),
path(
"admin/api/get-action-needed-email-for-user-json/",
get_action_needed_email_for_user_json,
name="get-action-needed-email-for-user-json",
),
path("admin/", admin.site.urls),
path(
"reports/export_data_type_user/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
{# 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 %}"/>
{% url 'get-action-needed-email-for-user-json' as url %}
<input id="get-action-needed-email-for-user-json" class="display-none" value="{{ url }}" />
{% for fieldset in adminform %}
{% comment %}
TODO: this will eventually need to be changed to something like this
Expand Down
Loading

0 comments on commit f9e60ac

Please sign in to comment.