From 952fe96c0066842362f8270ccb55a2d64a063d0e Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 12 Sep 2023 18:17:53 -0700 Subject: [PATCH 01/18] Fix subject and text of domain invitation --- .../templates/emails/domain_invitation.txt | 31 ++++++++++++++++--- .../emails/domain_invitation_subject.txt | 2 +- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index 8bfb53933..9fb11d2dd 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -1,6 +1,29 @@ -You have been invited to manage the domain {{ domain.name }} on get.gov, -the registrar for .gov domain names. +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +Hi $CONFIRM_firstname. +{{ contact.first_name }} -To accept your invitation, go to <{{ domain_url }}>. -You will need to log in with a Login.gov account using this email address. +$CONFIRM_name-of-person-who-added-this-person has added you as a manager on {{ domain.name }}. +{{ application.submitter.first_name }} +{{ application.submitter.creator }} + +YOU NEED A LOGIN.GOV ACCOUNT +You’ll need a Login.gov account to manage your .gov domain. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account . + +DOMAIN MANAGEMENT +As a .gov domain manager you can add or update information about your domain. You’ll also serve as a contact for your .gov domain. Please keep your contact information updated. Learn more about domain management . + +SOMETHING WRONG? +If you’re not affiliated with $domain.gov or think you received this message in error, contact the .gov team . + + +THANK YOU + +.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. + +---------------------------------------------------------------- + +The .gov team +Contact us: +Visit +{% endautoescape %} diff --git a/src/registrar/templates/emails/domain_invitation_subject.txt b/src/registrar/templates/emails/domain_invitation_subject.txt index 60db880de..319b80176 100644 --- a/src/registrar/templates/emails/domain_invitation_subject.txt +++ b/src/registrar/templates/emails/domain_invitation_subject.txt @@ -1 +1 @@ -You are invited to manage {{ domain.name }} on get.gov +You’ve been added to a .gov domain \ No newline at end of file From 8a8fbdab645323321e67212f1788f097c38184c1 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 12 Sep 2023 18:39:55 -0700 Subject: [PATCH 02/18] Need to fix variables --- src/registrar/templates/emails/domain_invitation.txt | 10 ++++------ src/registrar/views/domain.py | 2 ++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index 9fb11d2dd..5c643b72e 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -1,11 +1,9 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi $CONFIRM_firstname. -{{ contact.first_name }} +Hi {{ incoming-email-here }}. +# We don't know their first name until they make an account, we only know their email +# Stuck here because we only get their email from the form, so can't pull above? - -$CONFIRM_name-of-person-who-added-this-person has added you as a manager on {{ domain.name }}. -{{ application.submitter.first_name }} -{{ application.submitter.creator }} +{{ contact-of-whoever-owns-usually-admin-or-maybe-security-contact }} has added you as a manager on {{ domain.name }}. YOU NEED A LOGIN.GOV ACCOUNT You’ll need a Login.gov account to manage your .gov domain. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account . diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index f945bc443..f7075ed3d 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -338,6 +338,8 @@ class DomainAddUserView(DomainPermissionView, FormMixin): context={ "domain_url": self._domain_abs_url(), "domain": self.object, + # "user": the original person or contact + # "email": email of person we want to add }, ) except EmailSendingError: From bbe030ea3684095afc210f55bbb3d3089103148d Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 13 Sep 2023 16:31:39 -0700 Subject: [PATCH 03/18] Update variables and context --- src/registrar/templates/emails/domain_invitation.txt | 6 ++---- src/registrar/views/domain.py | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index 5c643b72e..05098e9db 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -1,9 +1,7 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi {{ incoming-email-here }}. -# We don't know their first name until they make an account, we only know their email -# Stuck here because we only get their email from the form, so can't pull above? +Hi. -{{ contact-of-whoever-owns-usually-admin-or-maybe-security-contact }} has added you as a manager on {{ domain.name }}. +{{ first_name }} has added you as a manager on {{ domain.name }}. YOU NEED A LOGIN.GOV ACCOUNT You’ll need a Login.gov account to manage your .gov domain. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account . diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 11e6cdb6d..68c578f0d 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -17,6 +17,7 @@ from django.views.generic.edit import FormMixin from registrar.models import ( Domain, DomainInvitation, + DomainApplication, User, UserDomainRole, ) @@ -335,6 +336,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin): ) else: # created a new invitation in the database, so send an email + dapplication = DomainApplication.objects.filter(approved_domain__name=self.object.name) try: send_templated_email( "emails/domain_invitation.txt", @@ -343,8 +345,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin): context={ "domain_url": self._domain_abs_url(), "domain": self.object, - # "user": the original person or contact - # "email": email of person we want to add + "first_name": dapplication.first().creator, }, ) except EmailSendingError: From fc1792fc3c0359981c940727ffad785abe259f6d Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 15 Sep 2023 10:28:54 -0700 Subject: [PATCH 04/18] Fix creator reference and wording on invitation --- .../templates/emails/domain_invitation.txt | 4 ++-- src/registrar/tests/common.py | 3 ++- src/registrar/tests/test_views.py | 23 +++++++++++++++++++ src/registrar/views/domain.py | 5 ++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index 05098e9db..8b895b679 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -1,7 +1,7 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi. -{{ first_name }} has added you as a manager on {{ domain.name }}. +{{ full_name }} has added you as a manager on {{ domain.name }}. YOU NEED A LOGIN.GOV ACCOUNT You’ll need a Login.gov account to manage your .gov domain. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account . @@ -10,7 +10,7 @@ DOMAIN MANAGEMENT As a .gov domain manager you can add or update information about your domain. You’ll also serve as a contact for your .gov domain. Please keep your contact information updated. Learn more about domain management . SOMETHING WRONG? -If you’re not affiliated with $domain.gov or think you received this message in error, contact the .gov team . +If you’re not affiliated with {{ domain.name }} or think you received this message in error, contact the .gov team . THANK YOU diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index e21431321..b0f920bfd 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -457,6 +457,7 @@ def completed_application( has_anything_else=True, status=DomainApplication.STARTED, user=False, + name="city.gov", ): """A completed domain application.""" if not user: @@ -468,7 +469,7 @@ def completed_application( email="testy@town.com", phone="(555) 555 5555", ) - domain, _ = DraftDomain.objects.get_or_create(name="city.gov") + domain, _ = DraftDomain.objects.get_or_create(name=name) alt, _ = Website.objects.get_or_create(website="city1.gov") current, _ = Website.objects.get_or_create(website="city.com") you, _ = Contact.objects.get_or_create( diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 318cc261d..88cd342b1 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1079,6 +1079,7 @@ class TestWithDomainPermissions(TestWithUser): self.domain_information.delete() if hasattr(self.domain, "contacts"): self.domain.contacts.all().delete() + DomainApplication.objects.all().delete() self.domain.delete() self.role.delete() except ValueError: # pass if already deleted @@ -1197,6 +1198,13 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() + # Create an application + application = completed_application( + status=DomainApplication.APPROVED, user=self.user, name=self.domain.name + ) + application.approved_domain = self.domain + application.save() + add_page = self.app.get( reverse("domain-users-add", kwargs={"pk": self.domain.id}) ) @@ -1218,6 +1226,13 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() + # Create an application + application = completed_application( + status=DomainApplication.APPROVED, user=self.user, name=self.domain.name + ) + application.approved_domain = self.domain + application.save() + mock_client = MagicMock() mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): @@ -1270,6 +1285,14 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): add_page = self.app.get( reverse("domain-users-add", kwargs={"pk": self.domain.id}) ) + + # Create an application + application = completed_application( + status=DomainApplication.APPROVED, user=self.user, name=self.domain.name + ) + application.approved_domain = self.domain + application.save() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] add_page.form["email"] = EMAIL self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 68c578f0d..d698fc04d 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -336,7 +336,8 @@ class DomainAddUserView(DomainPermissionView, FormMixin): ) else: # created a new invitation in the database, so send an email - dapplication = DomainApplication.objects.filter(approved_domain__name=self.object.name) + dapplication = DomainApplication.objects.filter(approved_domain=self.object) + try: send_templated_email( "emails/domain_invitation.txt", @@ -345,7 +346,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin): context={ "domain_url": self._domain_abs_url(), "domain": self.object, - "first_name": dapplication.first().creator, + "full_name": dapplication.first().creator, }, ) except EmailSendingError: From 82b097d2f6157f8e57ad7c3c9bd8077337363ef5 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 18 Sep 2023 13:54:53 -0400 Subject: [PATCH 05/18] add scripts, templates and settings for sending one-time domain invitations during data migration --- src/registrar/config/settings.py | 3 + .../generate_test_transition_domains.py | 60 +++++++ .../commands/send_domain_invitations.py | 148 ++++++++++++++++++ .../emails/transition_domain_invitation.txt | 11 ++ .../transition_domain_invitation_subject.txt | 1 + 5 files changed, 223 insertions(+) create mode 100644 src/registrar/management/commands/generate_test_transition_domains.py create mode 100644 src/registrar/management/commands/send_domain_invitations.py create mode 100644 src/registrar/templates/emails/transition_domain_invitation.txt create mode 100644 src/registrar/templates/emails/transition_domain_invitation_subject.txt diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index e272e6622..933d95828 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -523,6 +523,9 @@ STATIC_URL = "public/" # {% public_site_url subdir/path %} template tag GETGOV_PUBLIC_SITE_URL = env_getgov_public_site_url +# Base URL of application +BASE_URL = env_base_url + # endregion # region: Registry----------------------------------------------------------### diff --git a/src/registrar/management/commands/generate_test_transition_domains.py b/src/registrar/management/commands/generate_test_transition_domains.py new file mode 100644 index 000000000..911857a1c --- /dev/null +++ b/src/registrar/management/commands/generate_test_transition_domains.py @@ -0,0 +1,60 @@ +"""Data migration: Generate fake transition domains, replacing existing ones.""" + +import logging + +from django.core.management import BaseCommand +from registrar.models import TransitionDomain, Domain + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Generate test transition domains from existing domains" + + # Generates test transition domains for testing send_domain_invitations script. + # Running this script removes all existing transition domains, so use with caution. + # Transition domains are created with email addresses provided as command line + # argument. Email addresses for testing are passed as comma delimited list of + # email addresses, and are required to be provided. Email addresses from the list + # are assigned to transition domains at time of creation. + + def add_arguments(self, parser): + """Add command line arguments.""" + parser.add_argument( + "-e", + "--emails", + required=True, + dest="emails", + help="Comma-delimited list of email addresses to be used for testing", + ) + + def handle(self, **options): + """Delete existing TransitionDomains. Generate test ones.""" + + # split options[emails] into an array of test emails + test_emails = options["emails"].split(",") + + # setting up test data + self.delete_test_transition_domains() + self.load_test_transition_domains(test_emails) + + def load_test_transition_domains(self, test_emails): + """Load test transition domains""" + + # counter for test_emails index + test_emails_counter = 0 + # Need to get actual domain names from the database for this test + real_domains = Domain.objects.all() + for real_domain in real_domains: + TransitionDomain.objects.create( + username=test_emails[test_emails_counter % len(test_emails)], + domain_name=real_domain.name, + status="created", + email_sent=False, + ) + test_emails_counter += 1 + + def delete_test_transition_domains(self): + self.transition_domains = TransitionDomain.objects.all() + for transition_domain in self.transition_domains: + transition_domain.delete() diff --git a/src/registrar/management/commands/send_domain_invitations.py b/src/registrar/management/commands/send_domain_invitations.py new file mode 100644 index 000000000..3f3542c70 --- /dev/null +++ b/src/registrar/management/commands/send_domain_invitations.py @@ -0,0 +1,148 @@ +"""Data migration: Send domain invitations once to existing customers.""" + +import logging +import copy + +from django.conf import settings +from django.core.management import BaseCommand +from django.urls import reverse +from registrar.models import TransitionDomain, Domain +from ...utility.email import send_templated_email, EmailSendingError +from typing import List + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Send domain invitations once to existing customers." + + # this array is used to store and process the transition_domains + transition_domains: List[str] = [] + # this array is used to store domains with errors, which are not + # sent emails; this array is used to update the succesful + # transition_domains to email_sent=True, and also to report + # out errors + domains_with_errors: List[str] = [] + # this array is used to store email_context; each item in the array + # contains the context for a single email; single emails may be 1 + # or more transition_domains, as they are grouped by username + emails_to_send: List[str] = [] + + def add_arguments(self, parser): + """Add command line arguments.""" + parser.add_argument( + "-s", + "--send_emails", + action="store_true", + default=False, + dest="send_emails", + help="Send emails ", + ) + + def handle(self, **options): + """Process the objects in TransitionDomain.""" + + logger.debug("checking domains and preparing emails") + # Get all TransitionDomain objects + self.transition_domains = TransitionDomain.objects.filter( + email_sent=False, + ).order_by("username") + + self.build_emails_to_send_array() + + if options["send_emails"]: + logger.debug("about to send emails") + self.send_emails() + logger.debug("done sending emails") + + self.update_domains_as_sent() + + logger.debug("done sending emails and updating transition_domains") + else: + logger.debug("not sending emails") + + def build_emails_to_send_array(self): + """this method sends emails to distinct usernames""" + + # data structure to hold email context for a single email; + # transition_domains ordered by username, a single email_context + # may include information from more than one transition_domain + email_context = {"email": ""} + + # loop through all transition_domains; group them by username + # into emails_to_send_array + for transition_domain in self.transition_domains: + # attempt to get the domain from domain objects; if there is + # an error getting the domain, skip this domain and add it to + # domains_with_errors + try: + domain = Domain.objects.get(name=transition_domain.domain_name) + # if prior username does not match current username + if ( + not email_context["email"] + or email_context["email"] != transition_domain.username + ): + # if not first in list of transition_domains + if email_context["email"]: + # append the email context to the emails_to_send array + self.emails_to_send.append(copy.deepcopy(email_context)) + email_context["domains"] = [] + email_context["email"] = transition_domain.username + email_context["domains"].append( + { + "name": transition_domain.domain_name, + "url": settings.BASE_URL + + reverse("domain", kwargs={"pk": domain.id}), + } + ) + except Exception as err: + # error condition if domain not in database + self.domains_with_errors.append( + copy.deepcopy(transition_domain.domain_name) + ) + logger.error( + f"error retrieving domain {transition_domain.domain_name}: {err}" + ) + # if there are at least one more transition domains than errors, + # then send one more + if len(self.transition_domains) > len(self.domains_with_errors): + self.emails_to_send.append(email_context) + + def send_emails(self): + for email_data in self.emails_to_send: + self.send_email(email_data) + + def send_email(self, email_data): + try: + send_templated_email( + "emails/transition_domain_invitation.txt", + "emails/transition_domain_invitation_subject.txt", + to_address=email_data["email"], + context={ + "domains": email_data["domains"], + }, + ) + # if log level set to debug, success message is logged + logger.debug( + f"email sent successfully to {email_data['email']} for " + f"{[domain['name'] for domain in email_data['domains']]}" + ) + except EmailSendingError as err: + logger.error( + f"email did not send successfully to {email_data['email']} " + f"for {[domain['name'] for domain in email_data['domains']]}" + f": {err}" + ) + # if email failed to send, set error in domains_with_errors for each + # domain in the email so that transition domain email_sent is not set + # to True + for domain in email_data["domains"]: + self.domains_with_errors.append(domain) + + def update_domains_as_sent(self): + """set email_sent to True in all transition_domains which have + been processed successfully""" + for transition_domain in self.transition_domains: + if transition_domain.domain_name not in self.domains_with_errors: + transition_domain.email_sent = True + transition_domain.save() diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt new file mode 100644 index 000000000..8b7389c04 --- /dev/null +++ b/src/registrar/templates/emails/transition_domain_invitation.txt @@ -0,0 +1,11 @@ +You have been invited to manage {% if domains|length > 1 %}multiple domains{% else %}the domain {{ domains.0.name }}{% endif %} on get.gov, +the registrar for .gov domain names. +{%if domains|length > 1 %} +To accept your invitation, go to each of the following urls: + {% for domain in domains %} + {{ domain.url }} (to manage {{ domain.name }}) + {% endfor %} +{% else %} +To accept your invitation, go to <{{ domains.0.url }}>. +{% endif %} +You will need to log in with a Login.gov account using this email address. diff --git a/src/registrar/templates/emails/transition_domain_invitation_subject.txt b/src/registrar/templates/emails/transition_domain_invitation_subject.txt new file mode 100644 index 000000000..380b338b2 --- /dev/null +++ b/src/registrar/templates/emails/transition_domain_invitation_subject.txt @@ -0,0 +1 @@ +You are invited to manage {% if domains|length > 1 %}multiple domains{% else %}{{ domains.0 }}{% endif %} on get.gov From 91aaa0ff3e4747ab95a08863e7ab21bfe7575eec Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 20 Sep 2023 05:20:09 -0400 Subject: [PATCH 06/18] updated some error handling, logging, and text of emails --- .../generate_test_transition_domains.py | 15 +++++--- .../commands/send_domain_invitations.py | 23 ++++++----- .../emails/transition_domain_invitation.txt | 38 ++++++++++++++----- .../transition_domain_invitation_subject.txt | 2 +- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/registrar/management/commands/generate_test_transition_domains.py b/src/registrar/management/commands/generate_test_transition_domains.py index 911857a1c..20aba2c58 100644 --- a/src/registrar/management/commands/generate_test_transition_domains.py +++ b/src/registrar/management/commands/generate_test_transition_domains.py @@ -29,16 +29,21 @@ class Command(BaseCommand): ) def handle(self, **options): - """Delete existing TransitionDomains. Generate test ones.""" + """Delete existing TransitionDomains. Generate test ones. + expects options[emails]; emails will be assigned to transition + domains at the time of creation""" # split options[emails] into an array of test emails test_emails = options["emails"].split(",") - # setting up test data - self.delete_test_transition_domains() - self.load_test_transition_domains(test_emails) + if len(test_emails) > 0: + # set up test data + self.delete_test_transition_domains() + self.load_test_transition_domains(test_emails) + else: + logger.error("list of emails for testing is required") - def load_test_transition_domains(self, test_emails): + def load_test_transition_domains(self, test_emails: list): """Load test transition domains""" # counter for test_emails index diff --git a/src/registrar/management/commands/send_domain_invitations.py b/src/registrar/management/commands/send_domain_invitations.py index 3f3542c70..4c595a2b5 100644 --- a/src/registrar/management/commands/send_domain_invitations.py +++ b/src/registrar/management/commands/send_domain_invitations.py @@ -42,7 +42,7 @@ class Command(BaseCommand): def handle(self, **options): """Process the objects in TransitionDomain.""" - logger.debug("checking domains and preparing emails") + logger.info("checking domains and preparing emails") # Get all TransitionDomain objects self.transition_domains = TransitionDomain.objects.filter( email_sent=False, @@ -51,15 +51,15 @@ class Command(BaseCommand): self.build_emails_to_send_array() if options["send_emails"]: - logger.debug("about to send emails") + logger.info("about to send emails") self.send_emails() - logger.debug("done sending emails") + logger.info("done sending emails") self.update_domains_as_sent() - logger.debug("done sending emails and updating transition_domains") + logger.info("done sending emails and updating transition_domains") else: - logger.debug("not sending emails") + logger.info("not sending emails") def build_emails_to_send_array(self): """this method sends emails to distinct usernames""" @@ -104,13 +104,16 @@ class Command(BaseCommand): f"error retrieving domain {transition_domain.domain_name}: {err}" ) # if there are at least one more transition domains than errors, - # then send one more + # then append one more item if len(self.transition_domains) > len(self.domains_with_errors): self.emails_to_send.append(email_context) def send_emails(self): - for email_data in self.emails_to_send: - self.send_email(email_data) + if len(self.emails_to_send) > 0: + for email_data in self.emails_to_send: + self.send_email(email_data) + else: + logger.info("no emails to send") def send_email(self, email_data): try: @@ -122,8 +125,8 @@ class Command(BaseCommand): "domains": email_data["domains"], }, ) - # if log level set to debug, success message is logged - logger.debug( + # success message is logged + logger.info( f"email sent successfully to {email_data['email']} for " f"{[domain['name'] for domain in email_data['domains']]}" ) diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt index 8b7389c04..146db9743 100644 --- a/src/registrar/templates/emails/transition_domain_invitation.txt +++ b/src/registrar/templates/emails/transition_domain_invitation.txt @@ -1,11 +1,29 @@ -You have been invited to manage {% if domains|length > 1 %}multiple domains{% else %}the domain {{ domains.0.name }}{% endif %} on get.gov, -the registrar for .gov domain names. -{%if domains|length > 1 %} -To accept your invitation, go to each of the following urls: - {% for domain in domains %} - {{ domain.url }} (to manage {{ domain.name }}) - {% endfor %} -{% else %} -To accept your invitation, go to <{{ domains.0.url }}>. +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +Hi. + +You have been added as a manager on {% if domains|length > 1 %}multiple domains (listed below){% else %}{{ domains.0.name }}{% endif %}. + +YOU NEED A LOGIN.GOV ACCOUNT +You’ll need a Login.gov account to manage your .gov domain{% if domains|length > 1 %}s{% endif %}. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account . + +DOMAIN MANAGEMENT +As a .gov domain manager you can add or update information about your domain{% if domains|length > 1 %}s{% endif %}. You’ll also serve as a contact for your .gov domain{% if domains|length > 1 %}s{% endif %}. Please keep your contact information updated. Learn more about domain management . +{% if domains|length > 1 %} +DOMAINS +{% for domain in domains %} {{ domain.name }} +{% endfor %}{% else %} {% endif %} -You will need to log in with a Login.gov account using this email address. +SOMETHING WRONG? +If you’re not affiliated with {{ domain.name }} or think you received this message in error, contact the .gov team . + + +THANK YOU + +.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. + +---------------------------------------------------------------- + +The .gov team +Contact us: +Visit +{% endautoescape %} diff --git a/src/registrar/templates/emails/transition_domain_invitation_subject.txt b/src/registrar/templates/emails/transition_domain_invitation_subject.txt index 380b338b2..9302a748e 100644 --- a/src/registrar/templates/emails/transition_domain_invitation_subject.txt +++ b/src/registrar/templates/emails/transition_domain_invitation_subject.txt @@ -1 +1 @@ -You are invited to manage {% if domains|length > 1 %}multiple domains{% else %}{{ domains.0 }}{% endif %} on get.gov +You've been added to a .gov domain \ No newline at end of file From 3243399d265a171c091d9db0f376d39a7040f55c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 20 Sep 2023 08:40:29 -0400 Subject: [PATCH 07/18] simplified data sent to transition domain invitation email templates --- src/registrar/config/settings.py | 3 --- .../management/commands/send_domain_invitations.py | 12 +++--------- .../emails/transition_domain_invitation.txt | 6 +++--- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 933d95828..e272e6622 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -523,9 +523,6 @@ STATIC_URL = "public/" # {% public_site_url subdir/path %} template tag GETGOV_PUBLIC_SITE_URL = env_getgov_public_site_url -# Base URL of application -BASE_URL = env_base_url - # endregion # region: Registry----------------------------------------------------------### diff --git a/src/registrar/management/commands/send_domain_invitations.py b/src/registrar/management/commands/send_domain_invitations.py index 4c595a2b5..fea2139c8 100644 --- a/src/registrar/management/commands/send_domain_invitations.py +++ b/src/registrar/management/commands/send_domain_invitations.py @@ -3,9 +3,7 @@ import logging import copy -from django.conf import settings from django.core.management import BaseCommand -from django.urls import reverse from registrar.models import TransitionDomain, Domain from ...utility.email import send_templated_email, EmailSendingError from typing import List @@ -89,11 +87,7 @@ class Command(BaseCommand): email_context["domains"] = [] email_context["email"] = transition_domain.username email_context["domains"].append( - { - "name": transition_domain.domain_name, - "url": settings.BASE_URL - + reverse("domain", kwargs={"pk": domain.id}), - } + transition_domain.domain_name ) except Exception as err: # error condition if domain not in database @@ -128,12 +122,12 @@ class Command(BaseCommand): # success message is logged logger.info( f"email sent successfully to {email_data['email']} for " - f"{[domain['name'] for domain in email_data['domains']]}" + f"{[domain for domain in email_data['domains']]}" ) except EmailSendingError as err: logger.error( f"email did not send successfully to {email_data['email']} " - f"for {[domain['name'] for domain in email_data['domains']]}" + f"for {[domain for domain in email_data['domains']]}" f": {err}" ) # if email failed to send, set error in domains_with_errors for each diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt index 146db9743..42013dbf7 100644 --- a/src/registrar/templates/emails/transition_domain_invitation.txt +++ b/src/registrar/templates/emails/transition_domain_invitation.txt @@ -1,7 +1,7 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi. -You have been added as a manager on {% if domains|length > 1 %}multiple domains (listed below){% else %}{{ domains.0.name }}{% endif %}. +You have been added as a manager on {% if domains|length > 1 %}multiple domains (listed below){% else %}{{ domains.0 }}{% endif %}. YOU NEED A LOGIN.GOV ACCOUNT You’ll need a Login.gov account to manage your .gov domain{% if domains|length > 1 %}s{% endif %}. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account . @@ -10,11 +10,11 @@ DOMAIN MANAGEMENT As a .gov domain manager you can add or update information about your domain{% if domains|length > 1 %}s{% endif %}. You’ll also serve as a contact for your .gov domain{% if domains|length > 1 %}s{% endif %}. Please keep your contact information updated. Learn more about domain management . {% if domains|length > 1 %} DOMAINS -{% for domain in domains %} {{ domain.name }} +{% for domain in domains %} {{ domain }} {% endfor %}{% else %} {% endif %} SOMETHING WRONG? -If you’re not affiliated with {{ domain.name }} or think you received this message in error, contact the .gov team . +If you’re not affiliated with {{ domain }} or think you received this message in error, contact the .gov team . THANK YOU From 46b6467fcad575184f17c7d7dee28cf6851e838f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 20 Sep 2023 10:00:11 -0400 Subject: [PATCH 08/18] code reformatting --- src/registrar/management/commands/send_domain_invitations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/registrar/management/commands/send_domain_invitations.py b/src/registrar/management/commands/send_domain_invitations.py index fea2139c8..d42aa6ea0 100644 --- a/src/registrar/management/commands/send_domain_invitations.py +++ b/src/registrar/management/commands/send_domain_invitations.py @@ -74,7 +74,6 @@ class Command(BaseCommand): # an error getting the domain, skip this domain and add it to # domains_with_errors try: - domain = Domain.objects.get(name=transition_domain.domain_name) # if prior username does not match current username if ( not email_context["email"] @@ -86,9 +85,7 @@ class Command(BaseCommand): self.emails_to_send.append(copy.deepcopy(email_context)) email_context["domains"] = [] email_context["email"] = transition_domain.username - email_context["domains"].append( - transition_domain.domain_name - ) + email_context["domains"].append(transition_domain.domain_name) except Exception as err: # error condition if domain not in database self.domains_with_errors.append( From 6fea12bac6a2b4ef21348d02ddd9f4ed52a298d9 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 20 Sep 2023 10:05:34 -0400 Subject: [PATCH 09/18] remove unused import --- src/registrar/management/commands/send_domain_invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/send_domain_invitations.py b/src/registrar/management/commands/send_domain_invitations.py index d42aa6ea0..994013254 100644 --- a/src/registrar/management/commands/send_domain_invitations.py +++ b/src/registrar/management/commands/send_domain_invitations.py @@ -4,7 +4,7 @@ import logging import copy from django.core.management import BaseCommand -from registrar.models import TransitionDomain, Domain +from registrar.models import TransitionDomain from ...utility.email import send_templated_email, EmailSendingError from typing import List From df62bf12beec9c9cf1cc8b2b67f994ee513bf8a4 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:45:33 -0500 Subject: [PATCH 10/18] Name servers page: Remove link --- src/registrar/templates/domain_nameservers.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index 9c261306a..6a6a0b729 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -14,8 +14,6 @@

Before your domain can be used we'll need information about your domain name servers.

-

Get help with domain servers.

- {% include "includes/required_fields.html" %}
From 46a9241635916c7f9a2c03f826f34b8c21aafd7b Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:59:18 -0500 Subject: [PATCH 11/18] Domain request confirmation: add link --- src/registrar/templates/application_done.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/application_done.html b/src/registrar/templates/application_done.html index 5df03d698..a5b37d2ab 100644 --- a/src/registrar/templates/application_done.html +++ b/src/registrar/templates/application_done.html @@ -26,7 +26,7 @@
  • Domain meets our naming requirements
  • -

    You can check the status +

    You can check the status of your request at any time. We'll email you with any questions or when we complete our review.

    From 0818e982bcc461f990cbd0e8947319a7508190da Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Wed, 20 Sep 2023 17:52:05 -0500 Subject: [PATCH 12/18] Remove strikethru --- src/registrar/templates/application_done.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/application_done.html b/src/registrar/templates/application_done.html index a5b37d2ab..a9ee55b47 100644 --- a/src/registrar/templates/application_done.html +++ b/src/registrar/templates/application_done.html @@ -26,7 +26,7 @@
  • Domain meets our naming requirements
  • -

    You can check the status +

    You can check the status of your request at any time. We'll email you with any questions or when we complete our review.

    From 32406550def03255ceafd9abda49f1f61ed7bc2a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 20 Sep 2023 16:01:49 -0700 Subject: [PATCH 13/18] Use DomainInformation instead of DomainApplication --- src/registrar/tests/test_views.py | 21 ++++++--------------- src/registrar/views/domain.py | 7 ++++--- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 88cd342b1..0bd2b6399 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1198,12 +1198,9 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - # Create an application - application = completed_application( - status=DomainApplication.APPROVED, user=self.user, name=self.domain.name + self.domain_information, _ = DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain ) - application.approved_domain = self.domain - application.save() add_page = self.app.get( reverse("domain-users-add", kwargs={"pk": self.domain.id}) @@ -1226,12 +1223,9 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - # Create an application - application = completed_application( - status=DomainApplication.APPROVED, user=self.user, name=self.domain.name + self.domain_information, _ = DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain ) - application.approved_domain = self.domain - application.save() mock_client = MagicMock() mock_client_instance = mock_client.return_value @@ -1286,12 +1280,9 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): reverse("domain-users-add", kwargs={"pk": self.domain.id}) ) - # Create an application - application = completed_application( - status=DomainApplication.APPROVED, user=self.user, name=self.domain.name + self.domain_information, _ = DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain ) - application.approved_domain = self.domain - application.save() session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] add_page.form["email"] = EMAIL diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d698fc04d..45b64854f 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -16,6 +16,7 @@ from django.views.generic.edit import FormMixin from registrar.models import ( Domain, + DomainInformation, DomainInvitation, DomainApplication, User, @@ -336,8 +337,8 @@ class DomainAddUserView(DomainPermissionView, FormMixin): ) else: # created a new invitation in the database, so send an email - dapplication = DomainApplication.objects.filter(approved_domain=self.object) - + domaininfo = DomainInformation.objects.filter(domain=self.object) + full_name = domaininfo.first().creator try: send_templated_email( "emails/domain_invitation.txt", @@ -346,7 +347,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin): context={ "domain_url": self._domain_abs_url(), "domain": self.object, - "full_name": dapplication.first().creator, + "full_name": full_name, }, ) except EmailSendingError: From 28aa35cbf6f455781963bdd753ecad5adb7e16b5 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 20 Sep 2023 16:08:56 -0700 Subject: [PATCH 14/18] Lint error - remove domainapp --- src/registrar/views/domain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 45b64854f..d1b4eb48c 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -18,7 +18,6 @@ from registrar.models import ( Domain, DomainInformation, DomainInvitation, - DomainApplication, User, UserDomainRole, ) From 107299fb53484105dc527581e06fd24ad5ae0ca7 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 21 Sep 2023 09:54:54 -0700 Subject: [PATCH 15/18] Update column spacing for domain invitation email --- .../templates/emails/domain_invitation.txt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index 8b895b679..ed9c297f4 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -4,22 +4,29 @@ Hi. {{ full_name }} has added you as a manager on {{ domain.name }}. YOU NEED A LOGIN.GOV ACCOUNT -You’ll need a Login.gov account to manage your .gov domain. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account . +You’ll need a Login.gov account to manage your .gov domain. Login.gov provides +a simple and secure process for signing into many government services with one +account. If you don’t already have one, follow these steps to create your +Login.gov account . DOMAIN MANAGEMENT -As a .gov domain manager you can add or update information about your domain. You’ll also serve as a contact for your .gov domain. Please keep your contact information updated. Learn more about domain management . +As a .gov domain manager you can add or update information about your domain. +You’ll also serve as a contact for your .gov domain. Please keep your contact +information updated. Learn more about domain management . SOMETHING WRONG? -If you’re not affiliated with {{ domain.name }} or think you received this message in error, contact the .gov team . +If you’re not affiliated with {{ domain.name }} or think you received this +message in error, contact the .gov team . THANK YOU -.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. +.Gov helps the public identify official, trusted information. Thank you for +using a .gov domain. ---------------------------------------------------------------- The .gov team Contact us: Visit -{% endautoescape %} +{% endautoescape %} \ No newline at end of file From ebbbba41f6e308ce329eabb31d62abf11e87a53c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 21 Sep 2023 16:57:30 -0700 Subject: [PATCH 16/18] Fix naming --- src/registrar/views/domain.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d1b4eb48c..a4498146a 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -337,7 +337,10 @@ class DomainAddUserView(DomainPermissionView, FormMixin): else: # created a new invitation in the database, so send an email domaininfo = DomainInformation.objects.filter(domain=self.object) - full_name = domaininfo.first().creator + first = domaininfo.first().creator.first_name + last = domaininfo.first().creator.last_name + full_name = f"{first} {last}" + try: send_templated_email( "emails/domain_invitation.txt", From 16bec3f2c34d797b46b6dec20906888e61337daf Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 22 Sep 2023 13:05:20 -0400 Subject: [PATCH 17/18] Add columns and search on TransitionDomain --- src/registrar/admin.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 56f1c093a..8df9ef380 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -691,6 +691,21 @@ class DomainApplicationAdmin(ListHeaderAdmin): return super().change_view(request, object_id, form_url, extra_context) +class TransitionDomainAdmin(ListHeaderAdmin): + """Custom transition domain admin class.""" + + # Columns + list_display = [ + "username", + "domain_name", + "status", + "email_sent", + ] + + search_fields = ["username", "domain_name"] + search_help_text = "Search by user or domain name." + + admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) admin.site.register(models.User, MyUserAdmin) @@ -704,4 +719,4 @@ admin.site.register(models.Nameserver, MyHostAdmin) admin.site.register(models.Website, WebsiteAdmin) admin.site.register(models.PublicContact, AuditedAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin) -admin.site.register(models.TransitionDomain, AuditedAdmin) +admin.site.register(models.TransitionDomain, TransitionDomainAdmin) From 716782dc8ce3909b36fc8e253ef77c39856b0b7e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 22 Sep 2023 14:50:28 -0400 Subject: [PATCH 18/18] formatting to satisfy lint --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 78e756044..a75d644b7 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -653,7 +653,7 @@ class TransitionDomainAdmin(ListHeaderAdmin): search_fields = ["username", "domain_name"] search_help_text = "Search by user or domain name." - + class DomainInformationInline(admin.StackedInline): """Edit a domain information on the domain page. We had issues inheriting from both StackedInline