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