mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 18:39:21 +02:00
Initial architecture for rejection reason
This commit is contained in:
parent
48b9206ffc
commit
9b23262d61
15 changed files with 306 additions and 82 deletions
|
@ -1994,7 +1994,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
# Set the rejection_reason_email to the default if nothing exists.
|
# Set the rejection_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.
|
# Since this check occurs after save, if the user enters a value then we won't update.
|
||||||
|
|
||||||
default_email = get_rejection_reason_default_email(obj, obj.action_needed_reason)
|
default_email = get_rejection_reason_default_email(obj, obj.rejection_reason)
|
||||||
if obj.rejection_reason_email:
|
if obj.rejection_reason_email:
|
||||||
emails = get_all_rejection_reason_emails(obj)
|
emails = get_all_rejection_reason_emails(obj)
|
||||||
is_custom_email = obj.rejection_reason_email not in emails.values()
|
is_custom_email = obj.rejection_reason_email not in emails.values()
|
||||||
|
|
|
@ -348,7 +348,7 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
* status select and to show/hide the rejection reason
|
* status select and to show/hide the rejection reason
|
||||||
*/
|
*/
|
||||||
(function (){
|
(function (){
|
||||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason');
|
||||||
// This is the "action needed reason" field
|
// This is the "action needed reason" field
|
||||||
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
||||||
// This is the "Email" field
|
// This is the "Email" field
|
||||||
|
@ -501,7 +501,7 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
class CustomizableEmailBase {
|
class CustomizableEmailBase {
|
||||||
constructor(dropdown, textarea, textareaPlaceholder, directEditButton, modalTrigger, modalConfirm, formLabel, lastSentEmailContent, apiUrl) {
|
constructor(dropdown, textarea, textareaPlaceholder, directEditButton, modalTrigger, modalConfirm, formLabel, lastSentEmailContent, apiUrl, textAreaFormGroup, dropdownFormGroup) {
|
||||||
this.dropdown = dropdown;
|
this.dropdown = dropdown;
|
||||||
this.textarea = textarea;
|
this.textarea = textarea;
|
||||||
this.textareaPlaceholder = textareaPlaceholder;
|
this.textareaPlaceholder = textareaPlaceholder;
|
||||||
|
@ -512,6 +512,11 @@ class CustomizableEmailBase {
|
||||||
this.lastSentEmailContent = lastSentEmailContent;
|
this.lastSentEmailContent = lastSentEmailContent;
|
||||||
this.apiUrl = apiUrl;
|
this.apiUrl = apiUrl;
|
||||||
|
|
||||||
|
// These fields are hidden on pageload
|
||||||
|
this.textAreaFormGroup = textAreaFormGroup;
|
||||||
|
this.dropdownFormGroup = dropdownFormGroup;
|
||||||
|
this.statusSelect = document.getElementById("id_status");
|
||||||
|
|
||||||
this.domainRequestId = this.dropdown ? document.getElementById("domain_request_id").value : null
|
this.domainRequestId = this.dropdown ? document.getElementById("domain_request_id").value : null
|
||||||
this.initialDropdownValue = this.dropdown ? this.dropdown.value : null;
|
this.initialDropdownValue = this.dropdown ? this.dropdown.value : null;
|
||||||
this.initialEmailValue = this.textarea ? this.textarea.value : null;
|
this.initialEmailValue = this.textarea ? this.textarea.value : null;
|
||||||
|
@ -520,11 +525,47 @@ class CustomizableEmailBase {
|
||||||
if (lastSentEmailContent && textarea) {
|
if (lastSentEmailContent && textarea) {
|
||||||
this.isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
this.isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle showing/hiding the related fields on page load.
|
||||||
|
initializeFormGroups(statusToCheck, sessionVariableName) {
|
||||||
|
let isStatus = statusSelect.value == statusToCheck;
|
||||||
|
|
||||||
|
// Initial handling of these groups.
|
||||||
|
updateFormGroupVisibility(isStatus, isStatus);
|
||||||
|
|
||||||
|
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||||
|
this.statusSelect.addEventListener('change', () => {
|
||||||
|
// Show the action needed field if the status is what we expect.
|
||||||
|
// Then track if its shown or hidden in our session cache.
|
||||||
|
isStatus = statusSelect.value == statusToCheck;
|
||||||
|
updateFormGroupVisibility(isStatus, isStatus);
|
||||||
|
addOrRemoveSessionBoolean(sessionVariableName, add=isStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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(sessionVariableName) !== null;
|
||||||
|
updateFormGroupVisibility(showTextAreaFormGroup, isStatus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe({ type: "navigation" });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFormGroupVisibility(showTextAreaFormGroup, showdropDownFormGroup) {
|
||||||
|
showTextAreaFormGroup ? showElement(this.textAreaFormGroup) : hideElement(this.textAreaFormGroup);
|
||||||
|
showdropDownFormGroup ? showElement(this.dropdownFormGroup) : hideElement(this.dropdownFormGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDropdown(errorMessage) {
|
initializeDropdown(errorMessage) {
|
||||||
this.dropdown.addEventListener("change", () => {
|
this.dropdown.addEventListener("change", () => {
|
||||||
console.log(this.dropdown)
|
|
||||||
let reason = this.dropdown.value;
|
let reason = this.dropdown.value;
|
||||||
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
||||||
let searchParams = new URLSearchParams(
|
let searchParams = new URLSearchParams(
|
||||||
|
@ -542,7 +583,7 @@ class CustomizableEmailBase {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.error("Error in AJAX call: " + data.error);
|
console.error("Error in AJAX call: " + data.error);
|
||||||
}else {
|
}else {
|
||||||
this.textarea.value = data.action_needed_email;
|
this.textarea.value = data.email;
|
||||||
}
|
}
|
||||||
this.updateUserInterface(reason);
|
this.updateUserInterface(reason);
|
||||||
})
|
})
|
||||||
|
@ -578,11 +619,17 @@ class CustomizableEmailBase {
|
||||||
updateUserInterface(reason) {
|
updateUserInterface(reason) {
|
||||||
if (!reason) {
|
if (!reason) {
|
||||||
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
|
// 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.showPlaceholder("Email:", "Select an action needed reason to see email");
|
this.showPlaceholderNoReason();
|
||||||
} else if (reason === 'other') {
|
} else if (reason === 'other') {
|
||||||
// 'Other' selected, we will set the label to "Email", show the "No email will be sent" placeholder, hide the trigger, textarea, hide the help text
|
// '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.showPlaceholder("Email:", "No email will be sent");
|
this.showPlaceholderOtherReason();
|
||||||
} else {
|
} else {
|
||||||
|
this.showReadonlyTextarea();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function that makes overriding the readonly textarea easy
|
||||||
|
showReadonlyTextarea() {
|
||||||
// A triggering selection is selected, all hands on board:
|
// A triggering selection is selected, all hands on board:
|
||||||
this.textarea.setAttribute('readonly', true);
|
this.textarea.setAttribute('readonly', true);
|
||||||
showElement(this.textarea);
|
showElement(this.textarea);
|
||||||
|
@ -602,6 +649,15 @@ class CustomizableEmailBase {
|
||||||
this.formLabel.innerHTML = "Email:";
|
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) {
|
showPlaceholder(formLabelText, placeholderText) {
|
||||||
|
@ -629,6 +685,11 @@ class customActionNeededEmail extends CustomizableEmailBase {
|
||||||
|
|
||||||
let apiContainer = document.getElementById("get-action-needed-email-for-user-json")
|
let apiContainer = document.getElementById("get-action-needed-email-for-user-json")
|
||||||
const apiUrl = apiContainer ? apiContainer.value : null;
|
const apiUrl = apiContainer ? apiContainer.value : null;
|
||||||
|
|
||||||
|
// These fields are hidden on pageload
|
||||||
|
const textAreaFormGroup = document.querySelector('.field-action_needed_reason');
|
||||||
|
const dropdownFormGroup = document.querySelector('.field-action_needed_reason_email');
|
||||||
|
|
||||||
super(
|
super(
|
||||||
dropdown,
|
dropdown,
|
||||||
textarea,
|
textarea,
|
||||||
|
@ -638,16 +699,32 @@ class customActionNeededEmail extends CustomizableEmailBase {
|
||||||
modalConfirm,
|
modalConfirm,
|
||||||
formLabel,
|
formLabel,
|
||||||
lastSentEmailContent,
|
lastSentEmailContent,
|
||||||
apiUrl
|
apiUrl,
|
||||||
|
textAreaFormGroup,
|
||||||
|
dropdownFormGroup,
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadActionNeededEmail() {
|
loadActionNeededEmail() {
|
||||||
|
if (this.textAreaFormGroup && this.dropdownFormGroup) {
|
||||||
|
this.initializeFormGroups("action needed", "showActionNeededReason");
|
||||||
|
}
|
||||||
this.updateUserInterface(this.dropdown.value);
|
this.updateUserInterface(this.dropdown.value);
|
||||||
this.initializeDropdown("Error when attempting to grab action needed email: ")
|
this.initializeDropdown("Error when attempting to grab action needed email: ")
|
||||||
this.initializeModalConfirm()
|
this.initializeModalConfirm()
|
||||||
this.initializeDirectEditButton()
|
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.
|
/** An IIFE that hooks to the show/hide button underneath action needed reason.
|
||||||
|
@ -675,8 +752,13 @@ class customRejectedEmail extends CustomizableEmailBase {
|
||||||
const formLabel = document.querySelector('label[for="id_rejection_reason_email"]');
|
const formLabel = document.querySelector('label[for="id_rejection_reason_email"]');
|
||||||
const lastSentEmailContent = document.getElementById("last-sent-email-content");
|
const lastSentEmailContent = document.getElementById("last-sent-email-content");
|
||||||
|
|
||||||
let apiContainer = document.getElementById("get-rejection-reason-email-for-user-json")
|
let apiContainer = document.getElementById("get-rejection-email-for-user-json");
|
||||||
const apiUrl = apiContainer ? apiContainer.value : null;
|
const apiUrl = apiContainer ? apiContainer.value : null;
|
||||||
|
|
||||||
|
// These fields are hidden on pageload
|
||||||
|
const textAreaFormGroup = document.querySelector('.field-rejection_reason');
|
||||||
|
const dropdownFormGroup = document.querySelector('.field-rejection_reason_email');
|
||||||
|
|
||||||
super(
|
super(
|
||||||
dropdown,
|
dropdown,
|
||||||
textarea,
|
textarea,
|
||||||
|
@ -686,16 +768,31 @@ class customRejectedEmail extends CustomizableEmailBase {
|
||||||
modalConfirm,
|
modalConfirm,
|
||||||
formLabel,
|
formLabel,
|
||||||
lastSentEmailContent,
|
lastSentEmailContent,
|
||||||
apiUrl
|
apiUrl,
|
||||||
|
textAreaFormGroup,
|
||||||
|
dropdownFormGroup,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadRejectedEmail() {
|
loadRejectedEmail() {
|
||||||
|
if (this.textAreaFormGroup && this.dropdownFormGroup) {
|
||||||
|
this.initializeFormGroups("rejected", "showRejectionReason");
|
||||||
|
}
|
||||||
this.updateUserInterface(this.dropdown.value);
|
this.updateUserInterface(this.dropdown.value);
|
||||||
this.initializeDropdown("Error when attempting to grab rejected email: ")
|
this.initializeDropdown("Error when attempting to grab rejected email: ")
|
||||||
this.initializeModalConfirm()
|
this.initializeModalConfirm()
|
||||||
this.initializeDirectEditButton()
|
this.initializeDirectEditButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overrides the placeholder text when no reason is selected
|
||||||
|
showPlaceholderNoReason() {
|
||||||
|
this.showPlaceholder("Email:", "Select a rejection reason to see email");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the placeholder text when the reason other is selected
|
||||||
|
showPlaceholderOtherReason() {
|
||||||
|
this.showPlaceholder("Email:", "No email will be sent");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,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.domains_json import get_domains_json
|
from registrar.views.domains_json import get_domains_json
|
||||||
from registrar.views.utility import always_404
|
from registrar.views.utility import always_404
|
||||||
|
@ -159,6 +160,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/",
|
||||||
|
|
|
@ -640,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"""
|
||||||
|
@ -662,21 +663,42 @@ class DomainRequest(TimeStampedModel):
|
||||||
|
|
||||||
# Handle the action needed email.
|
# Handle the action needed email.
|
||||||
# An email is sent out when action_needed_reason is changed or added.
|
# 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:
|
if self.status == self.DomainRequestStatus.ACTION_NEEDED:
|
||||||
self.sync_action_needed_reason()
|
self.send_another_status_reason_email(
|
||||||
|
checked_status=self.DomainRequestStatus.ACTION_NEEDED,
|
||||||
|
old_reason=self._cached_action_needed_reason,
|
||||||
|
new_reason=self.action_needed_reason,
|
||||||
|
excluded_reasons=[DomainRequest.ActionNeededReasons.OTHER],
|
||||||
|
email_to_send=self.action_needed_reason_email
|
||||||
|
)
|
||||||
|
elif self.status == self.DomainRequestStatus.REJECTED:
|
||||||
|
self.send_another_status_reason_email(
|
||||||
|
checked_status=self.DomainRequestStatus.REJECTED,
|
||||||
|
old_reason=self._cached_rejection_reason,
|
||||||
|
new_reason=self.rejection_reason,
|
||||||
|
excluded_reasons=[DomainRequest.RejectionReasons.OTHER],
|
||||||
|
email_to_send=self.rejection_reason_email,
|
||||||
|
)
|
||||||
|
|
||||||
# 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_another_status_reason_email(self, checked_status, old_reason, new_reason, excluded_reasons, email_to_send):
|
||||||
"""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
|
# If the status itself changed, then we already sent out an email
|
||||||
if was_already_action_needed and reason_exists and reason_changed:
|
if self._cached_status != checked_status or old_reason is None:
|
||||||
# We don't send emails out in state "other"
|
return
|
||||||
if self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
|
||||||
self._send_action_needed_reason_email(email_content=self.action_needed_reason_email)
|
# 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 new_reason is None or self.action_needed_reason in excluded_reasons:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only send out an email if the underlying email itself changed
|
||||||
|
if old_reason != new_reason:
|
||||||
|
self._send_custom_status_update_email(email_content=email_to_send)
|
||||||
|
|
||||||
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.
|
||||||
|
@ -806,9 +828,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
def _send_custom_status_update_email(self, email_content):
|
def _send_custom_status_update_email(self, email_content):
|
||||||
"""Wrapper for `_send_status_update_email` that bcc's help@get.gov
|
"""Wrapper for `_send_status_update_email` that bcc's help@get.gov
|
||||||
and sends an email equivalent to the 'email_content' variable."""
|
and sends an email equivalent to the 'email_content' variable."""
|
||||||
if settings.IS_PRODUCTION:
|
bcc_address = settings.DEFAULT_FROM_EMAIL if settings.IS_PRODUCTION else ""
|
||||||
bcc_address = settings.DEFAULT_FROM_EMAIL
|
|
||||||
|
|
||||||
self._send_status_update_email(
|
self._send_status_update_email(
|
||||||
new_status="action needed",
|
new_status="action needed",
|
||||||
email_template=f"emails/includes/custom_email.txt",
|
email_template=f"emails/includes/custom_email.txt",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -226,6 +226,94 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
<input id="last-sent-email-content" class="display-none" value="None">
|
<input id="last-sent-email-content" class="display-none" value="None">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% elif field.field.name == "rejection_reason_email" %}
|
||||||
|
<div class="margin-top-05 text-faded field-rejection_reason_email__placeholder">
|
||||||
|
–
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ field.field }}
|
||||||
|
|
||||||
|
<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 field-rejection_reason_email__edit flex-align-self-start"
|
||||||
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#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-rejection_reason_email__modal-trigger flex-align-self-start"
|
||||||
|
aria-controls="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="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"
|
||||||
|
class="usa-button"
|
||||||
|
id="confirm-edit-email"
|
||||||
|
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_reason_email %}
|
||||||
|
<input id="last-sent-email-content" class="display-none" value="{{original_object.action_needed_reason_email}}">
|
||||||
|
{% else %}
|
||||||
|
<input id="last-sent-email-content" class="display-none" value="None">
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field.field }}
|
{{ field.field }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
{% include "emails/includes/status_change_rejected_header.html" %}
|
{% include "emails/includes/status_change_rejected_header.txt" %}
|
||||||
REJECTION REASON
|
REJECTION REASON
|
||||||
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.
|
contacts you provided. If you have questions or comments, reply to this email.
|
||||||
|
|
||||||
{% include "emails/includes/email_footer.html" %}
|
{% include "emails/includes/email_footer.txt" %}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
{% include "emails/includes/status_change_rejected_header.html" %}
|
{% include "emails/includes/status_change_rejected_header.txt" %}
|
||||||
REJECTION REASON
|
REJECTION REASON
|
||||||
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
|
||||||
|
@ -11,5 +11,5 @@ 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.
|
questions or want to discuss potential domain names, reply to this email.
|
||||||
|
|
||||||
{% include "emails/includes/email_footer.html" %}
|
{% include "emails/includes/email_footer.txt" %}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
{% include "emails/includes/status_change_rejected_header.html" %}
|
{% include "emails/includes/status_change_rejected_header.txt" %}
|
||||||
REJECTION REASON
|
REJECTION REASON
|
||||||
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
|
||||||
|
@ -11,5 +11,5 @@ Read more about our practice of approving one domain per online service
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.
|
If you have questions or comments, reply to this email.
|
||||||
|
|
||||||
{% include "emails/includes/email_footer.html" %}
|
{% include "emails/includes/email_footer.txt" %}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
{% include "emails/includes/status_change_rejected_header.html" %}
|
{% include "emails/includes/status_change_rejected_header.txt" %}
|
||||||
REJECTION REASON
|
REJECTION REASON
|
||||||
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
|
||||||
|
@ -10,5 +10,5 @@ Learn more about eligibility for .gov domains
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.
|
If you have questions or comments, reply to this email.
|
||||||
|
|
||||||
{% include "emails/includes/email_footer.html" %}
|
{% include "emails/includes/email_footer.txt" %}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
|
@ -1,15 +0,0 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
|
||||||
{% include "emails/includes/status_change_rejected_header.html" %}
|
|
||||||
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.
|
|
||||||
|
|
||||||
Learn more about:
|
|
||||||
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
|
||||||
- Choosing a .gov domain name <https://get.gov/domains/choosing>
|
|
||||||
|
|
||||||
|
|
||||||
NEED ASSISTANCE?
|
|
||||||
If you have questions about this domain request or need help choosing a new domain name, reply to this email.
|
|
||||||
|
|
||||||
{% include "emails/includes/email_footer.html" %}
|
|
||||||
{% endautoescape %}
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
{% include "emails/includes/status_change_rejected_header.html" %}
|
{% include "emails/includes/status_change_rejected_header.txt" %}
|
||||||
REJECTION REASON
|
REJECTION REASON
|
||||||
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
|
||||||
|
@ -11,5 +11,5 @@ Learn more about:
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.
|
If you have questions or comments, reply to this email.
|
||||||
|
|
||||||
{% include "emails/includes/email_footer.html" %}
|
{% include "emails/includes/email_footer.txt" %}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
{% include "emails/includes/status_change_rejected_header.html" %}
|
{% include "emails/includes/status_change_rejected_header.txt" %}
|
||||||
REJECTION REASON
|
REJECTION REASON
|
||||||
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
|
||||||
|
@ -10,5 +10,5 @@ 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.
|
discuss further, reply to this email.
|
||||||
|
|
||||||
{% include "emails/includes/email_footer.html" %}
|
{% include "emails/includes/email_footer.txt" %}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
|
@ -20,7 +20,7 @@ def get_action_needed_reason_default_email(domain_request, action_needed_reason)
|
||||||
"""Returns the default email associated with the given action needed reason"""
|
"""Returns the default email associated with the given action needed reason"""
|
||||||
return _get_default_email(
|
return _get_default_email(
|
||||||
domain_request,
|
domain_request,
|
||||||
path_root="emails/rejection_reasons",
|
path_root="emails/action_needed_reasons",
|
||||||
reason=action_needed_reason,
|
reason=action_needed_reason,
|
||||||
excluded_reasons=[DomainRequest.ActionNeededReasons.OTHER]
|
excluded_reasons=[DomainRequest.ActionNeededReasons.OTHER]
|
||||||
)
|
)
|
||||||
|
@ -40,12 +40,12 @@ def get_all_rejection_reason_emails(domain_request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_rejection_reason_default_email(domain_request, action_needed_reason):
|
def get_rejection_reason_default_email(domain_request, rejection_reason):
|
||||||
"""Returns the default email associated with the given rejection reason"""
|
"""Returns the default email associated with the given rejection reason"""
|
||||||
return _get_default_email(
|
return _get_default_email(
|
||||||
domain_request,
|
domain_request,
|
||||||
path_root="emails/rejection_reasons",
|
path_root="emails/rejection_reasons",
|
||||||
reason=action_needed_reason,
|
reason=rejection_reason,
|
||||||
excluded_reasons=[DomainRequest.RejectionReasons.OTHER]
|
excluded_reasons=[DomainRequest.RejectionReasons.OTHER]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ def _get_all_default_emails(reasons, path_root, excluded_reasons, domain_request
|
||||||
emails[reason.value] = _get_default_email(
|
emails[reason.value] = _get_default_email(
|
||||||
domain_request, path_root, reason, excluded_reasons
|
domain_request, path_root, reason, excluded_reasons
|
||||||
)
|
)
|
||||||
|
return emails
|
||||||
|
|
||||||
def _get_default_email(domain_request, path_root, reason, excluded_reasons=None):
|
def _get_default_email(domain_request, path_root, reason, excluded_reasons=None):
|
||||||
if not reason:
|
if not reason:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -90,5 +90,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(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