From 8b93216732946e1f327819d0aa1a9b1ee51a116e Mon Sep 17 00:00:00 2001 From: katypies Date: Tue, 18 Jun 2024 14:34:52 -0600 Subject: [PATCH 001/107] Add Github action to notify users by labels added to issues, and add Katherine for design-review label --- .github/workflows/issue-label-notifier.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/issue-label-notifier.yaml diff --git a/.github/workflows/issue-label-notifier.yaml b/.github/workflows/issue-label-notifier.yaml new file mode 100644 index 000000000..8e4a35d39 --- /dev/null +++ b/.github/workflows/issue-label-notifier.yaml @@ -0,0 +1,15 @@ +name: Notify users based on issue labels + +on: + issues: + types: [labeled] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - uses: jenschelkopf/issue-label-notification-action@1.3 + with: + recipients: | + design-review=@Katherine-Osos + message: 'cc/ {recipients} — adding you to this **{label}** issue!' \ No newline at end of file From f76e3619de92bf8ae946ed67b301ae024759b272 Mon Sep 17 00:00:00 2001 From: katypies Date: Thu, 20 Jun 2024 15:53:57 -0600 Subject: [PATCH 002/107] Add notification for when PRs are labeled, too --- .github/workflows/issue-label-notifier.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/issue-label-notifier.yaml b/.github/workflows/issue-label-notifier.yaml index 8e4a35d39..b5c9a93e6 100644 --- a/.github/workflows/issue-label-notifier.yaml +++ b/.github/workflows/issue-label-notifier.yaml @@ -3,6 +3,8 @@ name: Notify users based on issue labels on: issues: types: [labeled] + pull_request: + types: [labeled] jobs: notify: 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 003/107] 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 004/107] 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 005/107] 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 006/107] 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 007/107] 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 008/107] 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 009/107] 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 010/107] 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 011/107] 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 012/107] 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 013/107] 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 %} -
+
{% if field.name != "phone" %} {{ field.value }} {% else %} From a9c91bc94a79703b8eff9e4a976f027311278106 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:13:38 -0600 Subject: [PATCH 014/107] js bug --- src/registrar/assets/js/get-gov.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 6373f176f..73e5d1ee7 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1838,7 +1838,7 @@ document.addEventListener('DOMContentLoaded', function() { 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; + let inputField = document.getElementById(`id_${fieldName}`); if (!inputField) { return; } From 7a3f00971378824b140e32b610e26dbff2e6ae2a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:54:10 -0600 Subject: [PATCH 015/107] Edge cases --- src/registrar/assets/js/get-gov.js | 4 +--- src/registrar/forms/user_profile.py | 7 +++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 73e5d1ee7..34e22e49d 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1844,7 +1844,7 @@ document.addEventListener('DOMContentLoaded', function() { } let inputFieldValue = inputField.value - if (readonlyField && inputFieldValue){ + if (readonlyField && inputFieldValue || fieldName == "full_name"){ if (fieldName == "full_name"){ let firstName = document.querySelector(`#id_first_name`).value; let middleName = document.querySelector(`#id_middle_name`).value; @@ -1855,8 +1855,6 @@ document.addEventListener('DOMContentLoaded', function() { }else { readonlyField.innerHTML = "Unknown"; } - }else { - readonlyField.innerHTML = inputValue; } } } diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index 60e5032c8..02bc4e58f 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -71,7 +71,7 @@ class UserProfileForm(forms.ModelForm): class FinishSetupProfileForm(UserProfileForm): """Form for updating user profile.""" - full_name = forms.CharField(required=True, label="Full name") + full_name = forms.CharField(required=False, label="Full name") def clean(self): cleaned_data = super().clean() @@ -93,7 +93,10 @@ class FinishSetupProfileForm(UserProfileForm): self.fields["title"].label = "Title or role in your organization" # Define the "full_name" value - full_name = None + full_name = "" 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 + + # Set full_name as required for styling purposes + self.fields["full_name"].widget.attrs['required'] = 'required' From 2c6aa1bab1143c3965edd0aafd3040778334ae65 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:56:15 -0600 Subject: [PATCH 016/107] Update get-gov.js --- src/registrar/assets/js/get-gov.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 34e22e49d..30b80d356 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1844,7 +1844,7 @@ document.addEventListener('DOMContentLoaded', function() { } let inputFieldValue = inputField.value - if (readonlyField && inputFieldValue || fieldName == "full_name"){ + if (readonlyField && (inputFieldValue || fieldName == "full_name")){ if (fieldName == "full_name"){ let firstName = document.querySelector(`#id_first_name`).value; let middleName = document.querySelector(`#id_middle_name`).value; @@ -1855,6 +1855,8 @@ document.addEventListener('DOMContentLoaded', function() { }else { readonlyField.innerHTML = "Unknown"; } + + inputField.classList.add("text-base") } } } From 876041fe40a5bce462e287ce0af1f75d3c90436b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:18:20 -0600 Subject: [PATCH 017/107] Update get-gov.js --- src/registrar/assets/js/get-gov.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 30b80d356..90fb5711e 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1839,12 +1839,12 @@ document.addEventListener('DOMContentLoaded', function() { if (editableFormGroup){ let readonlyField = editableFormGroup.querySelector(".input-with-edit-button__readonly-field") let inputField = document.getElementById(`id_${fieldName}`); - if (!inputField) { + if (!inputField || !readonlyField) { return; } let inputFieldValue = inputField.value - if (readonlyField && (inputFieldValue || fieldName == "full_name")){ + if (inputFieldValue || fieldName == "full_name"){ if (fieldName == "full_name"){ let firstName = document.querySelector(`#id_first_name`).value; let middleName = document.querySelector(`#id_middle_name`).value; @@ -1857,6 +1857,8 @@ document.addEventListener('DOMContentLoaded', function() { } inputField.classList.add("text-base") + }else { + readonlyField.innerHTML = inputFieldValue } } } From 0edc8484735564e456f3008fb1e171e3ef36712e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:25:05 -0600 Subject: [PATCH 018/107] Cleanup --- src/registrar/assets/js/get-gov.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 90fb5711e..74f2715b2 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1846,17 +1846,23 @@ document.addEventListener('DOMContentLoaded', function() { let inputFieldValue = inputField.value if (inputFieldValue || fieldName == "full_name"){ 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 firstName = document.querySelector(`#id_first_name`); + let middleName = document.querySelector(`#id_middle_name`); + let lastName = document.querySelector(`#id_last_name`); + if (firstName && lastName) { let values = [firstName.value, middleName.value, lastName.value] + console.log(values) readonlyField.innerHTML = values.join(" "); }else { readonlyField.innerHTML = "Unknown"; } - - inputField.classList.add("text-base") + + // Technically, the full_name field is optional, but we want to display it as required. + // This style is applied to readonly fields (gray text). This just removes it, as + // this is difficult to achieve otherwise by modifying the .readonly property. + if (readonlyField.classList.contains("text-base")) { + readonlyField.classList.remove("text-base") + } }else { readonlyField.innerHTML = inputFieldValue } From ac4b657dbbb9da30a21d5ef7fa47b1df88a7ada0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:53:01 -0600 Subject: [PATCH 019/107] Add unit test and lint --- src/registrar/forms/user_profile.py | 2 +- src/registrar/tests/test_views.py | 84 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index 02bc4e58f..60e67886c 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -99,4 +99,4 @@ class FinishSetupProfileForm(UserProfileForm): self.fields["full_name"].initial = full_name # Set full_name as required for styling purposes - self.fields["full_name"].widget.attrs['required'] = 'required' + self.fields["full_name"].widget.attrs["required"] = "required" diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 61bc94a32..f8f4e1775 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -539,6 +539,49 @@ class FinishUserProfileTests(TestWithUser, WebTest): self._set_session_cookie() return page.follow() if follow else page + @less_console_noise_decorator + @override_flag("profile_feature", active=True) + def test_full_name_initial_value(self): + """Test that full_name initial value is empty when first_name or last_name is empty. + This will later be displayed as "unknown" using javascript.""" + self.app.set_user(self.incomplete_regular_user.username) + + # Test when first_name is empty + self.incomplete_regular_user.contact.first_name = "" + self.incomplete_regular_user.contact.last_name = "Doe" + self.incomplete_regular_user.contact.save() + + finish_setup_page = self.app.get(reverse("home")).follow() + form = finish_setup_page.form + self.assertEqual(form["full_name"].value, "") + + # Test when last_name is empty + self.incomplete_regular_user.contact.first_name = "John" + self.incomplete_regular_user.contact.last_name = "" + self.incomplete_regular_user.contact.save() + + finish_setup_page = self.app.get(reverse("home")).follow() + form = finish_setup_page.form + self.assertEqual(form["full_name"].value, "") + + # Test when both first_name and last_name are empty + self.incomplete_regular_user.contact.first_name = "" + self.incomplete_regular_user.contact.last_name = "" + self.incomplete_regular_user.contact.save() + + finish_setup_page = self.app.get(reverse("home")).follow() + form = finish_setup_page.form + self.assertEqual(form["full_name"].value, "") + + # Test when both first_name and last_name are present + self.incomplete_regular_user.contact.first_name = "John" + self.incomplete_regular_user.contact.last_name = "Doe" + self.incomplete_regular_user.contact.save() + + finish_setup_page = self.app.get(reverse("home")).follow() + form = finish_setup_page.form + self.assertEqual(form["full_name"].value, "John Doe") + @less_console_noise_decorator def test_new_user_with_profile_feature_on(self): """Tests that a new user is redirected to the profile setup page when profile_feature is on""" @@ -577,6 +620,47 @@ class FinishUserProfileTests(TestWithUser, WebTest): completed_setup_page = self.app.get(reverse("home")) self.assertContains(completed_setup_page, "Manage your domain") + @less_console_noise_decorator + def test_new_user_with_empty_name_profile_feature_on(self): + """Tests that a new user without a name can still enter this information accordingly""" + self.incomplete_regular_user.contact.first_name = None + self.incomplete_regular_user.contact.last_name = None + self.incomplete_regular_user.save() + self.app.set_user(self.incomplete_regular_user.username) + with override_flag("profile_feature", active=True): + # This will redirect the user to the setup page. + # Follow implicity checks if our redirect is working. + finish_setup_page = self.app.get(reverse("home")).follow() + self._set_session_cookie() + + # Assert that we're on the right page + self.assertContains(finish_setup_page, "Finish setting up your profile") + + finish_setup_page = self._submit_form_webtest(finish_setup_page.form) + + self.assertEqual(finish_setup_page.status_code, 200) + + # We're missing a phone number, so the page should tell us that + self.assertContains(finish_setup_page, "Enter your phone number.") + + # Check for the name of the save button + self.assertContains(finish_setup_page, "contact_setup_save_button") + + # Add a phone number + finish_setup_form = finish_setup_page.form + finish_setup_form["phone"] = "(201) 555-0123" + finish_setup_form["title"] = "CEO" + finish_setup_form["last_name"] = "example" + save_page = self._submit_form_webtest(finish_setup_form, follow=True) + + self.assertEqual(save_page.status_code, 200) + self.assertContains(save_page, "Your profile has been updated.") + + # Try to navigate back to the home page. + # This is the same as clicking the back button. + completed_setup_page = self.app.get(reverse("home")) + self.assertContains(completed_setup_page, "Manage your domain") + @less_console_noise_decorator def test_new_user_goes_to_domain_request_with_profile_feature_on(self): """Tests that a new user is redirected to the domain request page when profile_feature is on""" From a1fca00b8fa2163da37911b68e853424b3fd8799 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 5 Jul 2024 15:28:33 -0400 Subject: [PATCH 020/107] wip --- src/registrar/assets/js/get-gov.js | 4 + src/registrar/assets/sass/_theme/_base.scss | 84 +++++-------- src/registrar/assets/sass/_theme/_header.scss | 115 ++++++++++++++++++ src/registrar/assets/sass/_theme/styles.scss | 1 + src/registrar/config/settings.py | 4 + src/registrar/context_processors.py | 29 ++++- src/registrar/models/user.py | 10 ++ src/registrar/registrar_middleware.py | 20 +-- src/registrar/templates/base.html | 50 ++------ src/registrar/templates/dashboard_base.html | 1 + src/registrar/templates/home.html | 26 +--- .../includes/domain_requests_table.html | 3 + .../templates/includes/domains_table.html | 6 +- .../templates/includes/header_basic.html | 41 +++++++ .../templates/includes/header_extended.html | 74 +++++++++++ src/registrar/templates/portfolio.html | 24 ---- src/registrar/templates/portfolio_base.html | 41 +++++++ .../templates/portfolio_domains.html | 4 +- .../templates/portfolio_requests.html | 6 +- .../templates/portfolio_sidebar.html | 37 ------ src/registrar/views/domain.py | 7 -- src/registrar/views/domain_request.py | 23 +--- src/registrar/views/domains_json.py | 3 +- src/registrar/views/index.py | 2 - src/registrar/views/portfolios.py | 17 --- src/registrar/views/user_profile.py | 6 +- src/registrar/views/utility/error_views.py | 4 - 27 files changed, 392 insertions(+), 250 deletions(-) create mode 100644 src/registrar/assets/sass/_theme/_header.scss create mode 100644 src/registrar/templates/includes/header_basic.html create mode 100644 src/registrar/templates/includes/header_extended.html delete mode 100644 src/registrar/templates/portfolio.html create mode 100644 src/registrar/templates/portfolio_base.html delete mode 100644 src/registrar/templates/portfolio_sidebar.html diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 7052d786f..fe2946867 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1173,6 +1173,7 @@ document.addEventListener('DOMContentLoaded', function() { const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : ''; const expirationDateSortValue = expirationDate ? expirationDate.getTime() : ''; const actionUrl = domain.action_url; + const suborganization = domain.suborganization ? domain.suborganization : ''; const row = document.createElement('tr'); row.innerHTML = ` @@ -1195,6 +1196,9 @@ document.addEventListener('DOMContentLoaded', function() { + + ${suborganization} +
{% endblock %} - {% block banner %} -
- -
- {% endblock banner %} +
+ {% block header %} + {% if not is_org_user %} + {% include "includes/header_basic.html" %} + {% else %} + {% include "includes/header_extended.html" %} + {% endif %} + {% endblock header %} {% block wrapper %}
diff --git a/src/registrar/templates/dashboard_base.html b/src/registrar/templates/dashboard_base.html index 6dd2ce8fd..4f24b9280 100644 --- a/src/registrar/templates/dashboard_base.html +++ b/src/registrar/templates/dashboard_base.html @@ -9,6 +9,7 @@ {% block content %} {% block messages %} {% if messages %} +

test

    {% for message in messages %}
  • diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index a5ed4c86c..4518db271 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -4,17 +4,21 @@ {% block title %} Home | {% endblock %} +{% comment %} + home and portfolio_base test for is_authenticated +{% endcomment %} + {% block content %}
    {% if user.is_authenticated %} {# the entire logged in page goes here #} {% block homepage_content %} -
    {% block messages %} {% include "includes/form_messages.html" %} {% endblock %} +

    Manage your domains

    {% comment %} @@ -32,26 +36,8 @@ {% include "includes/domains_table.html" %} {% include "includes/domain_requests_table.html" %} - {# Note: Reimplement this after MVP #} - - - - - +
    {% endblock %} -
{% else %} {# not user.is_authenticated #} {# the entire logged out page goes here #} diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index 4f091ecf6..ad91699ef 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -12,6 +12,9 @@
{% if portfolio %}
- Filter by + Filter by
+
+ {% block usa_nav %} + + {% block usa_nav_secondary %}{% endblock %} + {% endblock %} +
+ \ No newline at end of file diff --git a/src/registrar/templates/includes/header_extended.html b/src/registrar/templates/includes/header_extended.html new file mode 100644 index 000000000..96a77afe5 --- /dev/null +++ b/src/registrar/templates/includes/header_extended.html @@ -0,0 +1,74 @@ +{% load static %} + +
+
+ {% block logo %} + {% include "includes/gov_extended_logo.html" with logo_clickable=True %} + {% endblock %} + +
+ {% block usa_nav %} + + {% endblock %} +
\ No newline at end of file diff --git a/src/registrar/templates/portfolio.html b/src/registrar/templates/portfolio.html deleted file mode 100644 index 4f37c0175..000000000 --- a/src/registrar/templates/portfolio.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends 'home.html' %} - -{% load static %} - -{% block homepage_content %} - -
-
-
- {% include "portfolio_sidebar.html" with portfolio=portfolio %} -
-
- {% block messages %} - {% include "includes/form_messages.html" %} - {% endblock %} - {# Note: Reimplement commented out functionality #} - - {% block portfolio_content %} - {% endblock %} - -
-
- -{% endblock %} diff --git a/src/registrar/templates/portfolio_base.html b/src/registrar/templates/portfolio_base.html new file mode 100644 index 000000000..4cb8145f8 --- /dev/null +++ b/src/registrar/templates/portfolio_base.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% comment %} + home and portfolio_base test for is_authenticated +{% endcomment %} + +{% block wrapper %} +
+ {% block content %} + +
+ {% if user.is_authenticated %} + {# the entire logged in page goes here #} + +
+ {% block messages %} + {% include "includes/form_messages.html" %} + {% endblock %} + + {% block portfolio_content %}{% endblock %} + +
+ {% else %} {# not user.is_authenticated #} + {# the entire logged out page goes here #} + +

+ Sign in +

+ + {% endif %} +
+ + {% endblock %} + +
{% block complementary %}{% endblock %}
+ + {% block content_bottom %}{% endblock %} +
+ + +{% endblock wrapper %} diff --git a/src/registrar/templates/portfolio_domains.html b/src/registrar/templates/portfolio_domains.html index 4b5e1148b..ede7886e6 100644 --- a/src/registrar/templates/portfolio_domains.html +++ b/src/registrar/templates/portfolio_domains.html @@ -1,7 +1,9 @@ -{% extends 'portfolio.html' %} +{% extends 'portfolio_base.html' %} {% load static %} +{% block title %} Domains | {% endblock %} + {% block portfolio_content %}

Domains

{% include "includes/domains_table.html" with portfolio=portfolio %} diff --git a/src/registrar/templates/portfolio_requests.html b/src/registrar/templates/portfolio_requests.html index 8d254a3e2..8c698ec83 100644 --- a/src/registrar/templates/portfolio_requests.html +++ b/src/registrar/templates/portfolio_requests.html @@ -1,7 +1,9 @@ -{% extends 'portfolio.html' %} +{% extends 'portfolio_base.html' %} {% load static %} +{% block title %} Domain requests | {% endblock %} + {% block portfolio_content %}

Domain requests

@@ -16,6 +18,6 @@ Start a new domain request

- + {% include "includes/domain_requests_table.html" with portfolio=portfolio %} {% endblock %} diff --git a/src/registrar/templates/portfolio_sidebar.html b/src/registrar/templates/portfolio_sidebar.html deleted file mode 100644 index d02f5ac0c..000000000 --- a/src/registrar/templates/portfolio_sidebar.html +++ /dev/null @@ -1,37 +0,0 @@ -{% load static url_helpers %} - -
- -
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 2414eba2c..6d30d9c77 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -102,13 +102,6 @@ class DomainBaseView(DomainPermissionView): domain_pk = "domain:" + str(self.kwargs.get("pk")) self.session[domain_pk] = self.object - def get_context_data(self, **kwargs): - """Extend get_context_data to add has_profile_feature_flag to context""" - context = super().get_context_data(**kwargs) - # This is a django waffle flag which toggles features based off of the "flag" table - context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") - return context - class DomainFormBaseView(DomainBaseView, FormMixin): """ diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index e8e82500e..40e7d7ed1 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -228,10 +228,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): if request.path_info == self.NEW_URL_NAME: # Clear context so the prop getter won't create a request here. # Creating a request will be handled in the post method for the - # intro page. Only TEMPORARY context needed is has_profile_flag - has_profile_flag = flag_is_active(self.request, "profile_feature") - context_stuff = {"has_profile_feature_flag": has_profile_flag} - return render(request, "domain_request_intro.html", context=context_stuff) + # intro page. + return render(request, "domain_request_intro.html", {}) else: return self.goto(self.steps.first) @@ -397,8 +395,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "modal_description": "Once you submit this request, you won’t be able to edit it until we review it.\ You’ll only be able to withdraw your request.", "review_form_is_complete": True, - # Use the profile waffle feature flag to toggle profile features throughout domain requests - "has_profile_feature_flag": has_profile_flag, "user": self.request.user, } else: # form is not complete @@ -414,7 +410,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "modal_description": 'This request cannot be submitted yet.\ Return to the request and visit the steps that are marked as "incomplete."', "review_form_is_complete": False, - "has_profile_feature_flag": has_profile_flag, "user": self.request.user, } return context_stuff @@ -740,13 +735,6 @@ class Finished(DomainRequestWizard): class DomainRequestStatus(DomainRequestPermissionView): template_name = "domain_request_status.html" - def get_context_data(self, **kwargs): - """Extend get_context_data to add has_profile_feature_flag to context""" - context = super().get_context_data(**kwargs) - # This is a django waffle flag which toggles features based off of the "flag" table - context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") - return context - class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView): """This page will ask user to confirm if they want to withdraw @@ -757,13 +745,6 @@ class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView): template_name = "domain_request_withdraw_confirmation.html" - def get_context_data(self, **kwargs): - """Extend get_context_data to add has_profile_feature_flag to context""" - context = super().get_context_data(**kwargs) - # This is a django waffle flag which toggles features based off of the "flag" table - context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") - return context - class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView): # this view renders no template diff --git a/src/registrar/views/domains_json.py b/src/registrar/views/domains_json.py index 8b107ca67..f17185ef2 100644 --- a/src/registrar/views/domains_json.py +++ b/src/registrar/views/domains_json.py @@ -11,7 +11,7 @@ def get_domains_json(request): """Given the current request, get all domains that are associated with the UserDomainRole object""" - user_domain_roles = UserDomainRole.objects.filter(user=request.user) + user_domain_roles = UserDomainRole.objects.filter(user=request.user).select_related('domain_info__sub_organization') domain_ids = user_domain_roles.values_list("domain_id", flat=True) objects = Domain.objects.filter(id__in=domain_ids) @@ -85,6 +85,7 @@ def get_domains_json(request): "action_url": reverse("domain", kwargs={"pk": domain.id}), "action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"), "svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"), + "suborganization": domain.domain_info.sub_organization.name if domain.domain_info and domain.domain_info.sub_organization else None, } for domain in page_obj.object_list ] diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index f975b9803..a2752168f 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -7,8 +7,6 @@ def index(request): context = {} if request.user.is_authenticated: - # This is a django waffle flag which toggles features based off of the "flag" table - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature") # This controls the creation of a new domain request in the wizard diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 5ecd5d1d0..1ec40580f 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -8,15 +8,6 @@ from django.contrib.auth.decorators import login_required def portfolio_domains(request, portfolio_id): context = {} - if request.user.is_authenticated: - # This is a django waffle flag which toggles features based off of the "flag" table - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") - context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature") - - # Retrieve the portfolio object based on the provided portfolio_id - portfolio = get_object_or_404(Portfolio, id=portfolio_id) - context["portfolio"] = portfolio - return render(request, "portfolio_domains.html", context) @@ -25,14 +16,6 @@ def portfolio_domain_requests(request, portfolio_id): context = {} if request.user.is_authenticated: - # This is a django waffle flag which toggles features based off of the "flag" table - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") - context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature") - - # Retrieve the portfolio object based on the provided portfolio_id - portfolio = get_object_or_404(Portfolio, id=portfolio_id) - context["portfolio"] = portfolio - # This controls the creation of a new domain request in the wizard request.session["new_request"] = True diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index 3f9aeb79f..e6909b3e9 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -55,10 +55,8 @@ class UserProfileView(UserProfilePermissionView, FormMixin): return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): - """Extend get_context_data to include has_profile_feature_flag""" + """Extend get_context_data""" context = super().get_context_data(**kwargs) - # This is a django waffle flag which toggles features based off of the "flag" table - context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") # Set the profile_back_button_text based on the redirect parameter if kwargs.get("redirect") == "domain-request:": @@ -139,7 +137,7 @@ class FinishProfileSetupView(UserProfileView): base_view_name = "finish-user-profile-setup" def get_context_data(self, **kwargs): - """Extend get_context_data to include has_profile_feature_flag""" + """Extend get_context_data""" context = super().get_context_data(**kwargs) # Show back button conditional on user having finished setup diff --git a/src/registrar/views/utility/error_views.py b/src/registrar/views/utility/error_views.py index 2374277d5..3e69d307d 100644 --- a/src/registrar/views/utility/error_views.py +++ b/src/registrar/views/utility/error_views.py @@ -14,14 +14,12 @@ Rather than dealing with that, we keep everything centralized in one location. """ from django.shortcuts import render -from waffle.decorators import flag_is_active def custom_500_error_view(request, context=None): """Used to redirect 500 errors to a custom view""" if context is None: context = {} - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") return render(request, "500.html", context=context, status=500) @@ -29,7 +27,6 @@ def custom_401_error_view(request, context=None): """Used to redirect 401 errors to a custom view""" if context is None: context = {} - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") return render(request, "401.html", context=context, status=401) @@ -37,5 +34,4 @@ def custom_403_error_view(request, exception=None, context=None): """Used to redirect 403 errors to a custom view""" if context is None: context = {} - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") return render(request, "403.html", context=context, status=403) From 619a71fdb601f1b4767977ac017b18fcf746580d Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 5 Jul 2024 18:08:34 -0400 Subject: [PATCH 021/107] extended header for org users --- src/registrar/assets/sass/_theme/_base.scss | 34 ------------------- .../assets/sass/_theme/_buttons.scss | 28 +++++++++++++++ src/registrar/assets/sass/_theme/_header.scss | 32 ++++++++--------- .../assets/sass/_theme/_identifier.scss | 10 ++++++ src/registrar/assets/sass/_theme/styles.scss | 1 + src/registrar/context_processors.py | 21 ++++++------ src/registrar/registrar_middleware.py | 2 +- .../templates/includes/header_basic.html | 2 +- src/registrar/views/domain.py | 2 +- src/registrar/views/domain_request.py | 1 - src/registrar/views/domains_json.py | 8 +++-- src/registrar/views/portfolios.py | 4 +-- src/registrar/views/user_profile.py | 2 +- 13 files changed, 77 insertions(+), 70 deletions(-) create mode 100644 src/registrar/assets/sass/_theme/_identifier.scss diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 2aa3da565..12d32d0fe 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -92,10 +92,6 @@ footer { color: color('primary'); } -.usa-identifier__logo { - height: units(7); -} - abbr[title] { // workaround for underlining abbr element border-bottom: none; @@ -135,36 +131,6 @@ abbr[title] { cursor: pointer; } -.input-with-edit-button { - svg.usa-icon { - width: 1.5em !important; - height: 1.5em !important; - color: #{$dhs-green}; - position: absolute; - } - &.input-with-edit-button__error { - svg.usa-icon { - color: #{$dhs-red}; - } - div.readonly-field { - color: #{$dhs-red}; - } - } -} - -// We need to deviate from some default USWDS styles here -// in this particular case, so we have to override this. -.usa-form .usa-button.readonly-edit-button { - margin-top: 0px !important; - padding-top: 0px !important; - svg { - width: 1.25em !important; - height: 1.25em !important; - } -} - - - .padding--8-8-9 { padding: 8px 8px 9px !important; } diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 92556556b..8ec43705f 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -162,6 +162,34 @@ a.usa-button--unstyled:visited { } } +.input-with-edit-button { + svg.usa-icon { + width: 1.5em !important; + height: 1.5em !important; + color: #{$dhs-green}; + position: absolute; + } + &.input-with-edit-button__error { + svg.usa-icon { + color: #{$dhs-red}; + } + div.readonly-field { + color: #{$dhs-red}; + } + } +} + +// We need to deviate from some default USWDS styles here +// in this particular case, so we have to override this. +.usa-form .usa-button.readonly-edit-button { + margin-top: 0px !important; + padding-top: 0px !important; + svg { + width: 1.25em !important; + height: 1.25em !important; + } +} + .usa-button--filter { width: auto; // For mobile stacking diff --git a/src/registrar/assets/sass/_theme/_header.scss b/src/registrar/assets/sass/_theme/_header.scss index a8c642804..6e12bff9e 100644 --- a/src/registrar/assets/sass/_theme/_header.scss +++ b/src/registrar/assets/sass/_theme/_header.scss @@ -67,30 +67,16 @@ } .usa-header--extended { - .usa-nav__primary { - .usa-nav-link, - .usa-nav-link:hover, - .usa-nav-link:active { - color: color('primary'); - font-weight: font-weight('normal'); - font-size: 16px; - } - .usa-current, - .usa-current:hover, - .usa-current:active { - font-weight: font-weight('bold'); - } - } @include at-media(desktop) { background-color: color('primary-darker'); border-top: solid 1px color('base'); - border-bottom: solid 1px color('base-lighter'); + border-bottom: solid 1px color('base-light'); .usa-logo__text a { color: color('white'); } .usa-nav { - background-color: color('primary-lightest'); + background-color: color('primary-lighter'); } .usa-nav__primary-item:last-child { margin-left: auto; @@ -98,6 +84,20 @@ padding-right: 0; } } + .usa-nav__primary { + .usa-nav-link, + .usa-nav-link:hover, + .usa-nav-link:active { + color: color('primary'); + font-weight: font-weight('normal'); + font-size: 16px; + } + .usa-current, + .usa-current:hover, + .usa-current:active { + font-weight: font-weight('bold'); + } + } .usa-nav__secondary { // I don't know why USWDS has this at 2 rem, which puts it out of alignment right: 3rem; diff --git a/src/registrar/assets/sass/_theme/_identifier.scss b/src/registrar/assets/sass/_theme/_identifier.scss new file mode 100644 index 000000000..e56d887b9 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_identifier.scss @@ -0,0 +1,10 @@ +@use "uswds-core" as *; + +.usa-banner { + background-color: color('primary-darker'); +} + +.usa-identifier__logo { + height: units(7); + } + \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index 142664423..4775b60c9 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -21,6 +21,7 @@ @forward "alerts"; @forward "tables"; @forward "sidenav"; +@forward "identifier"; @forward "header"; @forward "register-form"; diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 8072e85d4..acea72c4e 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -13,6 +13,7 @@ def language_code(request): """ return {"LANGUAGE_CODE": settings.LANGUAGE_CODE} + def canonical_path(request): """Add a canonical URL to the template context. @@ -22,6 +23,7 @@ def canonical_path(request): """ return {"CANONICAL_PATH": request.build_absolute_uri(request.path)} + def is_demo_site(request): """Add a boolean if this is a demo site. @@ -31,10 +33,12 @@ def is_demo_site(request): """ return {"IS_DEMO_SITE": settings.IS_DEMO_SITE} + def is_production(request): """Add a boolean if this is our production site.""" return {"IS_PRODUCTION": settings.IS_PRODUCTION} + def org_user_status(request): if request.user.is_authenticated: is_org_user = request.user.is_org_user(request) @@ -42,20 +46,17 @@ def org_user_status(request): is_org_user = False return { - 'is_org_user': is_org_user, + "is_org_user": is_org_user, } + def add_portfolio_to_context(request): - return { - 'portfolio': getattr(request, 'portfolio', None) - } + return {"portfolio": getattr(request, "portfolio", None)} + def add_path_to_context(request): - return { - 'path': getattr(request, 'path', None) - } + return {"path": getattr(request, "path", None)} + def add_has_profile_feature_flag_to_context(request): - return { - 'has_profile_feature_flag': flag_is_active(request, "profile_feature") - } + return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")} diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 5e534ea7e..80d1fe7a9 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -144,7 +144,7 @@ class CheckPortfolioMiddleware: if request.user.is_authenticated and request.user.is_org_user(request): user_portfolios = Portfolio.objects.filter(creator=request.user) first_portfolio = user_portfolios.first() - + if first_portfolio: # Add the portfolio to the request object request.portfolio = first_portfolio diff --git a/src/registrar/templates/includes/header_basic.html b/src/registrar/templates/includes/header_basic.html index 0fef664ac..692b2cb03 100644 --- a/src/registrar/templates/includes/header_basic.html +++ b/src/registrar/templates/includes/header_basic.html @@ -16,7 +16,7 @@
  • {% if user.is_authenticated %} - {{ user.email }} + {{ user.email }}
  • {% if has_profile_feature_flag %}
  • diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 6d30d9c77..1cfb259b0 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -59,7 +59,7 @@ from epplibwrapper import ( from ..utility.email import send_templated_email, EmailSendingError from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView -from waffle.decorators import flag_is_active, waffle_flag +from waffle.decorators import waffle_flag logger = logging.getLogger(__name__) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 40e7d7ed1..34e2bfc37 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -378,7 +378,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): def get_context_data(self): """Define context for access on all wizard pages.""" - has_profile_flag = flag_is_active(self.request, "profile_feature") context_stuff = {} if DomainRequest._form_complete(self.domain_request, self.request): diff --git a/src/registrar/views/domains_json.py b/src/registrar/views/domains_json.py index f17185ef2..f700f4396 100644 --- a/src/registrar/views/domains_json.py +++ b/src/registrar/views/domains_json.py @@ -11,7 +11,7 @@ def get_domains_json(request): """Given the current request, get all domains that are associated with the UserDomainRole object""" - user_domain_roles = UserDomainRole.objects.filter(user=request.user).select_related('domain_info__sub_organization') + user_domain_roles = UserDomainRole.objects.filter(user=request.user).select_related("domain_info__sub_organization") domain_ids = user_domain_roles.values_list("domain_id", flat=True) objects = Domain.objects.filter(id__in=domain_ids) @@ -85,7 +85,11 @@ def get_domains_json(request): "action_url": reverse("domain", kwargs={"pk": domain.id}), "action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"), "svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"), - "suborganization": domain.domain_info.sub_organization.name if domain.domain_info and domain.domain_info.sub_organization else None, + "suborganization": ( + domain.domain_info.sub_organization.name + if domain.domain_info and domain.domain_info.sub_organization + else None + ), } for domain in page_obj.object_list ] diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 1ec40580f..d5b6cbba5 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -1,6 +1,4 @@ -from django.shortcuts import get_object_or_404, render -from registrar.models.portfolio import Portfolio -from waffle.decorators import flag_is_active +from django.shortcuts import render from django.contrib.auth.decorators import login_required diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index e6909b3e9..0529e137b 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -15,7 +15,7 @@ from registrar.models import ( from registrar.models.user import User from registrar.models.utility.generic_helper import replace_url_queryparams from registrar.views.utility.permission_views import UserProfilePermissionView -from waffle.decorators import flag_is_active, waffle_flag +from waffle.decorators import waffle_flag logger = logging.getLogger(__name__) From a7c29f4d219ac6e57156e5d747236feef9c598ac Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 5 Jul 2024 18:13:46 -0400 Subject: [PATCH 022/107] clenup --- src/registrar/templates/includes/header_basic.html | 3 ++- src/registrar/templates/includes/header_extended.html | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/includes/header_basic.html b/src/registrar/templates/includes/header_basic.html index 692b2cb03..7076fcb89 100644 --- a/src/registrar/templates/includes/header_basic.html +++ b/src/registrar/templates/includes/header_basic.html @@ -38,4 +38,5 @@ {% block usa_nav_secondary %}{% endblock %} {% endblock %}
- \ No newline at end of file + + \ No newline at end of file diff --git a/src/registrar/templates/includes/header_extended.html b/src/registrar/templates/includes/header_extended.html index 96a77afe5..ccba93ea3 100644 --- a/src/registrar/templates/includes/header_extended.html +++ b/src/registrar/templates/includes/header_extended.html @@ -71,4 +71,5 @@
{% endblock %} - \ No newline at end of file + + \ No newline at end of file From b5d3df6d8579a326797573699fda2f4c91de9a4c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jul 2024 08:16:58 -0600 Subject: [PATCH 023/107] Update src/registrar/assets/js/get-gov.js --- src/registrar/assets/js/get-gov.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 74f2715b2..7bc19d0d5 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1851,7 +1851,6 @@ document.addEventListener('DOMContentLoaded', function() { let lastName = document.querySelector(`#id_last_name`); if (firstName && lastName) { let values = [firstName.value, middleName.value, lastName.value] - console.log(values) readonlyField.innerHTML = values.join(" "); }else { readonlyField.innerHTML = "Unknown"; From 0641e9ad7255e068a73e3079cfaf6b891b9b1146 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jul 2024 08:26:32 -0600 Subject: [PATCH 024/107] linting --- src/registrar/admin.py | 5 +++-- src/registrar/models/domain_request.py | 2 +- src/registrar/models/utility/generic_helper.py | 1 - src/registrar/tests/test_admin.py | 8 ++------ 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index a1ba6559d..850c385f0 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1746,7 +1746,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): 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) @@ -1968,7 +1967,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if domain_request.action_needed_reason == enum_value and domain_request.action_needed_reason_email: 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) + 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, custom_text=None): diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 387840161..632a16474 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -893,7 +893,7 @@ class DomainRequest(TimeStampedModel): # This is so you can override if you need, or have this taken care of for you. 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" diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 72dee8204..f9d4303c4 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -2,7 +2,6 @@ import time import logging -import hashlib from urllib.parse import urlparse, urlunparse, urlencode diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f9a107dc3..fecc733e3 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1485,9 +1485,7 @@ class TestDomainRequestAdmin(MockEppLib): domain_request.save() 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 - ) + 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): @@ -2234,6 +2232,7 @@ class TestDomainRequestAdmin(MockEppLib): self.assertContains(response, "Yes, select ineligible status") def test_readonly_when_restricted_creator(self): + self.maxDiff = None with less_console_noise(): domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): @@ -2252,7 +2251,6 @@ class TestDomainRequestAdmin(MockEppLib): "is_election_board", "federal_agency", "status_history", - "action_needed_reason_email", "id", "created_at", "updated_at", @@ -2314,7 +2312,6 @@ class TestDomainRequestAdmin(MockEppLib): "is_election_board", "federal_agency", "status_history", - "action_needed_reason_email", "creator", "about_your_organization", "requested_domain", @@ -2346,7 +2343,6 @@ class TestDomainRequestAdmin(MockEppLib): "is_election_board", "federal_agency", "status_history", - "action_needed_reason_email", ] self.assertEqual(readonly_fields, expected_fields) From 204b907a540586bcdab7e867546e228e70d58294 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jul 2024 08:30:47 -0600 Subject: [PATCH 025/107] lint --- src/registrar/admin.py | 2 +- src/registrar/tests/test_admin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 850c385f0..e50b262fb 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1984,7 +1984,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): 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_path = "emails/action_needed_reasons/custom_email.txt" template = get_template(template_path) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index fecc733e3..c100477a6 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1464,7 +1464,7 @@ class TestDomainRequestAdmin(MockEppLib): ) self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - # Test the email sent out for questionable_so + # Test that a custom email is sent out for questionable_so questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so) self.assert_email_is_accurate( From f18b10d669a8c2b3d9db94bd798dfb20aa9191a8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jul 2024 08:47:44 -0600 Subject: [PATCH 026/107] Add logic for if just the email is changed --- src/registrar/models/domain_request.py | 7 +++++-- src/registrar/tests/test_admin.py | 27 ++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 632a16474..5740d9504 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -598,6 +598,7 @@ class DomainRequest(TimeStampedModel): def _cache_status_and_action_needed_reason(self): """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_email = self.action_needed_reason_email self._cached_status = self.status def __init__(self, *args, **kwargs): @@ -614,7 +615,8 @@ class DomainRequest(TimeStampedModel): # Handle the action needed email. We send one when moving to action_needed, # but we don't send one when we are _already_ in the state and change the reason. - self.sync_action_needed_reason() + if self.status == self.DomainRequestStatus.ACTION_NEEDED and self.action_needed_reason: + self.sync_action_needed_reason() # Update the cached values after saving self._cache_status_and_action_needed_reason() @@ -624,7 +626,8 @@ class DomainRequest(TimeStampedModel): 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): + reason_email_changed = self._cached_action_needed_reason_email != self.action_needed_reason_email + if was_already_action_needed and (reason_exists and reason_changed or reason_email_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) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index c100477a6..22bdc8558 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1376,7 +1376,9 @@ class TestDomainRequestAdmin(MockEppLib): self.assertContains(response, "status in [submitted,in review,action needed]", count=1) @less_console_noise_decorator - def transition_state_and_send_email(self, domain_request, status, rejection_reason=None, action_needed_reason=None): + def transition_state_and_send_email( + self, domain_request, status, rejection_reason=None, action_needed_reason=None, action_needed_reason_email=None + ): """Helper method for the email test cases.""" with boto3_mocking.clients.handler_for("sesv2", self.mock_client): @@ -1392,6 +1394,9 @@ class TestDomainRequestAdmin(MockEppLib): if action_needed_reason: domain_request.action_needed_reason = action_needed_reason + if action_needed_reason_email: + domain_request.action_needed_reason_email = action_needed_reason_email + # Use the model admin's save_model method self.admin.save_model(request, domain_request, form=None, change=True) @@ -1481,13 +1486,27 @@ class TestDomainRequestAdmin(MockEppLib): # Tests if an analyst can override existing email content 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_so) + self.transition_state_and_send_email( + domain_request, + action_needed, + action_needed_reason=questionable_so, + action_needed_reason_email="custom email content", + ) self.assert_email_is_accurate("custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 5) + # Tests if a new email gets sent when just the email is changed + self.transition_state_and_send_email( + domain_request, + action_needed, + action_needed_reason=questionable_so, + action_needed_reason_email="dummy email content", + ) + + self.assert_email_is_accurate("dummy email content", 5, EMAIL, bcc_email_address=BCC_EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 6) + 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 173c0b51c0e69a9ca8f04e0becc0b042beec2f3a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:55:11 -0600 Subject: [PATCH 027/107] Update src/registrar/assets/js/get-gov.js --- src/registrar/assets/js/get-gov.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 7bc19d0d5..0d450b9e5 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1846,9 +1846,9 @@ document.addEventListener('DOMContentLoaded', function() { let inputFieldValue = inputField.value if (inputFieldValue || fieldName == "full_name"){ if (fieldName == "full_name"){ - let firstName = document.querySelector(`#id_first_name`); - let middleName = document.querySelector(`#id_middle_name`); - let lastName = document.querySelector(`#id_last_name`); + let firstName = document.querySelector("#id_first_name"); + let middleName = document.querySelector("#id_middle_name"); + let lastName = document.querySelector("#id_last_name"); if (firstName && lastName) { let values = [firstName.value, middleName.value, lastName.value] readonlyField.innerHTML = values.join(" "); From 425cfbcb4eae8b5b1e2a4ac2df5691a399ce9886 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:13:29 -0600 Subject: [PATCH 028/107] Logic refactor --- src/registrar/admin.py | 57 +++++++++++-------- src/registrar/assets/js/get-gov-admin.js | 6 ++ src/registrar/models/domain_request.py | 21 +------ .../admin/includes/detail_table_fieldset.html | 9 +++ 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e50b262fb..1561b7e1f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1735,15 +1735,16 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # 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) + if obj.action_needed_reason and obj.action_needed_reason != obj.ActionNeededReasons.OTHER: + text = self._get_action_needed_reason_default_email(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 + reason_changed = obj.action_needed_reason != original_obj.action_needed_reason + is_default_email = body_text == obj.action_needed_reason_email + if body_text and is_default_email and reason_changed: + obj.action_needed_reason_email = body_text + should_save = True + elif not obj.action_needed_reason and obj.action_needed_reason_email: + obj.action_needed_reason_email = None should_save = True if obj.status == original_obj.status: @@ -1967,12 +1968,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if domain_request.action_needed_reason == enum_value and domain_request.action_needed_reason_email: custom_text = domain_request.action_needed_reason_email - emails[enum_value] = self._get_action_needed_reason_default_email_text( + emails[enum_value] = self._get_action_needed_reason_default_email( 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, custom_text=None): + def _get_action_needed_reason_default_email(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 { @@ -1980,28 +1981,34 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "email_body_text": None, } - # Get the email body - if not custom_text: - template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt" - else: - template_path = "emails/action_needed_reasons/custom_email.txt" - - template = get_template(template_path) - - # Get the email subject - template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt" - subject_template = get_template(template_subject_path) - if flag_is_active(None, "profile_feature"): # type: ignore recipient = domain_request.creator else: recipient = domain_request.submitter - # Return the content of the rendered views + # Return the context of the rendered views context = {"domain_request": domain_request, "recipient": recipient} + + # Get the email subject + template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt" + subject_text = get_template(template_subject_path).render(context=context) + + # Get the email body + if not custom_text: + template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt" + else: + template_path = "emails/action_needed_reasons/custom_email.txt" + context["custom_email_content"] = custom_text + + email_body_text = get_template(template_path).render(context=context) + + email_body_text_cleaned = None + if email_body_text: + email_body_text_cleaned = email_body_text.strip().lstrip("\n") + return { - "subject_text": subject_template.render(context=context), - "email_body_text": template.render(context=context) if not custom_text else custom_text, + "subject_text": subject_text, + "email_body_text": email_body_text_cleaned, } 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 2a01eb304..3cf20311d 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -531,6 +531,12 @@ function initializeWidgetOnList(list, parentId) { if(actionNeededReasonDropdown && actionNeededEmail) { // Add a change listener to the action needed reason dropdown handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail); + + document.addEventListener('DOMContentLoaded', function() { + if (!actionNeededReasonDropdown.value || actionNeededReasonDropdown.value == "other") { + showNoEmailMessage(actionNeededEmail); + } + }); } function handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail) { diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 5740d9504..25138c54a 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -874,14 +874,10 @@ class DomainRequest(TimeStampedModel): 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 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 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: + if email_content is not None: email_template_name = "custom_email.txt" # Check for the "type" of action needed reason. @@ -916,21 +912,6 @@ class DomainRequest(TimeStampedModel): wrap_email=True, ) - def get_default_action_needed_reason_email(self, action_needed_reason): - """Returns the default email associated with the given action needed reason""" - 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/{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") - return body_text - @transition( field="status", source=[ diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index 8b8748f80..2aa5c0a60 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -143,6 +143,15 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% endwith %} {% endblock field_readonly %} +{% block field_other %} + {% if field.field.name == "action_needed_reason_email" %} + {{ field.field }} +

No email will be sent.

+ {% else %} + {{ field.field }} + {% endif %} +{% endblock field_other %} + {% block after_help_text %} {% if field.field.name == "action_needed_reason_email" %} {% comment %} From 91b9cb43795a61a6083b05a7f9a2ab220ee70466 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:29:36 -0600 Subject: [PATCH 029/107] Cleanup --- src/registrar/admin.py | 35 +++++++++++++++++--------- src/registrar/models/domain_request.py | 1 - 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 1561b7e1f..7d5708e62 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1735,17 +1735,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Get the original domain request from the database. original_obj = models.DomainRequest.objects.get(pk=obj.pk) + # If the reason is in a state where we can send out an email, + # set the email to a default one if a custom email isn't provided. if obj.action_needed_reason and obj.action_needed_reason != obj.ActionNeededReasons.OTHER: - text = self._get_action_needed_reason_default_email(obj, obj.action_needed_reason) - body_text = text.get("email_body_text") - reason_changed = obj.action_needed_reason != original_obj.action_needed_reason - is_default_email = body_text == obj.action_needed_reason_email - if body_text and is_default_email and reason_changed: - obj.action_needed_reason_email = body_text - should_save = True - elif not obj.action_needed_reason and obj.action_needed_reason_email: + obj = self._handle_existing_action_needed_reason_email(obj, original_obj) + else: obj.action_needed_reason_email = None - should_save = True if obj.status == original_obj.status: # If the status hasn't changed, let the base function take care of it @@ -1759,6 +1754,24 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if should_save: return super().save_model(request, obj, form, change) + def _handle_existing_action_needed_reason_email(self, obj, original_obj): + """ + Changes the action_needed_reason to the default email. + This occurs if the email changes and if it is empty. + """ + + # Get the default email + text = self._get_action_needed_reason_default_email(obj, obj.action_needed_reason) + body_text = text.get("email_body_text") + + # Set the action_needed_reason_email to the default + reason_changed = obj.action_needed_reason != original_obj.action_needed_reason + is_default_email = body_text == obj.action_needed_reason_email + if body_text and is_default_email and reason_changed: + obj.action_needed_reason_email = body_text + + return obj + def _handle_status_change(self, request, obj, original_obj): """ Checks for various conditions when a status change is triggered. @@ -1968,9 +1981,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if domain_request.action_needed_reason == enum_value and domain_request.action_needed_reason_email: custom_text = domain_request.action_needed_reason_email - emails[enum_value] = self._get_action_needed_reason_default_email( - domain_request, enum_value, custom_text - ) + emails[enum_value] = self._get_action_needed_reason_default_email(domain_request, enum_value, custom_text) return json.dumps(emails) def _get_action_needed_reason_default_email(self, domain_request, action_needed_reason: str, custom_text=None): diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 25138c54a..1d26e0d2f 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1,7 +1,6 @@ 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 From 72000b4a9b9b51e60d0156b66392b23bac905c06 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:44:52 -0600 Subject: [PATCH 030/107] Update email.py --- src/registrar/utility/email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 7f0b997fa..e274893a2 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -46,7 +46,7 @@ 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. + # Do cleanup on the email body. For emails with custom content. if email_body: email_body.strip().lstrip("\n") From dcb63d194733b94e0fa0e15840beb04c06e6a6e6 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 8 Jul 2024 18:57:46 -0400 Subject: [PATCH 031/107] Fix issues with org_user_status global context and logo_clickable profile context --- src/djangooidc/tests/test_views.py | 4 + .../assets/sass/_theme/_identifier.scss | 3 +- src/registrar/templates/base.html | 6 +- src/registrar/templates/dashboard_base.html | 1 - .../templates/finish_profile_setup.html | 4 +- .../templates/includes/header_basic.html | 73 ++++----- .../templates/includes/header_extended.html | 6 +- .../templates/includes/header_selector.html | 5 + src/registrar/templates/portfolio_base.html | 2 - src/registrar/templates/profile.html | 4 +- src/registrar/views/domains_json.py | 151 ++++++++++-------- 11 files changed, 132 insertions(+), 127 deletions(-) create mode 100644 src/registrar/templates/includes/header_selector.html diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index bdd61b346..6ebe25d45 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -429,6 +429,10 @@ class ViewsTest(TestCase): # Create a mock request request = self.factory.get("/some-url") request.session = {"acr_value": ""} + # Mock user and its attributes + mock_user = MagicMock() + mock_user.is_authenticated = True + request.user = mock_user # Ensure that the CLIENT instance used in login_callback is the mock # patch _requires_step_up_auth to return False with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch( diff --git a/src/registrar/assets/sass/_theme/_identifier.scss b/src/registrar/assets/sass/_theme/_identifier.scss index e56d887b9..58ccd659c 100644 --- a/src/registrar/assets/sass/_theme/_identifier.scss +++ b/src/registrar/assets/sass/_theme/_identifier.scss @@ -6,5 +6,4 @@ .usa-identifier__logo { height: units(7); - } - \ No newline at end of file +} diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index a7b2bb783..e5bd1b0b9 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -135,11 +135,7 @@
{% block header %} - {% if not is_org_user %} - {% include "includes/header_basic.html" %} - {% else %} - {% include "includes/header_extended.html" %} - {% endif %} + {% include "includes/header_selector.html" with logo_clickable=True %} {% endblock header %} {% block wrapper %} diff --git a/src/registrar/templates/dashboard_base.html b/src/registrar/templates/dashboard_base.html index 4f24b9280..6dd2ce8fd 100644 --- a/src/registrar/templates/dashboard_base.html +++ b/src/registrar/templates/dashboard_base.html @@ -9,7 +9,6 @@ {% block content %} {% block messages %} {% if messages %} -

test

    {% for message in messages %}
  • diff --git a/src/registrar/templates/finish_profile_setup.html b/src/registrar/templates/finish_profile_setup.html index 6e35ad5da..61cc192bf 100644 --- a/src/registrar/templates/finish_profile_setup.html +++ b/src/registrar/templates/finish_profile_setup.html @@ -4,8 +4,8 @@ {% block title %} Finish setting up your profile | {% endblock %} {# Disable the redirect #} -{% block logo %} - {% include "includes/gov_extended_logo.html" with logo_clickable=user_finished_setup %} +{% block header %} + {% include "includes/header_selector.html" with logo_clickable=user_finished_setup %} {% endblock %} {# Add the new form #} diff --git a/src/registrar/templates/includes/header_basic.html b/src/registrar/templates/includes/header_basic.html index 7076fcb89..fd3b71000 100644 --- a/src/registrar/templates/includes/header_basic.html +++ b/src/registrar/templates/includes/header_basic.html @@ -1,42 +1,39 @@ {% load static %}
    -
    -
    - {% block logo %} - {% include "includes/gov_extended_logo.html" with logo_clickable=True %} - {% endblock %} - -
    - {% block usa_nav %} - - {% block usa_nav_secondary %}{% endblock %} - {% endblock %} +
    +
    + {% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %} +
    -
    - \ No newline at end of file + {% block usa_nav %} + + {% block usa_nav_secondary %}{% endblock %} + {% endblock %} +
+ diff --git a/src/registrar/templates/includes/header_extended.html b/src/registrar/templates/includes/header_extended.html index ccba93ea3..f388a71bc 100644 --- a/src/registrar/templates/includes/header_extended.html +++ b/src/registrar/templates/includes/header_extended.html @@ -2,9 +2,7 @@
- {% block logo %} - {% include "includes/gov_extended_logo.html" with logo_clickable=True %} - {% endblock %} + {% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %}
{% block usa_nav %} @@ -71,5 +69,5 @@ {% endblock %} -
+ \ No newline at end of file diff --git a/src/registrar/templates/includes/header_selector.html b/src/registrar/templates/includes/header_selector.html new file mode 100644 index 000000000..3cb2bd51b --- /dev/null +++ b/src/registrar/templates/includes/header_selector.html @@ -0,0 +1,5 @@ +{% if not is_org_user %} + {% include "includes/header_basic.html" with logo_clickable=logo_clickable %} +{% else %} + {% include "includes/header_extended.html" with logo_clickable=logo_clickable %} +{% endif %} diff --git a/src/registrar/templates/portfolio_base.html b/src/registrar/templates/portfolio_base.html index 4cb8145f8..d4ba4dab2 100644 --- a/src/registrar/templates/portfolio_base.html +++ b/src/registrar/templates/portfolio_base.html @@ -36,6 +36,4 @@ {% block content_bottom %}{% endblock %} - - {% endblock wrapper %} diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index 41471fe88..6e1e7781f 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -6,8 +6,8 @@ Edit your User Profile | {% load static url_helpers %} {# Disable the redirect #} -{% block logo %} - {% include "includes/gov_extended_logo.html" with logo_clickable=user_finished_setup %} +{% block header %} + {% include "includes/header_selector.html" with logo_clickable=user_finished_setup %} {% endblock %} {% block content %} diff --git a/src/registrar/views/domains_json.py b/src/registrar/views/domains_json.py index f700f4396..59bc3e778 100644 --- a/src/registrar/views/domains_json.py +++ b/src/registrar/views/domains_json.py @@ -17,82 +17,15 @@ def get_domains_json(request): objects = Domain.objects.filter(id__in=domain_ids) unfiltered_total = objects.count() - # Handle sorting - sort_by = request.GET.get("sort_by", "id") # Default to 'id' - order = request.GET.get("order", "asc") # Default to 'asc' - - # Handle search term - search_term = request.GET.get("search_term") - if search_term: - objects = objects.filter(Q(name__icontains=search_term)) - - # Handle state - status_param = request.GET.get("status") - if status_param: - status_list = status_param.split(",") - - # if unknown is in status_list, append 'dns needed' since both - # unknown and dns needed display as DNS Needed, and both are - # searchable via state parameter of 'unknown' - if "unknown" in status_list: - status_list.append("dns needed") - - # Split the status list into normal states and custom states - normal_states = [state for state in status_list if state in Domain.State.values] - custom_states = [state for state in status_list if state == "expired"] - - # Construct Q objects for normal states that can be queried through ORM - state_query = Q() - if normal_states: - state_query |= Q(state__in=normal_states) - - # Handle custom states in Python, as expired can not be queried through ORM - if "expired" in custom_states: - expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"] - state_query |= Q(id__in=expired_domain_ids) - - # Apply the combined query - objects = objects.filter(state_query) - - # If there are filtered states, and expired is not one of them, domains with - # state_display of 'Expired' must be removed - if "expired" not in custom_states: - expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"] - objects = objects.exclude(id__in=expired_domain_ids) - - if sort_by == "state_display": - # Fetch the objects and sort them in Python - objects = list(objects) # Evaluate queryset to a list - objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc")) - else: - if order == "desc": - sort_by = f"-{sort_by}" - objects = objects.order_by(sort_by) + objects = apply_search(objects, request) + objects = apply_state_filter(objects, request) + objects = apply_sorting(objects, request) paginator = Paginator(objects, 10) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - # Convert objects to JSON-serializable format - domains = [ - { - "id": domain.id, - "name": domain.name, - "expiration_date": domain.expiration_date, - "state": domain.state, - "state_display": domain.state_display(), - "get_state_help_text": domain.get_state_help_text(), - "action_url": reverse("domain", kwargs={"pk": domain.id}), - "action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"), - "svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"), - "suborganization": ( - domain.domain_info.sub_organization.name - if domain.domain_info and domain.domain_info.sub_organization - else None - ), - } - for domain in page_obj.object_list - ] + domains = [serialize_domain(domain) for domain in page_obj.object_list] return JsonResponse( { @@ -105,3 +38,79 @@ def get_domains_json(request): "unfiltered_total": unfiltered_total, } ) + + +def apply_search(queryset, request): + search_term = request.GET.get("search_term") + if search_term: + queryset = queryset.filter(Q(name__icontains=search_term)) + return queryset + + +def apply_state_filter(queryset, request): + status_param = request.GET.get("status") + if status_param: + status_list = status_param.split(",") + # if unknown is in status_list, append 'dns needed' since both + # unknown and dns needed display as DNS Needed, and both are + # searchable via state parameter of 'unknown' + if "unknown" in status_list: + status_list.append("dns needed") + # Split the status list into normal states and custom states + normal_states = [state for state in status_list if state in Domain.State.values] + custom_states = [state for state in status_list if state == "expired"] + # Construct Q objects for normal states that can be queried through ORM + state_query = Q() + if normal_states: + state_query |= Q(state__in=normal_states) + # Handle custom states in Python, as expired can not be queried through ORM + if "expired" in custom_states: + expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] + state_query |= Q(id__in=expired_domain_ids) + # Apply the combined query + queryset = queryset.filter(state_query) + # If there are filtered states, and expired is not one of them, domains with + # state_display of 'Expired' must be removed + if "expired" not in custom_states: + expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] + queryset = queryset.exclude(id__in=expired_domain_ids) + + return queryset + + +def apply_sorting(queryset, request): + sort_by = request.GET.get("sort_by", "id") + order = request.GET.get("order", "asc") + if sort_by == "state_display": + objects = list(queryset) + objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc")) + return objects + else: + if order == "desc": + sort_by = f"-{sort_by}" + return queryset.order_by(sort_by) + + +def serialize_domain(domain): + suborganization_name = None + try: + domain_info = domain.domain_info + if domain_info: + suborganization = domain_info.sub_organization + if suborganization: + suborganization_name = suborganization.name + except Domain.domain_info.RelatedObjectDoesNotExist: + domain_info = None + + return { + "id": domain.id, + "name": domain.name, + "expiration_date": domain.expiration_date, + "state": domain.state, + "state_display": domain.state_display(), + "get_state_help_text": domain.get_state_help_text(), + "action_url": reverse("domain", kwargs={"pk": domain.id}), + "action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"), + "svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"), + "suborganization": suborganization_name, + } From d9f963361f7c3e002ac48b72a3f247b97f6a4c03 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 9 Jul 2024 09:27:38 -0600 Subject: [PATCH 032/107] PR suggestion --- src/registrar/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index f8f4e1775..a610cdf71 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -621,7 +621,7 @@ class FinishUserProfileTests(TestWithUser, WebTest): self.assertContains(completed_setup_page, "Manage your domain") @less_console_noise_decorator - def test_new_user_with_empty_name_profile_feature_on(self): + def test_new_user_with_empty_name_can_add_name(self): """Tests that a new user without a name can still enter this information accordingly""" self.incomplete_regular_user.contact.first_name = None self.incomplete_regular_user.contact.last_name = None From 59ed58f72c5697a8663efd0e03686b77d7db66b0 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 9 Jul 2024 12:45:34 -0400 Subject: [PATCH 033/107] Add suborg aria label and title --- src/registrar/assets/js/get-gov.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index fe2946867..1c0f08d10 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1197,7 +1197,7 @@ document.addEventListener('DOMContentLoaded', function() { - ${suborganization} + ${suborganization} From ee95a1aac1bcf35596235ce8885b2778b80dd624 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 9 Jul 2024 14:14:57 -0400 Subject: [PATCH 034/107] cleanup --- src/registrar/templates/includes/header_extended.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templates/includes/header_extended.html b/src/registrar/templates/includes/header_extended.html index f388a71bc..dcf950234 100644 --- a/src/registrar/templates/includes/header_extended.html +++ b/src/registrar/templates/includes/header_extended.html @@ -70,4 +70,3 @@ {% endblock %} - \ No newline at end of file From bf5a710754cbc5c2445d52febe357c87af499705 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 9 Jul 2024 15:22:56 -0400 Subject: [PATCH 035/107] Add edit icon to domains overview --- src/registrar/templates/includes/summary_item.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index a2f328e1f..d2804763f 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -112,8 +112,11 @@ From bf4628a8bcff07b94b1f734925a31b2d6e3143c0 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 9 Jul 2024 15:31:13 -0400 Subject: [PATCH 036/107] cleanup scss --- src/registrar/assets/sass/_theme/_links.scss | 18 +++++++----------- .../templates/includes/domains_table.html | 2 +- .../templates/includes/summary_item.html | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_links.scss b/src/registrar/assets/sass/_theme/_links.scss index 6d2e75a68..e9b71733a 100644 --- a/src/registrar/assets/sass/_theme/_links.scss +++ b/src/registrar/assets/sass/_theme/_links.scss @@ -1,18 +1,14 @@ @use "uswds-core" as *; -.dotgov-table { - a { - display: flex; - align-items: flex-start; - color: color('primary'); +.dotgov-table a, +.usa-link--icon { + display: flex; + align-items: flex-start; + color: color('primary'); - &:visited { - color: color('primary'); - } + &:visited { + color: color('primary'); } -} - -a { .usa-icon { // align icon with x height margin-top: units(0.5); diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 8fbfd44e8..f89bf68c1 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -156,7 +156,7 @@