mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-05 18:53:28 +02:00
Merge branch 'main' of https://github.com/cisagov/manage.get.gov into rh/2779-update-suborg-text
This commit is contained in:
commit
c9267c5307
15 changed files with 765 additions and 336 deletions
|
@ -5,6 +5,11 @@ from django import forms
|
||||||
from django.db.models import Value, CharField, Q
|
from django.db.models import Value, CharField, Q
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
from registrar.utility.admin_helpers import (
|
||||||
|
get_action_needed_reason_default_email,
|
||||||
|
get_rejection_reason_default_email,
|
||||||
|
get_field_links_as_list,
|
||||||
|
)
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
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
|
||||||
|
@ -20,11 +25,6 @@ from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from waffle.admin import FlagAdmin
|
from waffle.admin import FlagAdmin
|
||||||
from waffle.models import Sample, Switch
|
from waffle.models import Sample, Switch
|
||||||
from registrar.utility.admin_helpers import (
|
|
||||||
get_all_action_needed_reason_emails,
|
|
||||||
get_action_needed_reason_default_email,
|
|
||||||
get_field_links_as_list,
|
|
||||||
)
|
|
||||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||||
|
@ -237,6 +237,7 @@ class DomainRequestAdminForm(forms.ModelForm):
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
"action_needed_reason_email": "Email",
|
"action_needed_reason_email": "Email",
|
||||||
|
"rejection_reason_email": "Email",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -1750,6 +1751,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"status_history",
|
"status_history",
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
|
"rejection_reason_email",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
"action_needed_reason_email",
|
"action_needed_reason_email",
|
||||||
"investigator",
|
"investigator",
|
||||||
|
@ -1905,25 +1907,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
# Get the original domain request from the database.
|
# Get the original domain request from the database.
|
||||||
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
||||||
|
|
||||||
# == Handle action_needed_reason == #
|
# == Handle action needed and rejected emails == #
|
||||||
|
# Edge case: this logic is handled by javascript, so contexts outside that must be handled
|
||||||
reason_changed = obj.action_needed_reason != original_obj.action_needed_reason
|
obj = self._handle_custom_emails(obj)
|
||||||
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 = get_action_needed_reason_default_email(request, obj, obj.action_needed_reason)
|
|
||||||
if obj.action_needed_reason_email:
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
obj.action_needed_reason_email = default_email
|
|
||||||
|
|
||||||
|
# == Handle allowed emails == #
|
||||||
if obj.status in DomainRequest.get_statuses_that_send_emails() and not settings.IS_PRODUCTION:
|
if obj.status in DomainRequest.get_statuses_that_send_emails() and not settings.IS_PRODUCTION:
|
||||||
self._check_for_valid_email(request, obj)
|
self._check_for_valid_email(request, obj)
|
||||||
|
|
||||||
|
@ -1939,6 +1927,15 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if should_save:
|
if should_save:
|
||||||
return super().save_model(request, obj, form, change)
|
return super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
def _handle_custom_emails(self, obj):
|
||||||
|
if obj.status == DomainRequest.DomainRequestStatus.ACTION_NEEDED:
|
||||||
|
if obj.action_needed_reason and not obj.action_needed_reason_email:
|
||||||
|
obj.action_needed_reason_email = get_action_needed_reason_default_email(obj, obj.action_needed_reason)
|
||||||
|
elif obj.status == DomainRequest.DomainRequestStatus.REJECTED:
|
||||||
|
if obj.rejection_reason and not obj.rejection_reason_email:
|
||||||
|
obj.rejection_reason_email = get_rejection_reason_default_email(obj, obj.rejection_reason)
|
||||||
|
return obj
|
||||||
|
|
||||||
def _check_for_valid_email(self, request, obj):
|
def _check_for_valid_email(self, request, obj):
|
||||||
"""Certain emails are whitelisted in non-production environments,
|
"""Certain emails are whitelisted in non-production environments,
|
||||||
so we should display that information using this function.
|
so we should display that information using this function.
|
||||||
|
|
|
@ -344,69 +344,6 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
|
||||||
* status select and to show/hide the rejection reason
|
|
||||||
*/
|
|
||||||
(function (){
|
|
||||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
|
||||||
// This is the "action needed reason" field
|
|
||||||
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
|
||||||
// This is the "Email" field
|
|
||||||
let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email')
|
|
||||||
|
|
||||||
if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) {
|
|
||||||
let statusSelect = document.getElementById('id_status')
|
|
||||||
let isRejected = statusSelect.value == "rejected"
|
|
||||||
let isActionNeeded = statusSelect.value == "action needed"
|
|
||||||
|
|
||||||
// Initial handling of rejectionReasonFormGroup display
|
|
||||||
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
|
||||||
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
|
||||||
|
|
||||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
|
||||||
statusSelect.addEventListener('change', function() {
|
|
||||||
// Show the rejection reason field if the status is rejected.
|
|
||||||
// Then track if its shown or hidden in our session cache.
|
|
||||||
isRejected = statusSelect.value == "rejected"
|
|
||||||
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
|
||||||
addOrRemoveSessionBoolean("showRejectionReason", add=isRejected)
|
|
||||||
|
|
||||||
isActionNeeded = statusSelect.value == "action needed"
|
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
|
||||||
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
|
||||||
addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
|
||||||
|
|
||||||
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
|
||||||
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
|
|
||||||
// accurately for this edge case, we use cache and test for the back/forward navigation.
|
|
||||||
const observer = new PerformanceObserver((list) => {
|
|
||||||
list.getEntries().forEach((entry) => {
|
|
||||||
if (entry.type === "back_forward") {
|
|
||||||
let showRejectionReason = sessionStorage.getItem("showRejectionReason") !== null
|
|
||||||
showOrHideObject(rejectionReasonFormGroup, show=showRejectionReason)
|
|
||||||
|
|
||||||
let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null
|
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason)
|
|
||||||
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
observer.observe({ type: "navigation" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds or removes the display-none class to object depending on the value of boolean show
|
|
||||||
function showOrHideObject(object, show){
|
|
||||||
if (show){
|
|
||||||
object.classList.remove("display-none");
|
|
||||||
}else {
|
|
||||||
object.classList.add("display-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
/** An IIFE for toggling the submit bar on domain request forms
|
/** An IIFE for toggling the submit bar on domain request forms
|
||||||
*/
|
*/
|
||||||
|
@ -501,86 +438,110 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
/** An IIFE that hooks to the show/hide button underneath action needed reason.
|
class CustomizableEmailBase {
|
||||||
* This shows the auto generated email on action needed reason.
|
|
||||||
|
/**
|
||||||
|
* @param {Object} config - must contain the following:
|
||||||
|
* @property {HTMLElement} dropdown - The dropdown element.
|
||||||
|
* @property {HTMLElement} textarea - The textarea element.
|
||||||
|
* @property {HTMLElement} lastSentEmailContent - The last sent email content element.
|
||||||
|
* @property {HTMLElement} textAreaFormGroup - The form group for the textarea.
|
||||||
|
* @property {HTMLElement} dropdownFormGroup - The form group for the dropdown.
|
||||||
|
* @property {HTMLElement} modalConfirm - The confirm button in the modal.
|
||||||
|
* @property {string} apiUrl - The API URL for fetching email content.
|
||||||
|
* @property {string} statusToCheck - The status to check against. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||||
|
* @property {string} sessionVariableName - The session variable name. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||||
|
* @property {string} apiErrorMessage - The error message that the ajax call returns.
|
||||||
*/
|
*/
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
constructor(config) {
|
||||||
const dropdown = document.getElementById("id_action_needed_reason");
|
this.config = config;
|
||||||
const textarea = document.getElementById("id_action_needed_reason_email")
|
this.dropdown = config.dropdown;
|
||||||
const domainRequestId = dropdown ? document.getElementById("domain_request_id").value : null
|
this.textarea = config.textarea;
|
||||||
const textareaPlaceholder = document.querySelector(".field-action_needed_reason_email__placeholder");
|
this.lastSentEmailContent = config.lastSentEmailContent;
|
||||||
const directEditButton = document.querySelector('.field-action_needed_reason_email__edit');
|
this.apiUrl = config.apiUrl;
|
||||||
const modalTrigger = document.querySelector('.field-action_needed_reason_email__modal-trigger');
|
this.apiErrorMessage = config.apiErrorMessage;
|
||||||
const modalConfirm = document.getElementById('confirm-edit-email');
|
this.modalConfirm = config.modalConfirm;
|
||||||
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;
|
|
||||||
let initialEmailValue;
|
|
||||||
if (textarea)
|
|
||||||
initialEmailValue = textarea.value
|
|
||||||
|
|
||||||
// We will use the const to control the modal
|
// These fields are hidden/shown on pageload depending on the current status
|
||||||
let isEmailAlreadySentConst;
|
this.textAreaFormGroup = config.textAreaFormGroup;
|
||||||
if (lastSentEmailContent)
|
this.dropdownFormGroup = config.dropdownFormGroup;
|
||||||
isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
this.statusToCheck = config.statusToCheck;
|
||||||
// We will use the function to control the label and help
|
this.sessionVariableName = config.sessionVariableName;
|
||||||
function isEmailAlreadySent() {
|
|
||||||
return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
// Non-configurable variables
|
||||||
|
this.statusSelect = document.getElementById("id_status");
|
||||||
|
this.domainRequestId = this.dropdown ? document.getElementById("domain_request_id").value : null
|
||||||
|
this.initialDropdownValue = this.dropdown ? this.dropdown.value : null;
|
||||||
|
this.initialEmailValue = this.textarea ? this.textarea.value : null;
|
||||||
|
|
||||||
|
// Find other fields near the textarea
|
||||||
|
const parentDiv = this.textarea ? this.textarea.closest(".flex-container") : null;
|
||||||
|
this.directEditButton = parentDiv ? parentDiv.querySelector(".edit-email-button") : null;
|
||||||
|
this.modalTrigger = parentDiv ? parentDiv.querySelector(".edit-button-modal-trigger") : null;
|
||||||
|
|
||||||
|
this.textareaPlaceholder = parentDiv ? parentDiv.querySelector(".custom-email-placeholder") : null;
|
||||||
|
this.formLabel = this.textarea ? document.querySelector(`label[for="${this.textarea.id}"]`) : null;
|
||||||
|
|
||||||
|
this.isEmailAlreadySentConst;
|
||||||
|
if (this.lastSentEmailContent && this.textarea) {
|
||||||
|
this.isEmailAlreadySentConst = this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dropdown || !textarea || !domainRequestId || !formLabel || !modalConfirm) return;
|
}
|
||||||
const apiUrl = document.getElementById("get-action-needed-email-for-user-json").value;
|
|
||||||
|
|
||||||
function updateUserInterface(reason) {
|
// Handle showing/hiding the related fields on page load.
|
||||||
if (!reason) {
|
initializeFormGroups() {
|
||||||
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
|
let isStatus = this.statusSelect.value == this.statusToCheck;
|
||||||
formLabel.innerHTML = "Email:";
|
|
||||||
textareaPlaceholder.innerHTML = "Select an action needed reason to see email";
|
// Initial handling of these groups.
|
||||||
showElement(textareaPlaceholder);
|
this.updateFormGroupVisibility(isStatus);
|
||||||
hideElement(directEditButton);
|
|
||||||
hideElement(modalTrigger);
|
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||||
hideElement(textarea);
|
this.statusSelect.addEventListener('change', () => {
|
||||||
} else if (reason === 'other') {
|
// Show the action needed field if the status is what we expect.
|
||||||
// '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
|
// Then track if its shown or hidden in our session cache.
|
||||||
formLabel.innerHTML = "Email:";
|
isStatus = this.statusSelect.value == this.statusToCheck;
|
||||||
textareaPlaceholder.innerHTML = "No email will be sent";
|
this.updateFormGroupVisibility(isStatus);
|
||||||
showElement(textareaPlaceholder);
|
addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
|
||||||
hideElement(directEditButton);
|
});
|
||||||
hideElement(modalTrigger);
|
|
||||||
hideElement(textarea);
|
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
||||||
|
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
||||||
|
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
|
||||||
|
// accurately for this edge case, we use cache and test for the back/forward navigation.
|
||||||
|
const observer = new PerformanceObserver((list) => {
|
||||||
|
list.getEntries().forEach((entry) => {
|
||||||
|
if (entry.type === "back_forward") {
|
||||||
|
let showTextAreaFormGroup = sessionStorage.getItem(this.sessionVariableName) !== null;
|
||||||
|
this.updateFormGroupVisibility(showTextAreaFormGroup);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe({ type: "navigation" });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFormGroupVisibility(showFormGroups) {
|
||||||
|
if (showFormGroups) {
|
||||||
|
showElement(this.textAreaFormGroup);
|
||||||
|
showElement(this.dropdownFormGroup);
|
||||||
}else {
|
}else {
|
||||||
// A triggering selection is selected, all hands on board:
|
hideElement(this.textAreaFormGroup);
|
||||||
textarea.setAttribute('readonly', true);
|
hideElement(this.dropdownFormGroup);
|
||||||
showElement(textarea);
|
|
||||||
hideElement(textareaPlaceholder);
|
|
||||||
|
|
||||||
if (isEmailAlreadySentConst) {
|
|
||||||
hideElement(directEditButton);
|
|
||||||
showElement(modalTrigger);
|
|
||||||
} else {
|
|
||||||
showElement(directEditButton);
|
|
||||||
hideElement(modalTrigger);
|
|
||||||
}
|
|
||||||
if (isEmailAlreadySent()) {
|
|
||||||
formLabel.innerHTML = "Email sent to creator:";
|
|
||||||
} else {
|
|
||||||
formLabel.innerHTML = "Email:";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize UI
|
initializeDropdown() {
|
||||||
updateUserInterface(dropdown.value);
|
this.dropdown.addEventListener("change", () => {
|
||||||
|
let reason = this.dropdown.value;
|
||||||
dropdown.addEventListener("change", function() {
|
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
||||||
const reason = dropdown.value;
|
let searchParams = new URLSearchParams(
|
||||||
// Update the UI
|
{
|
||||||
updateUserInterface(reason);
|
"reason": reason,
|
||||||
if (reason && reason !== "other") {
|
"domain_request_id": this.domainRequestId,
|
||||||
// If it's not the initial value
|
}
|
||||||
if (initialDropdownValue !== dropdown.value || initialEmailValue !== textarea.value) {
|
);
|
||||||
// Replace the email content
|
// Replace the email content
|
||||||
fetch(`${apiUrl}?reason=${reason}&domain_request_id=${domainRequestId}`)
|
fetch(`${this.apiUrl}?${searchParams.toString()}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.json().then(data => data);
|
return response.json().then(data => data);
|
||||||
})
|
})
|
||||||
|
@ -588,30 +549,213 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.error("Error in AJAX call: " + data.error);
|
console.error("Error in AJAX call: " + data.error);
|
||||||
}else {
|
}else {
|
||||||
textarea.value = data.action_needed_email;
|
this.textarea.value = data.email;
|
||||||
}
|
}
|
||||||
updateUserInterface(reason);
|
this.updateUserInterface(reason);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Error action needed email: ", error)
|
console.error(this.apiErrorMessage, error)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeModalConfirm() {
|
||||||
|
this.modalConfirm.addEventListener("click", () => {
|
||||||
|
this.textarea.removeAttribute('readonly');
|
||||||
|
this.textarea.focus();
|
||||||
|
hideElement(this.directEditButton);
|
||||||
|
hideElement(this.modalTrigger);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDirectEditButton() {
|
||||||
|
this.directEditButton.addEventListener("click", () => {
|
||||||
|
this.textarea.removeAttribute('readonly');
|
||||||
|
this.textarea.focus();
|
||||||
|
hideElement(this.directEditButton);
|
||||||
|
hideElement(this.modalTrigger);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmailAlreadySent() {
|
||||||
|
return this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserInterface(reason=this.dropdown.value, excluded_reasons=["other"]) {
|
||||||
|
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
|
||||||
|
this.showPlaceholderNoReason();
|
||||||
|
} else if (excluded_reasons.includes(reason)) {
|
||||||
|
// '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
|
||||||
|
this.showPlaceholderOtherReason();
|
||||||
|
} else {
|
||||||
|
this.showReadonlyTextarea();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function that makes overriding the readonly textarea easy
|
||||||
|
showReadonlyTextarea() {
|
||||||
|
// A triggering selection is selected, all hands on board:
|
||||||
|
this.textarea.setAttribute('readonly', true);
|
||||||
|
showElement(this.textarea);
|
||||||
|
hideElement(this.textareaPlaceholder);
|
||||||
|
|
||||||
|
if (this.isEmailAlreadySentConst) {
|
||||||
|
hideElement(this.directEditButton);
|
||||||
|
showElement(this.modalTrigger);
|
||||||
|
} else {
|
||||||
|
showElement(this.directEditButton);
|
||||||
|
hideElement(this.modalTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isEmailAlreadySent()) {
|
||||||
|
this.formLabel.innerHTML = "Email sent to creator:";
|
||||||
|
} else {
|
||||||
|
this.formLabel.innerHTML = "Email:";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function that makes overriding the placeholder reason easy
|
||||||
|
showPlaceholderNoReason() {
|
||||||
|
this.showPlaceholder("Email:", "Select a reason to see email");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function that makes overriding the placeholder reason easy
|
||||||
|
showPlaceholderOtherReason() {
|
||||||
|
this.showPlaceholder("Email:", "No email will be sent");
|
||||||
|
}
|
||||||
|
|
||||||
|
showPlaceholder(formLabelText, placeholderText) {
|
||||||
|
this.formLabel.innerHTML = formLabelText;
|
||||||
|
this.textareaPlaceholder.innerHTML = placeholderText;
|
||||||
|
showElement(this.textareaPlaceholder);
|
||||||
|
hideElement(this.directEditButton);
|
||||||
|
hideElement(this.modalTrigger);
|
||||||
|
hideElement(this.textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class customActionNeededEmail extends CustomizableEmailBase {
|
||||||
|
constructor() {
|
||||||
|
const emailConfig = {
|
||||||
|
dropdown: document.getElementById("id_action_needed_reason"),
|
||||||
|
textarea: document.getElementById("id_action_needed_reason_email"),
|
||||||
|
lastSentEmailContent: document.getElementById("last-sent-action-needed-email-content"),
|
||||||
|
modalConfirm: document.getElementById("action-needed-reason__confirm-edit-email"),
|
||||||
|
apiUrl: document.getElementById("get-action-needed-email-for-user-json")?.value || null,
|
||||||
|
textAreaFormGroup: document.querySelector('.field-action_needed_reason'),
|
||||||
|
dropdownFormGroup: document.querySelector('.field-action_needed_reason_email'),
|
||||||
|
statusToCheck: "action needed",
|
||||||
|
sessionVariableName: "showActionNeededReason",
|
||||||
|
apiErrorMessage: "Error when attempting to grab action needed email: "
|
||||||
|
}
|
||||||
|
super(emailConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadActionNeededEmail() {
|
||||||
|
// Hide/show the email fields depending on the current status
|
||||||
|
this.initializeFormGroups();
|
||||||
|
// Setup the textarea, edit button, helper text
|
||||||
|
this.updateUserInterface();
|
||||||
|
this.initializeDropdown();
|
||||||
|
this.initializeModalConfirm();
|
||||||
|
this.initializeDirectEditButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the placeholder text when no reason is selected
|
||||||
|
showPlaceholderNoReason() {
|
||||||
|
this.showPlaceholder("Email:", "Select an action needed reason to see email");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the placeholder text when the reason other is selected
|
||||||
|
showPlaceholderOtherReason() {
|
||||||
|
this.showPlaceholder("Email:", "No email will be sent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An IIFE that hooks to the show/hide button underneath action needed reason.
|
||||||
|
* This shows the auto generated email on action needed reason.
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const domainRequestForm = document.getElementById("domainrequest_form");
|
||||||
|
if (!domainRequestForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize UI
|
||||||
|
const customEmail = new customActionNeededEmail();
|
||||||
|
|
||||||
|
// Check that every variable was setup correctly
|
||||||
|
const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
|
||||||
|
if (nullItems.length > 0) {
|
||||||
|
console.error(`Failed to load customActionNeededEmail(). Some variables were null: ${nullItems.join(", ")}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customEmail.loadActionNeededEmail()
|
||||||
});
|
});
|
||||||
|
|
||||||
modalConfirm.addEventListener("click", () => {
|
|
||||||
textarea.removeAttribute('readonly');
|
class customRejectedEmail extends CustomizableEmailBase {
|
||||||
textarea.focus();
|
constructor() {
|
||||||
hideElement(directEditButton);
|
const emailConfig = {
|
||||||
hideElement(modalTrigger);
|
dropdown: document.getElementById("id_rejection_reason"),
|
||||||
});
|
textarea: document.getElementById("id_rejection_reason_email"),
|
||||||
directEditButton.addEventListener("click", () => {
|
lastSentEmailContent: document.getElementById("last-sent-rejection-email-content"),
|
||||||
textarea.removeAttribute('readonly');
|
modalConfirm: document.getElementById("rejection-reason__confirm-edit-email"),
|
||||||
textarea.focus();
|
apiUrl: document.getElementById("get-rejection-email-for-user-json")?.value || null,
|
||||||
hideElement(directEditButton);
|
textAreaFormGroup: document.querySelector('.field-rejection_reason'),
|
||||||
hideElement(modalTrigger);
|
dropdownFormGroup: document.querySelector('.field-rejection_reason_email'),
|
||||||
});
|
statusToCheck: "rejected",
|
||||||
|
sessionVariableName: "showRejectionReason",
|
||||||
|
errorMessage: "Error when attempting to grab rejected email: "
|
||||||
|
};
|
||||||
|
super(emailConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRejectedEmail() {
|
||||||
|
this.initializeFormGroups();
|
||||||
|
this.updateUserInterface();
|
||||||
|
this.initializeDropdown();
|
||||||
|
this.initializeModalConfirm();
|
||||||
|
this.initializeDirectEditButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the placeholder text when no reason is selected
|
||||||
|
showPlaceholderNoReason() {
|
||||||
|
this.showPlaceholder("Email:", "Select a rejection reason to see email");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserInterface(reason=this.dropdown.value, excluded_reasons=[]) {
|
||||||
|
super.updateUserInterface(reason, excluded_reasons);
|
||||||
|
}
|
||||||
|
// Overrides the placeholder text when the reason other is selected
|
||||||
|
// showPlaceholderOtherReason() {
|
||||||
|
// this.showPlaceholder("Email:", "No email will be sent");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** An IIFE that hooks to the show/hide button underneath rejected reason.
|
||||||
|
* This shows the auto generated email on action needed reason.
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const domainRequestForm = document.getElementById("domainrequest_form");
|
||||||
|
if (!domainRequestForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize UI
|
||||||
|
const customEmail = new customRejectedEmail();
|
||||||
|
// Check that every variable was setup correctly
|
||||||
|
const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
|
||||||
|
if (nullItems.length > 0) {
|
||||||
|
console.error(`Failed to load customRejectedEmail(). Some variables were null: ${nullItems.join(", ")}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customEmail.loadRejectedEmail()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ from registrar.views.utility.api_views import (
|
||||||
get_senior_official_from_federal_agency_json,
|
get_senior_official_from_federal_agency_json,
|
||||||
get_federal_and_portfolio_types_from_federal_agency_json,
|
get_federal_and_portfolio_types_from_federal_agency_json,
|
||||||
get_action_needed_email_for_user_json,
|
get_action_needed_email_for_user_json,
|
||||||
|
get_rejection_email_for_user_json,
|
||||||
)
|
)
|
||||||
|
|
||||||
from registrar.views.domain_request import Step, PortfolioDomainRequestStep
|
from registrar.views.domain_request import Step, PortfolioDomainRequestStep
|
||||||
|
@ -175,6 +176,11 @@ urlpatterns = [
|
||||||
get_action_needed_email_for_user_json,
|
get_action_needed_email_for_user_json,
|
||||||
name="get-action-needed-email-for-user-json",
|
name="get-action-needed-email-for-user-json",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"admin/api/get-rejection-email-for-user-json/",
|
||||||
|
get_rejection_email_for_user_json,
|
||||||
|
name="get-rejection-email-for-user-json",
|
||||||
|
),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path(
|
path(
|
||||||
"reports/export_data_type_user/",
|
"reports/export_data_type_user/",
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-10-08 18:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0132_alter_domaininformation_portfolio_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="rejection_reason_email",
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="rejection_reason",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("domain_purpose", "Purpose requirements not met"),
|
||||||
|
("requestor_not_eligible", "Requestor not eligible to make request"),
|
||||||
|
("org_has_domain", "Org already has a .gov domain"),
|
||||||
|
("contacts_not_verified", "Org contacts couldn't be verified"),
|
||||||
|
("org_not_eligible", "Org not eligible for a .gov domain"),
|
||||||
|
("naming_requirements", "Naming requirements not met"),
|
||||||
|
("other", "Other/Unspecified"),
|
||||||
|
],
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -254,18 +254,18 @@ class DomainRequest(TimeStampedModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
class RejectionReasons(models.TextChoices):
|
class RejectionReasons(models.TextChoices):
|
||||||
DOMAIN_PURPOSE = "purpose_not_met", "Purpose requirements not met"
|
DOMAIN_PURPOSE = "domain_purpose", "Purpose requirements not met"
|
||||||
REQUESTOR = "requestor_not_eligible", "Requestor not eligible to make request"
|
REQUESTOR_NOT_ELIGIBLE = "requestor_not_eligible", "Requestor not eligible to make request"
|
||||||
SECOND_DOMAIN_REASONING = (
|
ORG_HAS_DOMAIN = (
|
||||||
"org_has_domain",
|
"org_has_domain",
|
||||||
"Org already has a .gov domain",
|
"Org already has a .gov domain",
|
||||||
)
|
)
|
||||||
CONTACTS_OR_ORGANIZATION_LEGITIMACY = (
|
CONTACTS_NOT_VERIFIED = (
|
||||||
"contacts_not_verified",
|
"contacts_not_verified",
|
||||||
"Org contacts couldn't be verified",
|
"Org contacts couldn't be verified",
|
||||||
)
|
)
|
||||||
ORGANIZATION_ELIGIBILITY = "org_not_eligible", "Org not eligible for a .gov domain"
|
ORG_NOT_ELIGIBLE = "org_not_eligible", "Org not eligible for a .gov domain"
|
||||||
NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met"
|
NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met"
|
||||||
OTHER = "other", "Other/Unspecified"
|
OTHER = "other", "Other/Unspecified"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -300,6 +300,11 @@ class DomainRequest(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rejection_reason_email = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
action_needed_reason = models.TextField(
|
action_needed_reason = models.TextField(
|
||||||
choices=ActionNeededReasons.choices,
|
choices=ActionNeededReasons.choices,
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -635,15 +640,16 @@ class DomainRequest(TimeStampedModel):
|
||||||
# Actually updates the organization_type field
|
# Actually updates the organization_type field
|
||||||
org_type_helper.create_or_update_organization_type()
|
org_type_helper.create_or_update_organization_type()
|
||||||
|
|
||||||
def _cache_status_and_action_needed_reason(self):
|
def _cache_status_and_status_reasons(self):
|
||||||
"""Maintains a cache of properties so we can avoid a DB call"""
|
"""Maintains a cache of properties so we can avoid a DB call"""
|
||||||
self._cached_action_needed_reason = self.action_needed_reason
|
self._cached_action_needed_reason = self.action_needed_reason
|
||||||
|
self._cached_rejection_reason = self.rejection_reason
|
||||||
self._cached_status = self.status
|
self._cached_status = self.status
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Store original values for caching purposes. Used to compare them on save.
|
# Store original values for caching purposes. Used to compare them on save.
|
||||||
self._cache_status_and_action_needed_reason()
|
self._cache_status_and_status_reasons()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Save override for custom properties"""
|
"""Save override for custom properties"""
|
||||||
|
@ -655,23 +661,63 @@ class DomainRequest(TimeStampedModel):
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Handle the action needed email.
|
# Handle custom status emails.
|
||||||
# An email is sent out when action_needed_reason is changed or added.
|
# An email is sent out when a, for example, action_needed_reason is changed or added.
|
||||||
if self.action_needed_reason and self.status == self.DomainRequestStatus.ACTION_NEEDED:
|
statuses_that_send_custom_emails = [self.DomainRequestStatus.ACTION_NEEDED, self.DomainRequestStatus.REJECTED]
|
||||||
self.sync_action_needed_reason()
|
if self.status in statuses_that_send_custom_emails:
|
||||||
|
self.send_custom_status_update_email(self.status)
|
||||||
|
|
||||||
# Update the cached values after saving
|
# Update the cached values after saving
|
||||||
self._cache_status_and_action_needed_reason()
|
self._cache_status_and_status_reasons()
|
||||||
|
|
||||||
def sync_action_needed_reason(self):
|
def send_custom_status_update_email(self, status):
|
||||||
"""Checks if we need to send another action needed email"""
|
"""Helper function to send out a second status email when the status remains the same,
|
||||||
was_already_action_needed = self._cached_status == self.DomainRequestStatus.ACTION_NEEDED
|
but the reason has changed."""
|
||||||
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
|
# Currently, we store all this information in three variables.
|
||||||
if was_already_action_needed and reason_exists and reason_changed:
|
# When adding new reasons, this can be a lot to manage so we store it here
|
||||||
# We don't send emails out in state "other"
|
# in a centralized location. However, this may need to change if this scales.
|
||||||
if self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
status_information = {
|
||||||
self._send_action_needed_reason_email(email_content=self.action_needed_reason_email)
|
self.DomainRequestStatus.ACTION_NEEDED: {
|
||||||
|
"cached_reason": self._cached_action_needed_reason,
|
||||||
|
"reason": self.action_needed_reason,
|
||||||
|
"email": self.action_needed_reason_email,
|
||||||
|
"excluded_reasons": [DomainRequest.ActionNeededReasons.OTHER],
|
||||||
|
"wrap_email": True,
|
||||||
|
},
|
||||||
|
self.DomainRequestStatus.REJECTED: {
|
||||||
|
"cached_reason": self._cached_rejection_reason,
|
||||||
|
"reason": self.rejection_reason,
|
||||||
|
"email": self.rejection_reason_email,
|
||||||
|
"excluded_reasons": [],
|
||||||
|
# "excluded_reasons": [DomainRequest.RejectionReasons.OTHER],
|
||||||
|
"wrap_email": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
status_info = status_information.get(status)
|
||||||
|
|
||||||
|
# Don't send an email if there is nothing to send.
|
||||||
|
if status_info.get("email") is None:
|
||||||
|
logger.warning("send_custom_status_update_email() => Tried sending an empty email.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# We should never send an email if no reason was specified.
|
||||||
|
# Additionally, Don't send out emails for reasons that shouldn't send them.
|
||||||
|
if status_info.get("reason") is None or status_info.get("reason") in status_info.get("excluded_reasons"):
|
||||||
|
logger.warning("send_custom_status_update_email() => Tried sending a status email without a reason.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only send out an email if the underlying reason itself changed or if no email was sent previously.
|
||||||
|
if status_info.get("cached_reason") != status_info.get("reason") or status_info.get("cached_reason") is None:
|
||||||
|
bcc_address = settings.DEFAULT_FROM_EMAIL if settings.IS_PRODUCTION else ""
|
||||||
|
self._send_status_update_email(
|
||||||
|
new_status=status,
|
||||||
|
email_template="emails/includes/custom_email.txt",
|
||||||
|
email_template_subject="emails/status_change_subject.txt",
|
||||||
|
bcc_address=bcc_address,
|
||||||
|
custom_email_content=status_info.get("email"),
|
||||||
|
wrap_email=status_information.get("wrap_email"),
|
||||||
|
)
|
||||||
|
|
||||||
def sync_yes_no_form_fields(self):
|
def sync_yes_no_form_fields(self):
|
||||||
"""Some yes/no forms use a db field to track whether it was checked or not.
|
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||||
|
@ -901,7 +947,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
target=DomainRequestStatus.ACTION_NEEDED,
|
target=DomainRequestStatus.ACTION_NEEDED,
|
||||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||||
)
|
)
|
||||||
def action_needed(self, send_email=True):
|
def action_needed(self):
|
||||||
"""Send back an domain request that is under investigation or rejected.
|
"""Send back an domain request that is under investigation or rejected.
|
||||||
|
|
||||||
This action is logged.
|
This action is logged.
|
||||||
|
@ -909,43 +955,23 @@ class DomainRequest(TimeStampedModel):
|
||||||
This action cleans up the rejection status if moving away from rejected.
|
This action cleans up the rejection status if moving away from rejected.
|
||||||
|
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade) when they exist."""
|
(will cascade) when they exist.
|
||||||
|
|
||||||
|
Afterwards, we send out an email for action_needed in def save().
|
||||||
|
See the function send_custom_status_update_email.
|
||||||
|
"""
|
||||||
|
|
||||||
if self.status == self.DomainRequestStatus.APPROVED:
|
if self.status == self.DomainRequestStatus.APPROVED:
|
||||||
self.delete_and_clean_up_domain("reject_with_prejudice")
|
self.delete_and_clean_up_domain("action_needed")
|
||||||
elif self.status == self.DomainRequestStatus.REJECTED:
|
elif self.status == self.DomainRequestStatus.REJECTED:
|
||||||
self.rejection_reason = None
|
self.rejection_reason = None
|
||||||
|
|
||||||
|
# Check if the tuple is setup correctly, then grab its value.
|
||||||
|
|
||||||
literal = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
literal = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
||||||
# Check if the tuple is setup correctly, then grab its value
|
|
||||||
action_needed = literal if literal is not None else "Action Needed"
|
action_needed = literal if literal is not None else "Action Needed"
|
||||||
logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
|
logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
|
||||||
|
|
||||||
# Send out an email if an action needed reason exists
|
|
||||||
if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
|
||||||
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, email_content=None):
|
|
||||||
"""Sends out an automatic email for each valid action needed reason provided"""
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[
|
source=[
|
||||||
|
@ -1039,18 +1065,20 @@ class DomainRequest(TimeStampedModel):
|
||||||
def reject(self):
|
def reject(self):
|
||||||
"""Reject an domain request that has been submitted.
|
"""Reject an domain request that has been submitted.
|
||||||
|
|
||||||
|
This action is logged.
|
||||||
|
|
||||||
|
This action cleans up the action needed status if moving away from action needed.
|
||||||
|
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade), and send an email notification."""
|
(will cascade) when they exist.
|
||||||
|
|
||||||
|
Afterwards, we send out an email for reject in def save().
|
||||||
|
See the function send_custom_status_update_email.
|
||||||
|
"""
|
||||||
|
|
||||||
if self.status == self.DomainRequestStatus.APPROVED:
|
if self.status == self.DomainRequestStatus.APPROVED:
|
||||||
self.delete_and_clean_up_domain("reject")
|
self.delete_and_clean_up_domain("reject")
|
||||||
|
|
||||||
self._send_status_update_email(
|
|
||||||
"action needed",
|
|
||||||
"emails/status_change_rejected.txt",
|
|
||||||
"emails/status_change_rejected_subject.txt",
|
|
||||||
)
|
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[
|
source=[
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
<input id="has_audit_logs" class="display-none" value="{%if filtered_audit_log_entries %}true{% else %}false{% endif %}"/>
|
<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 %}
|
{% url 'get-action-needed-email-for-user-json' as url %}
|
||||||
<input id="get-action-needed-email-for-user-json" class="display-none" value="{{ url }}" />
|
<input id="get-action-needed-email-for-user-json" class="display-none" value="{{ url }}" />
|
||||||
|
{% url 'get-rejection-email-for-user-json' as url_2 %}
|
||||||
|
<input id="get-rejection-email-for-user-json" class="display-none" value="{{ url_2 }}" />
|
||||||
{% for fieldset in adminform %}
|
{% for fieldset in adminform %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
TODO: this will eventually need to be changed to something like this
|
TODO: this will eventually need to be changed to something like this
|
||||||
|
|
|
@ -137,29 +137,28 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
|
|
||||||
{% block field_other %}
|
{% block field_other %}
|
||||||
{% if field.field.name == "action_needed_reason_email" %}
|
{% if field.field.name == "action_needed_reason_email" %}
|
||||||
|
{{ field.field }}
|
||||||
|
|
||||||
<div class="margin-top-05 text-faded field-action_needed_reason_email__placeholder">
|
<div class="margin-top-05 text-faded custom-email-placeholder">
|
||||||
–
|
–
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ field.field }}
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="Edit email in textarea"
|
aria-label="Edit email in textarea"
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text margin-left-1 text-no-underline field-action_needed_reason_email__edit flex-align-self-start"
|
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text margin-left-1 text-no-underline flex-align-self-start edit-email-button"
|
||||||
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#email-already-sent-modal"
|
href="#action-needed-email-already-sent-modal"
|
||||||
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1 field-action_needed_reason_email__modal-trigger flex-align-self-start"
|
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1 edit-button-modal-trigger flex-align-self-start"
|
||||||
aria-controls="email-already-sent-modal"
|
aria-controls="action-needed-email-already-sent-modal"
|
||||||
data-open-modal
|
data-open-modal
|
||||||
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</a
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</a
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="usa-modal"
|
class="usa-modal"
|
||||||
id="email-already-sent-modal"
|
id="action-needed-email-already-sent-modal"
|
||||||
aria-labelledby="Are you sure you want to edit this email?"
|
aria-labelledby="Are you sure you want to edit this email?"
|
||||||
aria-describedby="The creator of this request already received an email"
|
aria-describedby="The creator of this request already received an email"
|
||||||
>
|
>
|
||||||
|
@ -187,8 +186,8 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
id="action-needed-reason__confirm-edit-email"
|
||||||
class="usa-button"
|
class="usa-button"
|
||||||
id="confirm-edit-email"
|
|
||||||
data-close-modal
|
data-close-modal
|
||||||
>
|
>
|
||||||
Yes, continue editing
|
Yes, continue editing
|
||||||
|
@ -221,11 +220,99 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if original_object.action_needed_reason_email %}
|
{% if original_object.action_needed_reason_email %}
|
||||||
<input id="last-sent-email-content" class="display-none" value="{{original_object.action_needed_reason_email}}">
|
<input id="last-sent-action-needed-email-content" class="display-none" value="{{original_object.action_needed_reason_email}}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<input id="last-sent-email-content" class="display-none" value="None">
|
<input id="last-sent-action-needed-email-content" class="display-none" value="None">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% elif field.field.name == "rejection_reason_email" %}
|
||||||
|
{{ field.field }}
|
||||||
|
|
||||||
|
<div class="margin-top-05 text-faded custom-email-placeholder">
|
||||||
|
–
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
aria-label="Edit email in textarea"
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text margin-left-1 text-no-underline flex-align-self-start edit-email-button"
|
||||||
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#rejection-reason-email-already-sent-modal"
|
||||||
|
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1 edit-button-modal-trigger flex-align-self-start"
|
||||||
|
aria-controls="rejection-reason-email-already-sent-modal"
|
||||||
|
data-open-modal
|
||||||
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</a
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="rejection-reason-email-already-sent-modal"
|
||||||
|
aria-labelledby="Are you sure you want to edit this email?"
|
||||||
|
aria-describedby="The creator of this request already received an email"
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading">
|
||||||
|
Are you sure you want to edit this email?
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p>
|
||||||
|
The creator of this request already received an email for this status/reason:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="font-body-sm">Status: <b>Rejected</b></li>
|
||||||
|
<li class="font-body-sm">Reason: <b>{{ original_object.get_rejection_reason_display }}</b></li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
If you edit this email's text, <b>the system will send another email</b> to
|
||||||
|
the creator after you “save” your changes. If you do not want to send another email, click “cancel” below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
id="rejection-reason__confirm-edit-email"
|
||||||
|
class="usa-button"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Yes, continue editing
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
name="_cancel_edit_email"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if original_object.rejection_reason_email %}
|
||||||
|
<input id="last-sent-rejection-email-content" class="display-none" value="{{original_object.rejection_reason_email}}">
|
||||||
|
{% else %}
|
||||||
|
<input id="last-sent-rejection-email-content" class="display-none" value="None">
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field.field }}
|
{{ field.field }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -8,8 +8,8 @@ REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Rejected
|
STATUS: Rejected
|
||||||
|
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
{% if domain_request.rejection_reason != 'other' %}
|
{% if reason != domain_request.RejectionReasons.DOMAIN_PURPOSE.OTHER %}
|
||||||
REJECTION REASON{% endif %}{% if domain_request.rejection_reason == 'purpose_not_met' %}
|
REJECTION REASON{% endif %}{% if reason == domain_request.RejectionReasons.DOMAIN_PURPOSE %}
|
||||||
Your domain request was rejected because the purpose you provided did not meet our
|
Your domain request was rejected because the purpose you provided did not meet our
|
||||||
requirements. You didn’t provide enough information about how you intend to use the
|
requirements. You didn’t provide enough information about how you intend to use the
|
||||||
domain.
|
domain.
|
||||||
|
@ -18,7 +18,7 @@ Learn more about:
|
||||||
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
||||||
- What you can and can’t do with .gov domains <https://get.gov/domains/requirements/>
|
- What you can and can’t do with .gov domains <https://get.gov/domains/requirements/>
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'requestor_not_eligible' %}
|
If you have questions or comments, reply to this email.{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.REQUESTOR_NOT_ELIGIBLE %}
|
||||||
Your domain request was rejected because we don’t believe you’re eligible to request a
|
Your domain request was rejected because we don’t believe you’re eligible to request a
|
||||||
.gov domain on behalf of {{ domain_request.organization_name }}. You must be a government employee, or be
|
.gov domain on behalf of {{ domain_request.organization_name }}. You must be a government employee, or be
|
||||||
working on behalf of a government organization, to request a .gov domain.
|
working on behalf of a government organization, to request a .gov domain.
|
||||||
|
@ -26,7 +26,7 @@ working on behalf of a government organization, to request a .gov domain.
|
||||||
|
|
||||||
DEMONSTRATE ELIGIBILITY
|
DEMONSTRATE ELIGIBILITY
|
||||||
If you can provide more information that demonstrates your eligibility, or you want to
|
If you can provide more information that demonstrates your eligibility, or you want to
|
||||||
discuss further, reply to this email.{% elif domain_request.rejection_reason == 'org_has_domain' %}
|
discuss further, reply to this email.{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.ORG_HAS_DOMAIN %}
|
||||||
Your domain request was rejected because {{ domain_request.organization_name }} has a .gov domain. Our
|
Your domain request was rejected because {{ domain_request.organization_name }} has a .gov domain. Our
|
||||||
practice is to approve one domain per online service per government organization. We
|
practice is to approve one domain per online service per government organization. We
|
||||||
evaluate additional requests on a case-by-case basis. You did not provide sufficient
|
evaluate additional requests on a case-by-case basis. You did not provide sufficient
|
||||||
|
@ -35,9 +35,9 @@ justification for an additional domain.
|
||||||
Read more about our practice of approving one domain per online service
|
Read more about our practice of approving one domain per online service
|
||||||
<https://get.gov/domains/before/#one-domain-per-service>.
|
<https://get.gov/domains/before/#one-domain-per-service>.
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'contacts_not_verified' %}
|
If you have questions or comments, reply to this email.{% elif reason == 'contacts_not_verified' %}
|
||||||
Your domain request was rejected because we could not verify the organizational
|
Your domain request was rejected because we could not verify the organizational
|
||||||
contacts you provided. If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'org_not_eligible' %}
|
contacts you provided. If you have questions or comments, reply to this email.{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.ORG_NOT_ELIGIBLE %}
|
||||||
Your domain request was rejected because we determined that {{ domain_request.organization_name }} is not
|
Your domain request was rejected because we determined that {{ domain_request.organization_name }} is not
|
||||||
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
|
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
|
||||||
government organizations.
|
government organizations.
|
||||||
|
@ -46,7 +46,7 @@ Learn more about eligibility for .gov domains
|
||||||
<https://get.gov/domains/eligibility/>.
|
<https://get.gov/domains/eligibility/>.
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.
|
If you have questions or comments, reply to this email.
|
||||||
{% elif domain_request.rejection_reason == 'naming_not_met' %}
|
{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.NAMING_REQUIREMENTS %}
|
||||||
Your domain request was rejected because it does not meet our naming requirements.
|
Your domain request was rejected because it does not meet our naming requirements.
|
||||||
Domains should uniquely identify a government organization and be clear to the
|
Domains should uniquely identify a government organization and be clear to the
|
||||||
general public. Learn more about naming requirements for your type of organization
|
general public. Learn more about naming requirements for your type of organization
|
||||||
|
@ -55,7 +55,7 @@ general public. Learn more about naming requirements for your type of organizati
|
||||||
|
|
||||||
YOU CAN SUBMIT A NEW REQUEST
|
YOU CAN SUBMIT A NEW REQUEST
|
||||||
We encourage you to request a domain that meets our requirements. If you have
|
We encourage you to request a domain that meets our requirements. If you have
|
||||||
questions or want to discuss potential domain names, reply to this email.{% elif domain_request.rejection_reason == 'other' %}
|
questions or want to discuss potential domain names, reply to this email.{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.OTHER %}
|
||||||
YOU CAN SUBMIT A NEW REQUEST
|
YOU CAN SUBMIT A NEW REQUEST
|
||||||
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
|
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
|
||||||
|
|
||||||
|
|
|
@ -595,7 +595,12 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def transition_state_and_send_email(
|
def transition_state_and_send_email(
|
||||||
self, domain_request, status, rejection_reason=None, action_needed_reason=None, action_needed_reason_email=None
|
self,
|
||||||
|
domain_request,
|
||||||
|
status,
|
||||||
|
rejection_reason=None,
|
||||||
|
action_needed_reason=None,
|
||||||
|
action_needed_reason_email=None,
|
||||||
):
|
):
|
||||||
"""Helper method for the email test cases."""
|
"""Helper method for the email test cases."""
|
||||||
|
|
||||||
|
@ -687,6 +692,10 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
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)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# We use javascript to reset the content of this. It is only automatically set
|
||||||
|
# if the email itself is somehow None.
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Test the email sent out for bad_name
|
# Test the email sent out for bad_name
|
||||||
bad_name = DomainRequest.ActionNeededReasons.BAD_NAME
|
bad_name = DomainRequest.ActionNeededReasons.BAD_NAME
|
||||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name)
|
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name)
|
||||||
|
@ -694,6 +703,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL
|
"DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Test the email sent out for eligibility_unclear
|
# Test the email sent out for eligibility_unclear
|
||||||
eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR
|
eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR
|
||||||
|
@ -702,6 +712,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL
|
"ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Test that a custom email is sent out for questionable_so
|
# Test that a custom email is sent out for questionable_so
|
||||||
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
||||||
|
@ -710,6 +721,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, _creator.email, bcc_email_address=BCC_EMAIL
|
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, _creator.email, bcc_email_address=BCC_EMAIL
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Assert that no other emails are sent on OTHER
|
# Assert that no other emails are sent on OTHER
|
||||||
other = DomainRequest.ActionNeededReasons.OTHER
|
other = DomainRequest.ActionNeededReasons.OTHER
|
||||||
|
@ -717,6 +729,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
|
|
||||||
# Should be unchanged from before
|
# Should be unchanged from before
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Tests if an analyst can override existing email content
|
# Tests if an analyst can override existing email content
|
||||||
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
||||||
|
@ -730,6 +743,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
domain_request.refresh_from_db()
|
domain_request.refresh_from_db()
|
||||||
self.assert_email_is_accurate("custom email content", 4, _creator.email, bcc_email_address=BCC_EMAIL)
|
self.assert_email_is_accurate("custom email content", 4, _creator.email, bcc_email_address=BCC_EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Tests if a new email gets sent when just the email is changed.
|
# 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.
|
# An email should NOT be sent out if we just modify the email content.
|
||||||
|
@ -741,6 +755,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Set the request back to in review
|
# Set the request back to in review
|
||||||
domain_request.in_review()
|
domain_request.in_review()
|
||||||
|
@ -757,55 +772,53 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
|
||||||
|
|
||||||
# def test_action_needed_sends_reason_email_prod_bcc(self):
|
def _reset_action_needed_email(self, domain_request):
|
||||||
# """When an action needed reason is set, an email is sent out and help@get.gov
|
"""Sets the given action needed email back to none"""
|
||||||
# is BCC'd in production"""
|
domain_request.action_needed_reason_email = None
|
||||||
# # Ensure there is no user with this email
|
domain_request.save()
|
||||||
# EMAIL = "mayor@igorville.gov"
|
domain_request.refresh_from_db()
|
||||||
# BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
|
||||||
# User.objects.filter(email=EMAIL).delete()
|
|
||||||
# in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
|
||||||
# action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
|
||||||
|
|
||||||
# # Create a sample domain request
|
@override_settings(IS_PRODUCTION=True)
|
||||||
# domain_request = completed_domain_request(status=in_review)
|
@less_console_noise_decorator
|
||||||
|
def test_rejected_sends_reason_email_prod_bcc(self):
|
||||||
|
"""When a rejection reason is set, an email is sent out and help@get.gov
|
||||||
|
is BCC'd in production"""
|
||||||
|
# Create fake creator
|
||||||
|
EMAIL = "meoward.jones@igorville.gov"
|
||||||
|
|
||||||
# # Test the email sent out for already_has_domains
|
_creator = User.objects.create(
|
||||||
# already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
username="MrMeoward",
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains)
|
first_name="Meoward",
|
||||||
# self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
last_name="Jones",
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
email=EMAIL,
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# # Test the email sent out for bad_name
|
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||||
# bad_name = DomainRequest.ActionNeededReasons.BAD_NAME
|
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name)
|
rejected = DomainRequest.DomainRequestStatus.REJECTED
|
||||||
# self.assert_email_is_accurate(
|
|
||||||
# "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL
|
|
||||||
# )
|
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
|
||||||
|
|
||||||
# # Test the email sent out for eligibility_unclear
|
# Create a sample domain request
|
||||||
# eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR
|
domain_request = completed_domain_request(status=in_review, user=_creator)
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=eligibility_unclear)
|
|
||||||
# self.assert_email_is_accurate(
|
|
||||||
# "ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL
|
|
||||||
# )
|
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
|
||||||
|
|
||||||
# # Test the email sent out for questionable_so
|
expected_emails = {
|
||||||
# questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
DomainRequest.RejectionReasons.DOMAIN_PURPOSE: "You didn’t provide enough information about how",
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so)
|
DomainRequest.RejectionReasons.REQUESTOR_NOT_ELIGIBLE: "You must be a government employee, or be",
|
||||||
# self.assert_email_is_accurate(
|
DomainRequest.RejectionReasons.ORG_HAS_DOMAIN: "practice is to approve one domain",
|
||||||
# "SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL
|
DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED: "we could not verify the organizational",
|
||||||
# )
|
DomainRequest.RejectionReasons.ORG_NOT_ELIGIBLE: ".Gov domains are only available to official U.S.-based",
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
DomainRequest.RejectionReasons.NAMING_REQUIREMENTS: "does not meet our naming requirements",
|
||||||
|
DomainRequest.RejectionReasons.OTHER: "YOU CAN SUBMIT A NEW REQUEST",
|
||||||
# # Assert that no other emails are sent on OTHER
|
}
|
||||||
# other = DomainRequest.ActionNeededReasons.OTHER
|
for i, (reason, email_content) in enumerate(expected_emails.items()):
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=other)
|
with self.subTest(reason=reason):
|
||||||
|
self.transition_state_and_send_email(domain_request, status=rejected, rejection_reason=reason)
|
||||||
# # Should be unchanged from before
|
self.assert_email_is_accurate(email_content, i, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), i + 1)
|
||||||
|
domain_request.rejection_reason_email = None
|
||||||
|
domain_request.save()
|
||||||
|
domain_request.refresh_from_db()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_save_model_sends_submitted_email(self):
|
def test_save_model_sends_submitted_email(self):
|
||||||
|
@ -1034,7 +1047,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
|
|
||||||
# Reject for reason REQUESTOR and test email including dynamic organization name
|
# Reject for reason REQUESTOR and test email including dynamic organization name
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
domain_request, DomainRequest.DomainRequestStatus.REJECTED, DomainRequest.RejectionReasons.REQUESTOR
|
domain_request,
|
||||||
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
|
DomainRequest.RejectionReasons.REQUESTOR_NOT_ELIGIBLE,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov "
|
"Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov "
|
||||||
|
@ -1072,7 +1087,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
domain_request,
|
domain_request,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.SECOND_DOMAIN_REASONING,
|
DomainRequest.RejectionReasons.ORG_HAS_DOMAIN,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because Testorg has a .gov domain.", 0, _creator.email
|
"Your domain request was rejected because Testorg has a .gov domain.", 0, _creator.email
|
||||||
|
@ -1108,7 +1123,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
domain_request,
|
domain_request,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY,
|
DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because we could not verify the organizational \n"
|
"Your domain request was rejected because we could not verify the organizational \n"
|
||||||
|
@ -1146,7 +1161,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
domain_request,
|
domain_request,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.ORGANIZATION_ELIGIBILITY,
|
DomainRequest.RejectionReasons.ORG_NOT_ELIGIBLE,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because we determined that Testorg is not \neligible for "
|
"Your domain request was rejected because we determined that Testorg is not \neligible for "
|
||||||
|
@ -1275,7 +1290,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
stack.enter_context(patch.object(messages, "error"))
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
stack.enter_context(patch.object(messages, "warning"))
|
stack.enter_context(patch.object(messages, "warning"))
|
||||||
domain_request.status = DomainRequest.DomainRequestStatus.REJECTED
|
domain_request.status = DomainRequest.DomainRequestStatus.REJECTED
|
||||||
domain_request.rejection_reason = DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY
|
domain_request.rejection_reason = DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED
|
||||||
|
|
||||||
self.admin.save_model(request, domain_request, None, True)
|
self.admin.save_model(request, domain_request, None, True)
|
||||||
|
|
||||||
|
@ -1621,6 +1636,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
|
"rejection_reason_email",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
"action_needed_reason_email",
|
"action_needed_reason_email",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
@ -1840,7 +1856,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.trigger_saving_approved_to_another_state(
|
self.trigger_saving_approved_to_another_state(
|
||||||
False,
|
False,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY,
|
DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_side_effects_when_saving_approved_to_ineligible(self):
|
def test_side_effects_when_saving_approved_to_ineligible(self):
|
||||||
|
|
|
@ -143,8 +143,8 @@ class GetActionNeededEmailForUserJsonTest(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.assertIn("action_needed_email", data)
|
self.assertIn("email", data)
|
||||||
self.assertIn("ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", data["action_needed_email"])
|
self.assertIn("ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", data["email"])
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_get_action_needed_email_for_user_json_analyst(self):
|
def test_get_action_needed_email_for_user_json_analyst(self):
|
||||||
|
@ -160,8 +160,8 @@ class GetActionNeededEmailForUserJsonTest(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.assertIn("action_needed_email", data)
|
self.assertIn("email", data)
|
||||||
self.assertIn("SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", data["action_needed_email"])
|
self.assertIn("SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", data["email"])
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_get_action_needed_email_for_user_json_regular(self):
|
def test_get_action_needed_email_for_user_json_regular(self):
|
||||||
|
@ -176,3 +176,71 @@ class GetActionNeededEmailForUserJsonTest(TestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
|
||||||
|
class GetRejectionEmailForUserJsonTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.analyst_user = create_user()
|
||||||
|
self.agency = FederalAgency.objects.create(agency="Test Agency")
|
||||||
|
self.domain_request = completed_domain_request(
|
||||||
|
federal_agency=self.agency,
|
||||||
|
name="test.gov",
|
||||||
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.api_url = reverse("get-rejection-email-for-user-json")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
FederalAgency.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_rejected_email_for_user_json_superuser(self):
|
||||||
|
"""Test that a superuser can fetch the action needed email."""
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
self.api_url,
|
||||||
|
{
|
||||||
|
"reason": DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
|
"domain_request_id": self.domain_request.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("email", data)
|
||||||
|
self.assertIn("we could not verify the organizational", data["email"])
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_rejected_email_for_user_json_analyst(self):
|
||||||
|
"""Test that an analyst can fetch the action needed email."""
|
||||||
|
self.client.force_login(self.analyst_user)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
self.api_url,
|
||||||
|
{
|
||||||
|
"reason": DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
|
"domain_request_id": self.domain_request.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("email", data)
|
||||||
|
self.assertIn("we could not verify the organizational", data["email"])
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_rejected_email_for_user_json_regular(self):
|
||||||
|
"""Test that a regular user receives a 403 with an error message."""
|
||||||
|
p = "password"
|
||||||
|
self.client.login(username="testuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
self.api_url,
|
||||||
|
{
|
||||||
|
"reason": DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
|
"domain_request_id": self.domain_request.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
|
@ -267,7 +267,6 @@ class TestDomainRequest(TestCase):
|
||||||
domain_request.submit()
|
domain_request.submit()
|
||||||
self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED)
|
self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def check_email_sent(
|
def check_email_sent(
|
||||||
self, domain_request, msg, action, expected_count, expected_content=None, expected_email="mayor@igorville.com"
|
self, domain_request, msg, action, expected_count, expected_content=None, expected_email="mayor@igorville.com"
|
||||||
):
|
):
|
||||||
|
@ -278,6 +277,7 @@ class TestDomainRequest(TestCase):
|
||||||
# Perform the specified action
|
# Perform the specified action
|
||||||
action_method = getattr(domain_request, action)
|
action_method = getattr(domain_request, action)
|
||||||
action_method()
|
action_method()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
# Check if an email was sent
|
# Check if an email was sent
|
||||||
sent_emails = [
|
sent_emails = [
|
||||||
|
@ -337,12 +337,30 @@ class TestDomainRequest(TestCase):
|
||||||
domain_request, msg, "withdraw", 1, expected_content="withdrawn", expected_email=user.email
|
domain_request, msg, "withdraw", 1, expected_content="withdrawn", expected_email=user.email
|
||||||
)
|
)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_reject_sends_email(self):
|
def test_reject_sends_email(self):
|
||||||
msg = "Create a domain request and reject it and see if email was sent."
|
"Create a domain request and reject it and see if email was sent."
|
||||||
user, _ = User.objects.get_or_create(username="testy")
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED, user=user)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED, user=user)
|
||||||
self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hi", expected_email=user.email)
|
expected_email = user.email
|
||||||
|
email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email)
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
domain_request.reject()
|
||||||
|
domain_request.rejection_reason = domain_request.RejectionReasons.CONTACTS_NOT_VERIFIED
|
||||||
|
domain_request.rejection_reason_email = "test"
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
# Check if an email was sent
|
||||||
|
sent_emails = [
|
||||||
|
email
|
||||||
|
for email in MockSESClient.EMAILS_SENT
|
||||||
|
if expected_email in email["kwargs"]["Destination"]["ToAddresses"]
|
||||||
|
]
|
||||||
|
self.assertEqual(len(sent_emails), 1)
|
||||||
|
|
||||||
|
email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
self.assertIn("test", email_content)
|
||||||
|
|
||||||
|
email_allowed.delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_reject_with_prejudice_does_not_send_email(self):
|
def test_reject_with_prejudice_does_not_send_email(self):
|
||||||
|
|
|
@ -6,36 +6,39 @@ from django.utils.html import escape
|
||||||
from registrar.models.utility.generic_helper import value_of_attribute
|
from registrar.models.utility.generic_helper import value_of_attribute
|
||||||
|
|
||||||
|
|
||||||
def get_all_action_needed_reason_emails(request, domain_request):
|
def get_action_needed_reason_default_email(domain_request, action_needed_reason):
|
||||||
"""Returns a dictionary of every action needed reason and its associated email
|
"""Returns the default email associated with the given action needed reason"""
|
||||||
for this particular domain request."""
|
return _get_default_email(
|
||||||
|
domain_request,
|
||||||
emails = {}
|
file_path=f"emails/action_needed_reasons/{action_needed_reason}.txt",
|
||||||
for action_needed_reason in domain_request.ActionNeededReasons:
|
reason=action_needed_reason,
|
||||||
# Map the action_needed_reason to its default email
|
excluded_reasons=[DomainRequest.ActionNeededReasons.OTHER],
|
||||||
emails[action_needed_reason.value] = get_action_needed_reason_default_email(
|
|
||||||
request, domain_request, action_needed_reason.value
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return emails
|
|
||||||
|
def get_rejection_reason_default_email(domain_request, rejection_reason):
|
||||||
|
"""Returns the default email associated with the given rejection reason"""
|
||||||
|
return _get_default_email(
|
||||||
|
domain_request,
|
||||||
|
file_path="emails/status_change_rejected.txt",
|
||||||
|
reason=rejection_reason,
|
||||||
|
# excluded_reasons=[DomainRequest.RejectionReasons.OTHER]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_action_needed_reason_default_email(request, domain_request, action_needed_reason):
|
def _get_default_email(domain_request, file_path, reason, excluded_reasons=None):
|
||||||
"""Returns the default email associated with the given action needed reason"""
|
if not reason:
|
||||||
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
|
return None
|
||||||
|
|
||||||
|
if excluded_reasons and reason in excluded_reasons:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
recipient = domain_request.creator
|
recipient = domain_request.creator
|
||||||
# Return the context of the rendered views
|
# Return the context of the rendered views
|
||||||
context = {"domain_request": domain_request, "recipient": recipient}
|
context = {"domain_request": domain_request, "recipient": recipient, "reason": reason}
|
||||||
|
|
||||||
# Get the email body
|
email_body_text = get_template(file_path).render(context=context)
|
||||||
template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt"
|
email_body_text_cleaned = email_body_text.strip().lstrip("\n") if email_body_text else None
|
||||||
|
|
||||||
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
|
return email_body_text_cleaned
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.forms.models import model_to_dict
|
||||||
from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
|
from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from registrar.utility.admin_helpers import get_all_action_needed_reason_emails
|
from registrar.utility.admin_helpers import get_action_needed_reason_default_email, get_rejection_reason_default_email
|
||||||
from registrar.models.portfolio import Portfolio
|
from registrar.models.portfolio import Portfolio
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
|
|
||||||
|
@ -88,5 +88,30 @@ def get_action_needed_email_for_user_json(request):
|
||||||
return JsonResponse({"error": "No domain_request_id specified"}, status=404)
|
return JsonResponse({"error": "No domain_request_id specified"}, status=404)
|
||||||
|
|
||||||
domain_request = DomainRequest.objects.filter(id=domain_request_id).first()
|
domain_request = DomainRequest.objects.filter(id=domain_request_id).first()
|
||||||
emails = get_all_action_needed_reason_emails(request, domain_request)
|
|
||||||
return JsonResponse({"action_needed_email": emails.get(reason)}, status=200)
|
email = get_action_needed_reason_default_email(domain_request, reason)
|
||||||
|
return JsonResponse({"email": email}, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@staff_member_required
|
||||||
|
def get_rejection_email_for_user_json(request):
|
||||||
|
"""Returns a default rejection email for a given user"""
|
||||||
|
|
||||||
|
# This API is only accessible to admins and analysts
|
||||||
|
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||||
|
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||||
|
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
||||||
|
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
||||||
|
|
||||||
|
reason = request.GET.get("reason")
|
||||||
|
domain_request_id = request.GET.get("domain_request_id")
|
||||||
|
if not reason:
|
||||||
|
return JsonResponse({"error": "No reason specified"}, status=404)
|
||||||
|
|
||||||
|
if not domain_request_id:
|
||||||
|
return JsonResponse({"error": "No domain_request_id specified"}, status=404)
|
||||||
|
|
||||||
|
domain_request = DomainRequest.objects.filter(id=domain_request_id).first()
|
||||||
|
email = get_rejection_reason_default_email(domain_request, reason)
|
||||||
|
return JsonResponse({"email": email}, status=200)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue