From 7ddf1d87bc7f2c4da8033e767fc79865ec8bf191 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:55:40 -0600 Subject: [PATCH 01/70] changes --- src/docker-compose.yml | 2 +- src/registrar/admin.py | 10 +++--- src/registrar/models/domain_request.py | 31 ++++++++++++++----- .../action_needed_reasons/custom_email.txt | 2 ++ 4 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 src/registrar/templates/emails/action_needed_reasons/custom_email.txt diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 1a9064ac8..39282ff96 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -67,8 +67,8 @@ services: # command: "python" command: > bash -c " python manage.py migrate && - python manage.py load && python manage.py createcachetable && + python manage.py load && python manage.py runserver 0.0.0.0:8080" db: diff --git a/src/registrar/admin.py b/src/registrar/admin.py index eee2eda2f..44dedd25d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1690,8 +1690,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return super().save_model(request, obj, form, change) # == Handle non-status changes == # - # Change this in #1901. Add a check on "not self.action_needed_reason_email" - if obj.action_needed_reason: + if obj.action_needed_reason and not self.action_needed_reason_email: self._handle_action_needed_reason_email(obj) should_save = True @@ -1911,14 +1910,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Call the superclass method with updated extra_context return super().change_view(request, object_id, form_url, extra_context) + # TODO - scrap this approach and just centralize everything def get_all_action_needed_reason_emails_as_json(self, domain_request): """Returns a json dictionary of every action needed reason and its associated email for this particular domain request.""" emails = {} for action_needed_reason in domain_request.ActionNeededReasons: enum_value = action_needed_reason.value - # Change this in #1901. Just add a check for the current value. - emails[enum_value] = self._get_action_needed_reason_default_email_text(domain_request, enum_value) + if domain_request.action_needed_reason == enum_value and domain_request.action_needed_reason_email: + emails[enum_value] = domain_request.action_needed_reason_email + else: + emails[enum_value] = self._get_action_needed_reason_default_email_text(domain_request, enum_value) return json.dumps(emails) def _get_action_needed_reason_default_email_text(self, domain_request, action_needed_reason: str): diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index d26bb3284..febf0a5e6 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -672,7 +672,7 @@ class DomainRequest(TimeStampedModel): logger.error(f"Can't query an approved domain while attempting {called_from}") def _send_status_update_email( - self, new_status, email_template, email_template_subject, send_email=True, bcc_address="", wrap_email=False + self, new_status, email_template, email_template_subject, bcc_address="", context=None, **kwargs ): """Send a status update email to the creator. @@ -683,13 +683,21 @@ class DomainRequest(TimeStampedModel): If the waffle flag "profile_feature" is active, then this email will be sent to the domain request creator rather than the submitter + kwargs: send_email: bool -> Used to bypass the send_templated_email function, in the event we just want to log that an email would have been sent, rather than actually sending one. wrap_email: bool -> Wraps emails using `wrap_text_and_preserve_paragraphs` if any given paragraph exceeds our desired max length (for prettier display). + + custom_email_content: str -> Renders an email with the content of this string as its body text. """ + # Email config options + wrap_email = kwargs.get("wrap_email", False) + send_email = kwargs.get("send_email", True) + custom_email_content = kwargs.get("custom_email_content", None) + recipient = self.creator if flag_is_active(None, "profile_feature") else self.submitter if recipient is None or recipient.email is None: logger.warning( @@ -705,15 +713,21 @@ class DomainRequest(TimeStampedModel): return None try: + if not context: + context = { + "domain_request": self, + # This is the user that we refer to in the email + "recipient": recipient, + } + + if custom_email_content: + context["custom_email_content"] = custom_email_content + send_templated_email( email_template, email_template_subject, recipient.email, - context={ - "domain_request": self, - # This is the user that we refer to in the email - "recipient": recipient, - }, + context=context, bcc_address=bcc_address, wrap_email=wrap_email, ) @@ -844,12 +858,12 @@ class DomainRequest(TimeStampedModel): if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER: self._send_action_needed_reason_email(send_email) - def _send_action_needed_reason_email(self, send_email=True): + def _send_action_needed_reason_email(self, send_email=True, custom_email_content=None): """Sends out an automatic email for each valid action needed reason provided""" # Store the filenames of the template and template subject email_template_name: str = "" - email_template_subject_name: str = "" + email_template_subject_name: str = "" if not custom_email_content else "custom_email" # Check for the "type" of action needed reason. can_send_email = True @@ -877,6 +891,7 @@ class DomainRequest(TimeStampedModel): email_template_subject=f"emails/action_needed_reasons/{email_template_subject_name}", send_email=send_email, bcc_address=bcc_address, + custom_email_content=custom_email_content, wrap_email=True, ) diff --git a/src/registrar/templates/emails/action_needed_reasons/custom_email.txt b/src/registrar/templates/emails/action_needed_reasons/custom_email.txt new file mode 100644 index 000000000..8e58c8f2e --- /dev/null +++ b/src/registrar/templates/emails/action_needed_reasons/custom_email.txt @@ -0,0 +1,2 @@ +{% comment %} {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} {% endcomment %} +{{ custom_email_content }} \ No newline at end of file From 1cbd9234bee5a49655330fec6ee0316c744f8b10 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 28 Jun 2024 15:24:22 -0600 Subject: [PATCH 02/70] basic logic --- src/registrar/admin.py | 14 +++++++------- src/registrar/assets/js/get-gov-admin.js | 3 ++- src/registrar/models/domain_request.py | 9 +++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index b2db130d9..1fed780e1 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1606,7 +1606,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "is_election_board", "federal_agency", "status_history", - "action_needed_reason_email", ) # Read only that we'll leverage for CISA Analysts @@ -1705,7 +1704,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return super().save_model(request, obj, form, change) # == Handle non-status changes == # - if obj.action_needed_reason and not self.action_needed_reason_email: + if obj.action_needed_reason and not obj.action_needed_reason_email: self._handle_action_needed_reason_email(obj) should_save = True @@ -1933,13 +1932,14 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): emails = {} for action_needed_reason in domain_request.ActionNeededReasons: enum_value = action_needed_reason.value + custom_text = None if domain_request.action_needed_reason == enum_value and domain_request.action_needed_reason_email: - emails[enum_value] = domain_request.action_needed_reason_email - else: - emails[enum_value] = self._get_action_needed_reason_default_email_text(domain_request, enum_value) + custom_text = domain_request.action_needed_reason_email + + emails[enum_value] = self._get_action_needed_reason_default_email_text(domain_request, enum_value, custom_text) return json.dumps(emails) - def _get_action_needed_reason_default_email_text(self, domain_request, action_needed_reason: str): + def _get_action_needed_reason_default_email_text(self, domain_request, action_needed_reason: str, custom_text=None): """Returns the default email associated with the given action needed reason""" if action_needed_reason is None or action_needed_reason == domain_request.ActionNeededReasons.OTHER: return { @@ -1961,7 +1961,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return { "subject_text": subject_template.render(context=context), - "email_body_text": template.render(context=context), + "email_body_text": template.render(context=context) if not custom_text else custom_text, } def process_log_entry(self, log_entry): diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 898c41c4b..6d753744f 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -525,7 +525,7 @@ function initializeWidgetOnList(list, parentId) { */ (function () { let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason"); - let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more"); + let actionNeededEmail = document.querySelector("#id_action_needed_reason_email"); if(actionNeededReasonDropdown && actionNeededEmail) { // Add a change listener to the action needed reason dropdown handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail); @@ -546,6 +546,7 @@ function initializeWidgetOnList(list, parentId) { let actionNeededEmails = JSON.parse(document.getElementById('action-needed-emails-data').textContent) let emailData = actionNeededEmails[reason]; if (emailData) { + // TODO: do we need a revert to default button? let emailBody = emailData.email_body_text if (emailBody) { actionNeededEmail.value = emailBody diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index febf0a5e6..830ad9480 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -785,8 +785,8 @@ class DomainRequest(TimeStampedModel): "submission confirmation", "emails/submission_confirmation.txt", "emails/submission_confirmation_subject.txt", - True, - bcc_address, + send_email=True, + bcc_address=bcc_address, ) @transition( @@ -856,7 +856,8 @@ class DomainRequest(TimeStampedModel): # Send out an email if an action needed reason exists if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER: - self._send_action_needed_reason_email(send_email) + custom_email_content = self.action_needed_reason_email + self._send_action_needed_reason_email(send_email, custom_email_content) def _send_action_needed_reason_email(self, send_email=True, custom_email_content=None): """Sends out an automatic email for each valid action needed reason provided""" @@ -951,7 +952,7 @@ class DomainRequest(TimeStampedModel): "domain request approved", "emails/status_change_approved.txt", "emails/status_change_approved_subject.txt", - send_email, + send_email=send_email, ) @transition( From b6a70e6d3c585968d3b3c4291fcfbc75ac03e1c9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:56:09 -0600 Subject: [PATCH 03/70] custom content logic --- src/registrar/admin.py | 6 +++++- src/registrar/models/domain_request.py | 11 +++++++---- .../emails/action_needed_reasons/custom_email.txt | 5 +++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 1fed780e1..e2cd6e17d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1948,7 +1948,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): } # Get the email body - template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt" + if not custom_text: + template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt" + else: + template_path = f"emails/action_needed_reasons/custom_email.txt" + template = get_template(template_path) # Get the email subject diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 830ad9480..62eb3f4f8 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -618,7 +618,8 @@ class DomainRequest(TimeStampedModel): 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._send_action_needed_reason_email(custom_email_content=_email_content) def sync_yes_no_form_fields(self): """Some yes/no forms use a db field to track whether it was checked or not. @@ -863,8 +864,8 @@ class DomainRequest(TimeStampedModel): """Sends out an automatic email for each valid action needed reason provided""" # Store the filenames of the template and template subject - email_template_name: str = "" - email_template_subject_name: str = "" if not custom_email_content else "custom_email" + email_template_name: str = "" if not custom_email_content else "custom_email.txt" + email_template_subject_name: str = "" # Check for the "type" of action needed reason. can_send_email = True @@ -876,8 +877,10 @@ class DomainRequest(TimeStampedModel): # Assumes that the template name matches the action needed reason if nothing is specified. # This is so you can override if you need, or have this taken care of for you. - if not email_template_name and not email_template_subject_name: + if not email_template_name: email_template_name = f"{self.action_needed_reason}.txt" + + if not email_template_subject_name: email_template_subject_name = f"{self.action_needed_reason}_subject.txt" bcc_address = "" diff --git a/src/registrar/templates/emails/action_needed_reasons/custom_email.txt b/src/registrar/templates/emails/action_needed_reasons/custom_email.txt index 8e58c8f2e..58e539816 100644 --- a/src/registrar/templates/emails/action_needed_reasons/custom_email.txt +++ b/src/registrar/templates/emails/action_needed_reasons/custom_email.txt @@ -1,2 +1,3 @@ -{% comment %} {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} {% endcomment %} -{{ custom_email_content }} \ No newline at end of file +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +{{ custom_email_content }} +{% endautoescape %} From 5815d6733c9db3f258879178ace83cb069e3860b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:28:58 -0600 Subject: [PATCH 04/70] hash --- src/registrar/models/domain_request.py | 29 +++++++++++++++++-- .../models/utility/generic_helper.py | 5 ++++ src/registrar/tests/test_admin.py | 11 +++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 62eb3f4f8..66c70f9a7 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1,7 +1,7 @@ from __future__ import annotations from typing import Union import logging - +from django.template.loader import get_template from django.apps import apps from django.conf import settings from django.db import models @@ -10,7 +10,7 @@ from django.utils import timezone from waffle import flag_is_active from registrar.models.domain import Domain from registrar.models.federal_agency import FederalAgency -from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper +from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper, convert_string_to_sha256_hash from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.utility.constants import BranchChoices @@ -864,9 +864,16 @@ class DomainRequest(TimeStampedModel): """Sends out an automatic email for each valid action needed reason provided""" # Store the filenames of the template and template subject - email_template_name: str = "" if not custom_email_content else "custom_email.txt" + email_template_name: str = "" email_template_subject_name: str = "" + # Check if the current email that we sent out is the same as our defaults. + # If these hashes differ, then that means that we're sending custom content. + default_email_hash = self._get_action_needed_reason_email_hash() + current_email_hash = convert_string_to_sha256_hash(self.action_needed_reason_email) + if default_email_hash != current_email_hash: + email_template_name = "custom_email.txt" + # Check for the "type" of action needed reason. can_send_email = True match self.action_needed_reason: @@ -899,6 +906,22 @@ class DomainRequest(TimeStampedModel): wrap_email=True, ) + # TODO - rework this + def _get_action_needed_reason_email_hash(self): + """Returns the default email associated with the given action needed reason""" + if self.action_needed_reason is None or self.action_needed_reason == self.ActionNeededReasons.OTHER: + return None + + # Get the email body + template_path = f"emails/action_needed_reasons/{self.action_needed_reason}.txt" + template = get_template(template_path) + + recipient = self.creator if flag_is_active(None, "profile_feature") else self.submitter + # Return the content of the rendered views + context = {"domain_request": self, "recipient": recipient} + body_text = template.render(context=context) + return convert_string_to_sha256_hash(body_text) + @transition( field="status", source=[ diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index f9d4303c4..f35e2f619 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -2,6 +2,7 @@ import time import logging +import hashlib from urllib.parse import urlparse, urlunparse, urlencode @@ -321,3 +322,7 @@ def convert_queryset_to_dict(queryset, is_model=True, key="id"): request_dict = {value[key]: value for value in queryset} return request_dict + + +def convert_string_to_sha256_hash(string_to_convert): + return hashlib.sha256(string_to_convert.encode('utf-8')).hexdigest() \ No newline at end of file diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 44a131d55..3dd0cdb6c 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1540,6 +1540,17 @@ class TestDomainRequestAdmin(MockEppLib): # Should be unchanged from before self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) + # Tests if an analyst can override existing email content + questionable_ao = DomainRequest.ActionNeededReasons.QUESTIONABLE_AUTHORIZING_OFFICIAL + domain_request.action_needed_reason_email = "custom email content" + domain_request.save() + self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_ao) + + self.assert_email_is_accurate( + "custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 5) + def test_save_model_sends_submitted_email(self): """When transitioning to submitted from started or withdrawn on a domain request, an email is sent out. From 34f6b03078a09499da281fbcce7b1d13414ae60a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:29:57 -0600 Subject: [PATCH 05/70] Update test_admin.py --- src/registrar/tests/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 11f9bf5fe..ca5e4a93c 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1480,10 +1480,10 @@ class TestDomainRequestAdmin(MockEppLib): self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) # Tests if an analyst can override existing email content - questionable_ao = DomainRequest.ActionNeededReasons.QUESTIONABLE_AUTHORIZING_OFFICIAL + questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL domain_request.action_needed_reason_email = "custom email content" domain_request.save() - self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_ao) + self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so) self.assert_email_is_accurate( "custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL From a8fa24411e9238f43626b32a1d644e8bdea1f2c8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:56:26 -0600 Subject: [PATCH 06/70] Email logic (needs cleanup) --- src/registrar/admin.py | 21 ++++++++++++------- src/registrar/models/domain_request.py | 16 +++++++------- .../models/utility/generic_helper.py | 4 +++- src/registrar/utility/email.py | 2 ++ 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index a7e078f46..9efbf1dae 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1703,12 +1703,21 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return super().save_model(request, obj, form, change) # == Handle non-status changes == # - if obj.action_needed_reason and not obj.action_needed_reason_email: - self._handle_action_needed_reason_email(obj) - should_save = True - # Get the original domain request from the database. original_obj = models.DomainRequest.objects.get(pk=obj.pk) + + if obj.action_needed_reason: + text = self._get_action_needed_reason_default_email_text(obj, obj.action_needed_reason) + body_text = text.get("email_body_text") + if body_text: + body_text.strip().lstrip("\n") + is_default_email = body_text == obj.action_needed_reason_email + reason_changed = obj.action_needed_reason != original_obj.action_needed_reason + if is_default_email and reason_changed: + obj.action_needed_reason_email = body_text + should_save = True + + if obj.status == original_obj.status: # If the status hasn't changed, let the base function take care of it return super().save_model(request, obj, form, change) @@ -1721,10 +1730,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if should_save: return super().save_model(request, obj, form, change) - def _handle_action_needed_reason_email(self, obj): - text = self._get_action_needed_reason_default_email_text(obj, obj.action_needed_reason) - obj.action_needed_reason_email = text.get("email_body_text") - def _handle_status_change(self, request, obj, original_obj): """ Checks for various conditions when a status change is triggered. diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 2c155a6d9..53c5765e9 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -619,7 +619,7 @@ class DomainRequest(TimeStampedModel): # We don't send emails out in state "other" if self.action_needed_reason != self.ActionNeededReasons.OTHER: _email_content = self.action_needed_reason_email - self._send_action_needed_reason_email(custom_email_content=_email_content) + self._send_action_needed_reason_email(email_content=_email_content) def sync_yes_no_form_fields(self): """Some yes/no forms use a db field to track whether it was checked or not. @@ -857,10 +857,10 @@ class DomainRequest(TimeStampedModel): # Send out an email if an action needed reason exists if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER: - custom_email_content = self.action_needed_reason_email - self._send_action_needed_reason_email(send_email, custom_email_content) + 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, custom_email_content=None): + def _send_action_needed_reason_email(self, send_email=True, email_content=None): """Sends out an automatic email for each valid action needed reason provided""" # Store the filenames of the template and template subject @@ -871,7 +871,8 @@ class DomainRequest(TimeStampedModel): # If these hashes differ, then that means that we're sending custom content. default_email_hash = self._get_action_needed_reason_email_hash() current_email_hash = convert_string_to_sha256_hash(self.action_needed_reason_email) - if default_email_hash != current_email_hash: + if self.action_needed_reason_email and default_email_hash != current_email_hash: + print(f"sending custom email for: {current_email_hash}") email_template_name = "custom_email.txt" # Check for the "type" of action needed reason. @@ -902,7 +903,7 @@ class DomainRequest(TimeStampedModel): email_template_subject=f"emails/action_needed_reasons/{email_template_subject_name}", send_email=send_email, bcc_address=bcc_address, - custom_email_content=custom_email_content, + custom_email_content=email_content, wrap_email=True, ) @@ -919,7 +920,8 @@ class DomainRequest(TimeStampedModel): recipient = self.creator if flag_is_active(None, "profile_feature") else self.submitter # Return the content of the rendered views context = {"domain_request": self, "recipient": recipient} - body_text = template.render(context=context) + body_text = template.render(context=context).strip().lstrip("\n") + print(f"body: {body_text}") return convert_string_to_sha256_hash(body_text) @transition( diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index f35e2f619..75c2492c4 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -324,5 +324,7 @@ def convert_queryset_to_dict(queryset, is_model=True, key="id"): return request_dict -def convert_string_to_sha256_hash(string_to_convert): +def convert_string_to_sha256_hash(string_to_convert: str): + if not string_to_convert: + return None return hashlib.sha256(string_to_convert.encode('utf-8')).hexdigest() \ No newline at end of file diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 6fdaa3e3d..3d49dab02 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -45,6 +45,8 @@ def send_templated_email( template = get_template(template_name) email_body = template.render(context=context) + if email_body: + email_body.strip().lstrip("\n") subject_template = get_template(subject_template_name) subject = subject_template.render(context=context) From 55a74f63dd09478eb027c525669ef61c150cf0ef Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 09:11:09 -0600 Subject: [PATCH 07/70] Cleanup --- src/registrar/models/domain_request.py | 18 +++++++----------- src/registrar/models/utility/generic_helper.py | 6 ------ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 53c5765e9..5ec72aad5 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -868,11 +868,9 @@ class DomainRequest(TimeStampedModel): email_template_subject_name: str = "" # Check if the current email that we sent out is the same as our defaults. - # If these hashes differ, then that means that we're sending custom content. - default_email_hash = self._get_action_needed_reason_email_hash() - current_email_hash = convert_string_to_sha256_hash(self.action_needed_reason_email) - if self.action_needed_reason_email and default_email_hash != current_email_hash: - print(f"sending custom email for: {current_email_hash}") + # If these differ, then that means that we're sending custom content. + default_email = self.get_default_action_needed_reason_email(self.action_needed_reason) + if self.action_needed_reason_email and self.action_needed_reason_email != default_email: email_template_name = "custom_email.txt" # Check for the "type" of action needed reason. @@ -907,22 +905,20 @@ class DomainRequest(TimeStampedModel): wrap_email=True, ) - # TODO - rework this - def _get_action_needed_reason_email_hash(self): + def get_default_action_needed_reason_email(self, action_needed_reason): """Returns the default email associated with the given action needed reason""" - if self.action_needed_reason is None or self.action_needed_reason == self.ActionNeededReasons.OTHER: + if action_needed_reason is None or action_needed_reason == self.ActionNeededReasons.OTHER: return None # Get the email body - template_path = f"emails/action_needed_reasons/{self.action_needed_reason}.txt" + template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt" template = get_template(template_path) recipient = self.creator if flag_is_active(None, "profile_feature") else self.submitter # Return the content of the rendered views context = {"domain_request": self, "recipient": recipient} body_text = template.render(context=context).strip().lstrip("\n") - print(f"body: {body_text}") - return convert_string_to_sha256_hash(body_text) + return body_text @transition( field="status", diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 75c2492c4..72dee8204 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -322,9 +322,3 @@ def convert_queryset_to_dict(queryset, is_model=True, key="id"): request_dict = {value[key]: value for value in queryset} return request_dict - - -def convert_string_to_sha256_hash(string_to_convert: str): - if not string_to_convert: - return None - return hashlib.sha256(string_to_convert.encode('utf-8')).hexdigest() \ No newline at end of file From 87c7a4dba282d46ea7d3ae7241f06a53d53be6b2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 09:19:33 -0600 Subject: [PATCH 08/70] Update admin.py --- src/registrar/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ae3a95740..a1ba6559d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1958,7 +1958,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Call the superclass method with updated extra_context return super().change_view(request, object_id, form_url, extra_context) - # TODO - scrap this approach and just centralize everything def get_all_action_needed_reason_emails_as_json(self, domain_request): """Returns a json dictionary of every action needed reason and its associated email for this particular domain request.""" From 4bdf2cbe8f27c0e3712b44e9577b937b5c58607c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 09:27:55 -0600 Subject: [PATCH 09/70] Fix imports --- src/registrar/assets/js/get-gov-admin.js | 1 - src/registrar/models/domain_request.py | 5 ++--- src/registrar/utility/email.py | 2 ++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 83c958566..2a01eb304 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -548,7 +548,6 @@ function initializeWidgetOnList(list, parentId) { let actionNeededEmails = JSON.parse(document.getElementById('action-needed-emails-data').textContent) let emailData = actionNeededEmails[reason]; if (emailData) { - // TODO: do we need a revert to default button? let emailBody = emailData.email_body_text if (emailBody) { actionNeededEmail.value = emailBody diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 7bb3a5a96..387840161 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -10,7 +10,7 @@ from django.utils import timezone from waffle import flag_is_active from registrar.models.domain import Domain from registrar.models.federal_agency import FederalAgency -from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper, convert_string_to_sha256_hash +from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.utility.constants import BranchChoices @@ -627,8 +627,7 @@ class DomainRequest(TimeStampedModel): 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: - _email_content = self.action_needed_reason_email - self._send_action_needed_reason_email(email_content=_email_content) + self._send_action_needed_reason_email(email_content=self.action_needed_reason_email) def sync_yes_no_form_fields(self): """Some yes/no forms use a db field to track whether it was checked or not. diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 3d49dab02..7f0b997fa 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -45,6 +45,8 @@ def send_templated_email( template = get_template(template_name) email_body = template.render(context=context) + + # Do cleanup on the email body. Mostly for emails with custom content. if email_body: email_body.strip().lstrip("\n") From f84989589762c4ff268742f78065b28406c64b8c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:09:45 -0600 Subject: [PATCH 10/70] Logic to update label --- src/registrar/assets/js/get-gov.js | 26 +++++++++++++++++++++ src/registrar/assets/sass/_theme/_base.scss | 2 +- src/registrar/forms/user_profile.py | 5 +++- src/registrar/models/contact.py | 7 ++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 7052d786f..6373f176f 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1834,6 +1834,32 @@ document.addEventListener('DOMContentLoaded', function() { // When the edit button is clicked, show the input field under it handleEditButtonClick(fieldName, button); + + let editableFormGroup = button.parentElement.parentElement.parentElement; + if (editableFormGroup){ + let readonlyField = editableFormGroup.querySelector(".input-with-edit-button__readonly-field") + let inputField = document.getElementById(`id_${fieldName}`).value; + if (!inputField) { + return; + } + + let inputFieldValue = inputField.value + if (readonlyField && inputFieldValue){ + if (fieldName == "full_name"){ + let firstName = document.querySelector(`#id_first_name`).value; + let middleName = document.querySelector(`#id_middle_name`).value; + let lastName = document.querySelector(`#id_last_name`).value; + if (firstName && middleName && lastName) { + let values = [firstName.value, middleName.value, lastName.value] + readonlyField.innerHTML = values.join(" "); + }else { + readonlyField.innerHTML = "Unknown"; + } + }else { + readonlyField.innerHTML = inputValue; + } + } + } } }); } diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index b2bad1edb..b7cedfe85 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -190,7 +190,7 @@ abbr[title] { svg.usa-icon { color: #{$dhs-red}; } - div.readonly-field { + div.input-with-edit-button__readonly-field-field { color: #{$dhs-red}; } } diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index 682e1a5df..60e5032c8 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -93,4 +93,7 @@ class FinishSetupProfileForm(UserProfileForm): self.fields["title"].label = "Title or role in your organization" # Define the "full_name" value - self.fields["full_name"].initial = self.instance.get_formatted_name() + full_name = None + if self.instance.first_name and self.instance.last_name: + full_name = self.instance.get_formatted_name(return_unknown_when_none=False) + self.fields["full_name"].initial = full_name diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index f94938dd1..b0d6f3ac3 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -102,10 +102,13 @@ class Contact(TimeStampedModel): return getattr(self, relation).count() > threshold return False - def get_formatted_name(self): + def get_formatted_name(self, return_unknown_when_none=True): """Returns the contact's name in Western order.""" names = [n for n in [self.first_name, self.middle_name, self.last_name] if n] - return " ".join(names) if names else "Unknown" + if names: + return " ".join(names) + else: + return "Unknown" if return_unknown_when_none else None def has_contact_info(self): return bool(self.title or self.email or self.phone) From 08a3ba53780f2f5dde7f62d050bfc242b3ff8262 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:11:48 -0600 Subject: [PATCH 11/70] css changes --- src/registrar/assets/sass/_theme/_base.scss | 2 +- src/registrar/templates/includes/readonly_input.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index b7cedfe85..5fb8ce86b 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -190,7 +190,7 @@ abbr[title] { svg.usa-icon { color: #{$dhs-red}; } - div.input-with-edit-button__readonly-field-field { + div.input-with-edit-button__readonly-field { color: #{$dhs-red}; } } diff --git a/src/registrar/templates/includes/readonly_input.html b/src/registrar/templates/includes/readonly_input.html index ebd5d788e..47db97f00 100644 --- a/src/registrar/templates/includes/readonly_input.html +++ b/src/registrar/templates/includes/readonly_input.html @@ -8,7 +8,7 @@ {%endif %} -