-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#2355: Custom rejection reason emails - [BACKUP] - MIGRATION #2869
Changes from 7 commits
61b1cee
48b9206
9b23262
1ce0724
e37a95e
c084ec6
ff5212f
7a5eda0
cfa1879
7cc5231
6bb907c
5a96855
47d226f
62156ac
162369f
0a7d4e4
06e4dae
ad43fab
997fc1b
db0d4a1
2766202
1424197
b30e017
02e18a3
5fc8d0d
ade6733
e3d2dc0
c76e27f
081c762
a4ffc75
fc8090d
7c69205
3ba4293
bd603f6
ec1df25
dbfc548
3e54bb1
3fb9ab5
8b70554
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Generated by Django 4.2.10 on 2024-09-26 21:18 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("registrar", "0128_alter_domaininformation_state_territory_and_more"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="domainrequest", | ||
name="rejection_reason_email", | ||
field=models.TextField(blank=True, null=True), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -300,6 +300,11 @@ def get_action_needed_reason_label(cls, action_needed_reason: str): | |
blank=True, | ||
) | ||
|
||
rejection_reason_email = models.TextField( | ||
null=True, | ||
blank=True, | ||
) | ||
|
||
action_needed_reason = models.TextField( | ||
choices=ActionNeededReasons.choices, | ||
null=True, | ||
|
@@ -635,15 +640,16 @@ def sync_organization_type(self): | |
# Actually updates the organization_type field | ||
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""" | ||
self._cached_action_needed_reason = self.action_needed_reason | ||
self._cached_rejection_reason = self.rejection_reason | ||
self._cached_status = self.status | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
# 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): | ||
"""Save override for custom properties""" | ||
|
@@ -657,21 +663,42 @@ def save(self, *args, **kwargs): | |
|
||
# Handle the action needed email. | ||
# An email is sent out when action_needed_reason is changed or added. | ||
if self.action_needed_reason and self.status == self.DomainRequestStatus.ACTION_NEEDED: | ||
self.sync_action_needed_reason() | ||
if self.status == self.DomainRequestStatus.ACTION_NEEDED: | ||
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 | ||
self._cache_status_and_action_needed_reason() | ||
|
||
def sync_action_needed_reason(self): | ||
"""Checks if we need to send another action needed email""" | ||
was_already_action_needed = self._cached_status == self.DomainRequestStatus.ACTION_NEEDED | ||
reason_exists = self._cached_action_needed_reason is not None and self.action_needed_reason is not None | ||
reason_changed = self._cached_action_needed_reason != self.action_needed_reason | ||
if was_already_action_needed and reason_exists and reason_changed: | ||
# We don't send emails out in state "other" | ||
if self.action_needed_reason != self.ActionNeededReasons.OTHER: | ||
self._send_action_needed_reason_email(email_content=self.action_needed_reason_email) | ||
self._cache_status_and_status_reasons() | ||
|
||
def send_another_status_reason_email(self, checked_status, old_reason, new_reason, excluded_reasons, email_to_send): | ||
"""Helper function to send out a second status email when the status remains the same, | ||
but the reason has changed.""" | ||
|
||
# If the status itself changed, then we already sent out an email | ||
if self._cached_status != checked_status or old_reason is None: | ||
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 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): | ||
"""Some yes/no forms use a db field to track whether it was checked or not. | ||
|
@@ -798,6 +825,19 @@ def _send_status_update_email( | |
except EmailSendingError: | ||
logger.warning("Failed to send confirmation email", exc_info=True) | ||
|
||
def _send_custom_status_update_email(self, email_content): | ||
"""Wrapper for `_send_status_update_email` that bcc's [email protected] | ||
and sends an email equivalent to the 'email_content' variable.""" | ||
bcc_address = settings.DEFAULT_FROM_EMAIL if settings.IS_PRODUCTION else "" | ||
self._send_status_update_email( | ||
new_status="action needed", | ||
email_template=f"emails/includes/custom_email.txt", | ||
email_template_subject=f"emails/status_change_subject.txt", | ||
bcc_address=bcc_address, | ||
custom_email_content=email_content, | ||
wrap_email=True, | ||
) | ||
|
||
def investigator_exists_and_is_staff(self): | ||
"""Checks if the current investigator is in a valid state for a state transition""" | ||
is_valid = True | ||
|
@@ -901,7 +941,7 @@ def in_review(self): | |
target=DomainRequestStatus.ACTION_NEEDED, | ||
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. | ||
|
||
This action is logged. | ||
|
@@ -924,27 +964,7 @@ def action_needed(self, send_email=True): | |
# 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, | ||
) | ||
self._send_custom_status_update_email(email_content) | ||
|
||
@transition( | ||
field="status", | ||
|
@@ -1045,11 +1065,10 @@ def reject(self): | |
if self.status == self.DomainRequestStatus.APPROVED: | ||
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", | ||
) | ||
# Send out an email if a rejection reason exists | ||
if self.rejection_reason: | ||
email_content = self.rejection_reason_email | ||
self._send_custom_status_update_email(email_content) | ||
|
||
@transition( | ||
field="status", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -221,11 +221,99 @@ <h2 class="usa-modal__heading"> | |
</div> | ||
|
||
{% 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 %} | ||
<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 %} | ||
|
||
{% 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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since these buttons/ modals will never exist on the same page at the same time, I think there's an opportunity to significantly simplify the HTML and the JS by using generic handles (email-edit instead of field-rejection_reason_email__edit). On the JS: You can now trim down config by moving a lot of the handles to the parent class On the template: You can probably pull out the modals into an include that's a variant of modal.html, if not modal.html itself (that would be ideal) It's not insignificant and your code has nothing wrong with it, so I won' block if you decide not to do it, but we should capture this plan for the next custom email ticket if it exists. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, I think they do actually! Basically the blocks for detail_table_fieldset.html are nested inside in a for loop on each field present on the page. You can verify this via inspect element or by breaking the js somewhere to prevent hiding dom elements. However, I do really like your train of thought here. We could achieve what your describing by assigning those generic names as classes and having the js infer what the right values are given their expected dom position
You're totally right about that. I'll look at it and see if this is easily achievable (it might be) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay. I went through and simplified the javascript (though more can be done with the formgroups). Let me know what you think. For the templates + further refinement we can definitely do that in a follow-on like you said |
||
><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_email %} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NITPICK: Tabbing |
||
<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 %} | ||
{{ field.field }} | ||
{% endif %} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
THANK YOU | ||
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. | ||
|
||
---------------------------------------------------------------- | ||
|
||
The .gov team | ||
Contact us: <https://get.gov/contact/> | ||
Learn about .gov <https://get.gov> | ||
|
||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
Hi, {{ recipient.first_name }}. | ||
|
||
Your .gov domain request has been rejected. | ||
|
||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} | ||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} | ||
STATUS: Rejected | ||
---------------------------------------------------------------- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} | ||
{% include "emails/includes/status_change_rejected_header.txt" %} | ||
REJECTION REASON | ||
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. | ||
|
||
{% include "emails/includes/email_footer.txt" %} | ||
{% endautoescape %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NITPICK: Fix the tabbing