diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 1f927f31b..f03495748 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -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 diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 524cfe594..140421db5 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -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); + } + +})(); diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 360055d91..aa8020ede 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -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; +} diff --git a/src/registrar/migrations/0107_domainrequest_action_needed_reason_email.py b/src/registrar/migrations/0107_domainrequest_action_needed_reason_email.py new file mode 100644 index 000000000..80caea245 --- /dev/null +++ b/src/registrar/migrations/0107_domainrequest_action_needed_reason_email.py @@ -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), + ), + ] diff --git a/src/registrar/migrations/0108_domaininformation_authorizing_official_and_more.py b/src/registrar/migrations/0108_domaininformation_authorizing_official_and_more.py new file mode 100644 index 000000000..cac8bc688 --- /dev/null +++ b/src/registrar/migrations/0108_domaininformation_authorizing_official_and_more.py @@ -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", "Doesn’t meet naming requirements"), + ("other", "Other (no auto-email sent)"), + ], + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 9265a726e..a7abff89b 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -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, diff --git a/src/registrar/templates/django/admin/domain_request_change_form.html b/src/registrar/templates/django/admin/domain_request_change_form.html index 1c8ce2633..a7d59d22c 100644 --- a/src/registrar/templates/django/admin/domain_request_change_form.html +++ b/src/registrar/templates/django/admin/domain_request_change_form.html @@ -5,7 +5,9 @@ {% block field_sets %} {# Create an invisible tag so that we can use a click event to toggle the modal. #} - + {# Store the current object id so we can access it easier #} + + {% for fieldset in adminform %} {% comment %} TODO: this will eventually need to be changed to something like this 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 72ed0cc7b..e3e7ef42c 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -62,17 +62,18 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% endwith %} {% else %} -
No changelog to display.
+ {% 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 %} + + {% endif %} +No email will be sent.
+