Initial architecture for rejection reason

This commit is contained in:
zandercymatics 2024-09-27 11:34:45 -06:00
parent 48b9206ffc
commit 9b23262d61
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
15 changed files with 306 additions and 82 deletions

View file

@ -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()

View file

@ -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");
}
} }

View file

@ -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/",

View file

@ -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",

View file

@ -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

View file

@ -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">
&ndash;
</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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 didnt provide enough information about how you intend to use the requirements. You didnt 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 %}

View file

@ -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 dont believe youre eligible to request a Your domain request was rejected because we dont believe youre 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 %}

View file

@ -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:

View file

@ -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)