mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 02:36:02 +02:00
Merge branch 'main' into za/profile-page-update-label
This commit is contained in:
commit
2d1ae881eb
8 changed files with 414 additions and 204 deletions
90
.github/workflows/deploy-branch-to-sandbox.yaml
vendored
Normal file
90
.github/workflows/deploy-branch-to-sandbox.yaml
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
# Manually deploy a branch of choice to an environment of choice.
|
||||
|
||||
name: Manual Build and Deploy
|
||||
run-name: Manually build and deploy branch to sandbox of choice
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: 'Environment to deploy'
|
||||
required: true
|
||||
default: 'backup'
|
||||
type: 'choice'
|
||||
options:
|
||||
- ab
|
||||
- backup
|
||||
- cb
|
||||
- dk
|
||||
- es
|
||||
- gd
|
||||
- ko
|
||||
- ky
|
||||
- nl
|
||||
- rb
|
||||
- rh
|
||||
- rjm
|
||||
- meoward
|
||||
- bob
|
||||
- hotgov
|
||||
- litterbox
|
||||
# GitHub Actions has no "good" way yet to dynamically input branches
|
||||
branch:
|
||||
description: 'Branch to deploy'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
|
||||
jobs:
|
||||
variables:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setting global variables
|
||||
uses: actions/github-script@v6
|
||||
id: var
|
||||
with:
|
||||
script: |
|
||||
core.setOutput('environment', '${{ github.head_ref }}'.split("/")[0]);
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Compile USWDS assets
|
||||
working-directory: ./src
|
||||
run: |
|
||||
docker compose run node npm install &&
|
||||
docker compose run node npx gulp copyAssets &&
|
||||
docker compose run node npx gulp compile
|
||||
- name: Collect static assets
|
||||
working-directory: ./src
|
||||
run: docker compose run app python manage.py collectstatic --no-input
|
||||
- name: Deploy to cloud.gov sandbox
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
env:
|
||||
ENVIRONMENT: ${{ github.event.inputs.environment }}
|
||||
CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME
|
||||
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
|
||||
with:
|
||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: ${{ env.ENVIRONMENT }}
|
||||
cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deploy]
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
env:
|
||||
ENVIRONMENT: ${{ github.event.inputs.environment }}
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.'
|
||||
})
|
||||
|
||||
|
|
@ -1342,7 +1342,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
for name, data in fieldsets:
|
||||
fields = data.get("fields", [])
|
||||
fields = tuple(field for field in fields if field not in DomainInformationAdmin.superuser_only_fields)
|
||||
modified_fieldsets.append((name, {"fields": fields}))
|
||||
modified_fieldsets.append((name, {**data, "fields": fields}))
|
||||
return modified_fieldsets
|
||||
return fieldsets
|
||||
|
||||
|
@ -1603,7 +1603,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"is_election_board",
|
||||
"federal_agency",
|
||||
"status_history",
|
||||
"action_needed_reason_email",
|
||||
)
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
|
@ -1657,7 +1656,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
for name, data in fieldsets:
|
||||
fields = data.get("fields", [])
|
||||
fields = tuple(field for field in fields if field not in self.superuser_only_fields)
|
||||
modified_fieldsets.append((name, {"fields": fields}))
|
||||
modified_fieldsets.append((name, {**data, "fields": fields}))
|
||||
return modified_fieldsets
|
||||
return fieldsets
|
||||
|
||||
|
@ -1704,19 +1703,33 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if not change:
|
||||
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)
|
||||
|
||||
# == Handle action_needed_reason == #
|
||||
|
||||
reason_changed = obj.action_needed_reason != original_obj.action_needed_reason
|
||||
if reason_changed:
|
||||
# Track the fact that we sent out an email
|
||||
request.session["action_needed_email_sent"] = True
|
||||
|
||||
# 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)
|
||||
if obj.action_needed_reason_email:
|
||||
emails = self.get_all_action_needed_reason_emails(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
|
||||
else:
|
||||
obj.action_needed_reason_email = default_email
|
||||
|
||||
# == Handle status == #
|
||||
if obj.status == original_obj.status:
|
||||
# If the status hasn't changed, let the base function take care of it
|
||||
return super().save_model(request, obj, form, change)
|
||||
|
||||
# == Handle status changes == #
|
||||
else:
|
||||
# Run some checks on the current object for invalid status changes
|
||||
obj, should_save = self._handle_status_change(request, obj, original_obj)
|
||||
|
||||
|
@ -1724,10 +1737,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
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.
|
||||
|
@ -1921,49 +1930,54 @@ 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)
|
||||
emails = self.get_all_action_needed_reason_emails(obj)
|
||||
extra_context["action_needed_reason_emails"] = json.dumps(emails)
|
||||
extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||
|
||||
# Denote if an action needed email was sent or not
|
||||
email_sent = request.session.get("action_needed_email_sent", False)
|
||||
extra_context["action_needed_email_sent"] = email_sent
|
||||
if email_sent:
|
||||
request.session["action_needed_email_sent"] = False
|
||||
|
||||
# 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):
|
||||
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:
|
||||
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)
|
||||
# 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
|
||||
)
|
||||
|
||||
def _get_action_needed_reason_default_email_text(self, domain_request, action_needed_reason: str):
|
||||
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 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)
|
||||
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
|
||||
return None
|
||||
|
||||
if flag_is_active(None, "profile_feature"): # type: ignore
|
||||
recipient = domain_request.creator
|
||||
else:
|
||||
recipient = domain_request.submitter
|
||||
|
||||
# Return the content of the rendered views
|
||||
# Return the context of the rendered views
|
||||
context = {"domain_request": domain_request, "recipient": recipient}
|
||||
return {
|
||||
"subject_text": subject_template.render(context=context),
|
||||
"email_body_text": template.render(context=context),
|
||||
}
|
||||
|
||||
# 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."""
|
||||
|
|
|
@ -36,6 +36,15 @@ function openInNewTab(el, removeAttribute = false){
|
|||
}
|
||||
};
|
||||
|
||||
// Adds or removes a boolean from our session
|
||||
function addOrRemoveSessionBoolean(name, add){
|
||||
if (add) {
|
||||
sessionStorage.setItem(name, "true");
|
||||
}else {
|
||||
sessionStorage.removeItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||
// Event handlers.
|
||||
|
||||
|
@ -418,15 +427,6 @@ function initializeWidgetOnList(list, parentId) {
|
|||
object.classList.add("display-none");
|
||||
}
|
||||
}
|
||||
|
||||
// Adds or removes a boolean from our session
|
||||
function addOrRemoveSessionBoolean(name, add){
|
||||
if (add) {
|
||||
sessionStorage.setItem(name, "true");
|
||||
}else {
|
||||
sessionStorage.removeItem(name);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
/** An IIFE for toggling the submit bar on domain request forms
|
||||
|
@ -526,54 +526,80 @@ function initializeWidgetOnList(list, parentId) {
|
|||
* This 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) {
|
||||
// Add a change listener to the action needed reason dropdown
|
||||
handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail);
|
||||
}
|
||||
// Since this is an iife, these vars will be removed from memory afterwards
|
||||
var actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
|
||||
var actionNeededEmail = document.querySelector("#id_action_needed_reason_email");
|
||||
var readonlyView = document.querySelector("#action-needed-reason-email-readonly");
|
||||
|
||||
function handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail) {
|
||||
actionNeededReasonDropdown.addEventListener("change", function() {
|
||||
let emailWasSent = document.getElementById("action-needed-email-sent");
|
||||
let actionNeededEmailData = document.getElementById('action-needed-emails-data').textContent;
|
||||
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;
|
||||
|
||||
// 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;
|
||||
// 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);
|
||||
}
|
||||
|
||||
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);
|
||||
// Show an editable email field or a readonly one
|
||||
updateActionNeededEmailDisplay(reason)
|
||||
});
|
||||
|
||||
// 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) {
|
||||
// Replace the email content
|
||||
actionNeededEmail.value = emailBody;
|
||||
|
||||
// Reset the session object on change since change refreshes the email content.
|
||||
if (oldDropdownValue !== actionNeededReasonDropdown.value || oldEmailValue !== actionNeededEmail.value) {
|
||||
let emailSent = sessionStorage.getItem(emailSentSessionVariableName)
|
||||
if (emailSent !== null){
|
||||
addOrRemoveSessionBoolean(emailSentSessionVariableName, add=false)
|
||||
}
|
||||
}
|
||||
}else {
|
||||
showNoEmailMessage(actionNeededEmail);
|
||||
}
|
||||
|
||||
// Show an editable email field or a readonly one
|
||||
updateActionNeededEmailDisplay(reason)
|
||||
});
|
||||
}
|
||||
|
||||
// Show the text field. Hide the "no email" message.
|
||||
function showActionNeededEmail(actionNeededEmail){
|
||||
let noEmailMessage = document.getElementById("no-email-message");
|
||||
showElement(actionNeededEmail);
|
||||
hideElement(noEmailMessage);
|
||||
// Shows an editable email field or a readonly one.
|
||||
// If the email doesn't exist or if we're of reason "other", display that no email was sent.
|
||||
// Likewise, if we've sent this email before, we should just display the content.
|
||||
function updateActionNeededEmailDisplay(reason) {
|
||||
let emailHasBeenSentBefore = sessionStorage.getItem(emailSentSessionVariableName) !== null;
|
||||
let collapseableDiv = readonlyView.querySelector(".collapse--dgsimple");
|
||||
let showMoreButton = document.querySelector("#action_needed_reason_email__show_details");
|
||||
if ((reason && reason != "other") && !emailHasBeenSentBefore) {
|
||||
showElement(actionNeededEmail.parentElement)
|
||||
hideElement(readonlyView)
|
||||
hideElement(showMoreButton)
|
||||
} else {
|
||||
if (!reason || reason === "other") {
|
||||
collapseableDiv.innerHTML = reason ? "No email will be sent." : "-";
|
||||
hideElement(showMoreButton)
|
||||
if (collapseableDiv.classList.contains("collapsed")) {
|
||||
showMoreButton.click()
|
||||
}
|
||||
}else {
|
||||
showElement(showMoreButton)
|
||||
}
|
||||
hideElement(actionNeededEmail.parentElement)
|
||||
showElement(readonlyView)
|
||||
}
|
||||
|
||||
// Hide the text field. Show the "no email" message.
|
||||
function showNoEmailMessage(actionNeededEmail) {
|
||||
let noEmailMessage = document.getElementById("no-email-message");
|
||||
hideElement(actionNeededEmail);
|
||||
showElement(noEmailMessage);
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import annotations
|
||||
from typing import Union
|
||||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
@ -619,8 +618,9 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Handle the action needed email. We send one when moving to action_needed,
|
||||
# but we don't send one when we are _already_ in the state and change the reason.
|
||||
# Handle the action needed email.
|
||||
# An email is sent out when action_needed_reason is changed or added.
|
||||
if self.action_needed_reason and self.status == self.DomainRequestStatus.ACTION_NEEDED:
|
||||
self.sync_action_needed_reason()
|
||||
|
||||
# Update the cached values after saving
|
||||
|
@ -631,10 +631,10 @@ class DomainRequest(TimeStampedModel):
|
|||
was_already_action_needed = self._cached_status == self.DomainRequestStatus.ACTION_NEEDED
|
||||
reason_exists = self._cached_action_needed_reason is not None and self.action_needed_reason is not None
|
||||
reason_changed = self._cached_action_needed_reason != self.action_needed_reason
|
||||
if was_already_action_needed and (reason_exists and reason_changed):
|
||||
if was_already_action_needed and reason_exists and reason_changed:
|
||||
# We don't send emails out in state "other"
|
||||
if self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
||||
self._send_action_needed_reason_email()
|
||||
self._send_action_needed_reason_email(email_content=self.action_needed_reason_email)
|
||||
|
||||
def sync_yes_no_form_fields(self):
|
||||
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||
|
@ -688,7 +688,15 @@ class DomainRequest(TimeStampedModel):
|
|||
logger.error(f"Can't query an approved domain while attempting {called_from}")
|
||||
|
||||
def _send_status_update_email(
|
||||
self, new_status, email_template, email_template_subject, send_email=True, bcc_address="", wrap_email=False
|
||||
self,
|
||||
new_status,
|
||||
email_template,
|
||||
email_template_subject,
|
||||
bcc_address="",
|
||||
context=None,
|
||||
send_email=True,
|
||||
wrap_email=False,
|
||||
custom_email_content=None,
|
||||
):
|
||||
"""Send a status update email to the creator.
|
||||
|
||||
|
@ -699,11 +707,18 @@ class DomainRequest(TimeStampedModel):
|
|||
If the waffle flag "profile_feature" is active, then this email will be sent to the
|
||||
domain request creator rather than the submitter
|
||||
|
||||
Optional args:
|
||||
bcc_address: str -> the address to bcc to
|
||||
|
||||
context: dict -> The context sent to the template
|
||||
|
||||
send_email: bool -> Used to bypass the send_templated_email function, in the event
|
||||
we just want to log that an email would have been sent, rather than actually sending one.
|
||||
|
||||
wrap_email: bool -> Wraps emails using `wrap_text_and_preserve_paragraphs` if any given
|
||||
paragraph exceeds our desired max length (for prettier display).
|
||||
|
||||
custom_email_content: str -> Renders an email with the content of this string as its body text.
|
||||
"""
|
||||
|
||||
recipient = self.creator if flag_is_active(None, "profile_feature") else self.submitter
|
||||
|
@ -721,15 +736,21 @@ class DomainRequest(TimeStampedModel):
|
|||
return None
|
||||
|
||||
try:
|
||||
send_templated_email(
|
||||
email_template,
|
||||
email_template_subject,
|
||||
recipient.email,
|
||||
if not context:
|
||||
context = {
|
||||
"domain_request": self,
|
||||
# This is the user that we refer to in the email
|
||||
"recipient": recipient,
|
||||
},
|
||||
}
|
||||
|
||||
if custom_email_content:
|
||||
context["custom_email_content"] = custom_email_content
|
||||
|
||||
send_templated_email(
|
||||
email_template,
|
||||
email_template_subject,
|
||||
recipient.email,
|
||||
context=context,
|
||||
bcc_address=bcc_address,
|
||||
wrap_email=wrap_email,
|
||||
)
|
||||
|
@ -787,8 +808,8 @@ class DomainRequest(TimeStampedModel):
|
|||
"submission confirmation",
|
||||
"emails/submission_confirmation.txt",
|
||||
"emails/submission_confirmation_subject.txt",
|
||||
True,
|
||||
bcc_address,
|
||||
send_email=True,
|
||||
bcc_address=bcc_address,
|
||||
)
|
||||
|
||||
@transition(
|
||||
|
@ -858,41 +879,26 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
# Send out an email if an action needed reason exists
|
||||
if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
||||
self._send_action_needed_reason_email(send_email)
|
||||
email_content = self.action_needed_reason_email
|
||||
self._send_action_needed_reason_email(send_email, email_content)
|
||||
|
||||
def _send_action_needed_reason_email(self, send_email=True):
|
||||
def _send_action_needed_reason_email(self, send_email=True, email_content=None):
|
||||
"""Sends out an automatic email for each valid action needed reason provided"""
|
||||
|
||||
# Store the filenames of the template and template subject
|
||||
email_template_name: str = ""
|
||||
email_template_subject_name: str = ""
|
||||
|
||||
# Check for the "type" of action needed reason.
|
||||
can_send_email = True
|
||||
match self.action_needed_reason:
|
||||
# Add to this match if you need to pass in a custom filename for these templates.
|
||||
case self.ActionNeededReasons.OTHER, _:
|
||||
# Unknown and other are default cases - do nothing
|
||||
can_send_email = False
|
||||
|
||||
# Assumes that the template name matches the action needed reason if nothing is specified.
|
||||
# This is so you can override if you need, or have this taken care of for you.
|
||||
if not email_template_name and not email_template_subject_name:
|
||||
email_template_name = f"{self.action_needed_reason}.txt"
|
||||
email_template_name = "custom_email.txt"
|
||||
email_template_subject_name = f"{self.action_needed_reason}_subject.txt"
|
||||
|
||||
bcc_address = ""
|
||||
if settings.IS_PRODUCTION:
|
||||
bcc_address = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# If we can, try to send out an email as long as send_email=True
|
||||
if can_send_email:
|
||||
self._send_status_update_email(
|
||||
new_status="action needed",
|
||||
email_template=f"emails/action_needed_reasons/{email_template_name}",
|
||||
email_template_subject=f"emails/action_needed_reasons/{email_template_subject_name}",
|
||||
send_email=send_email,
|
||||
bcc_address=bcc_address,
|
||||
custom_email_content=email_content,
|
||||
wrap_email=True,
|
||||
)
|
||||
|
||||
|
@ -952,7 +958,7 @@ class DomainRequest(TimeStampedModel):
|
|||
"domain request approved",
|
||||
"emails/status_change_approved.txt",
|
||||
"emails/status_change_approved_subject.txt",
|
||||
send_email,
|
||||
send_email=send_email,
|
||||
)
|
||||
|
||||
@transition(
|
||||
|
|
|
@ -143,6 +143,28 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% endwith %}
|
||||
{% endblock field_readonly %}
|
||||
|
||||
{% block field_other %}
|
||||
{% if field.field.name == "action_needed_reason_email" %}
|
||||
<div id="action-needed-reason-email-readonly" class="readonly margin-top-0 padding-top-0 display-none">
|
||||
<div class="margin-top-05 collapse--dgsimple collapsed">
|
||||
{{ field.field.value|linebreaks }}
|
||||
</div>
|
||||
<button id="action_needed_reason_email__show_details" 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>
|
||||
<div>
|
||||
{{ field.field }}
|
||||
<input id="action-needed-email-sent" class="display-none" value="{{action_needed_email_sent}}">
|
||||
</div>
|
||||
{% else %}
|
||||
{{ field.field }}
|
||||
{% endif %}
|
||||
{% endblock field_other %}
|
||||
|
||||
{% block after_help_text %}
|
||||
{% if field.field.name == "action_needed_reason_email" %}
|
||||
{% comment %}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
{{ custom_email_content }}
|
||||
{% endautoescape %}
|
|
@ -1399,13 +1399,18 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertContains(response, "status in [submitted,in review,action needed]", count=1)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def transition_state_and_send_email(self, domain_request, status, rejection_reason=None, action_needed_reason=None):
|
||||
def transition_state_and_send_email(
|
||||
self, domain_request, status, rejection_reason=None, action_needed_reason=None, action_needed_reason_email=None
|
||||
):
|
||||
"""Helper method for the email test cases."""
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||
|
||||
# Create a fake session to hook to
|
||||
request.session = {}
|
||||
|
||||
# Modify the domain request's properties
|
||||
domain_request.status = status
|
||||
|
||||
|
@ -1415,6 +1420,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
if action_needed_reason:
|
||||
domain_request.action_needed_reason = action_needed_reason
|
||||
|
||||
if action_needed_reason_email:
|
||||
domain_request.action_needed_reason_email = action_needed_reason_email
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, domain_request, form=None, change=True)
|
||||
|
||||
|
@ -1468,6 +1476,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
# Test the email sent out for already_has_domains
|
||||
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains)
|
||||
|
||||
self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
|
@ -1487,7 +1496,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Test the email sent out for questionable_so
|
||||
# Test that a custom email is sent out for questionable_so
|
||||
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so)
|
||||
self.assert_email_is_accurate(
|
||||
|
@ -1502,6 +1511,43 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
# Should be unchanged from before
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||
|
||||
# Tests if an analyst can override existing email content
|
||||
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
||||
self.transition_state_and_send_email(
|
||||
domain_request,
|
||||
action_needed,
|
||||
action_needed_reason=questionable_so,
|
||||
action_needed_reason_email="custom email content",
|
||||
)
|
||||
|
||||
domain_request.refresh_from_db()
|
||||
self.assert_email_is_accurate("custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
||||
|
||||
# Tests if a new email gets sent when just the email is changed.
|
||||
# An email should NOT be sent out if we just modify the email content.
|
||||
self.transition_state_and_send_email(
|
||||
domain_request,
|
||||
action_needed,
|
||||
action_needed_reason=questionable_so,
|
||||
action_needed_reason_email="dummy email content",
|
||||
)
|
||||
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
||||
|
||||
# Set the request back to in review
|
||||
domain_request.in_review()
|
||||
|
||||
# Try sending another email when changing states AND including content
|
||||
self.transition_state_and_send_email(
|
||||
domain_request,
|
||||
action_needed,
|
||||
action_needed_reason=eligibility_unclear,
|
||||
action_needed_reason_email="custom content when starting anew",
|
||||
)
|
||||
self.assert_email_is_accurate("custom content when starting anew", 5, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
|
||||
|
||||
def test_save_model_sends_submitted_email(self):
|
||||
"""When transitioning to submitted from started or withdrawn on a domain request,
|
||||
an email is sent out.
|
||||
|
@ -2242,8 +2288,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertContains(response, "When a domain request is in ineligible status")
|
||||
self.assertContains(response, "Yes, select ineligible status")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_readonly_when_restricted_creator(self):
|
||||
with less_console_noise():
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
domain_request.creator.status = User.RESTRICTED
|
||||
|
@ -2261,7 +2307,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"is_election_board",
|
||||
"federal_agency",
|
||||
"status_history",
|
||||
"action_needed_reason_email",
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
|
@ -2323,7 +2368,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"is_election_board",
|
||||
"federal_agency",
|
||||
"status_history",
|
||||
"action_needed_reason_email",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
|
@ -2355,7 +2399,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"is_election_board",
|
||||
"federal_agency",
|
||||
"status_history",
|
||||
"action_needed_reason_email",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
@ -2425,6 +2468,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||
request.user = self.superuser
|
||||
|
||||
request.session = {}
|
||||
|
||||
# Define a custom implementation for is_active
|
||||
def custom_is_active(self):
|
||||
return domain_is_active # Override to return True
|
||||
|
|
|
@ -46,6 +46,10 @@ def send_templated_email(
|
|||
template = get_template(template_name)
|
||||
email_body = template.render(context=context)
|
||||
|
||||
# Do cleanup on the email body. For emails with custom content.
|
||||
if email_body:
|
||||
email_body.strip().lstrip("\n")
|
||||
|
||||
subject_template = get_template(subject_template_name)
|
||||
subject = subject_template.render(context=context)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue