add scripts, templates and settings for sending one-time domain invitations during data migration

This commit is contained in:
David Kennedy 2023-09-18 13:54:53 -04:00
parent 4daec217d7
commit 82b097d2f6
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
5 changed files with 223 additions and 0 deletions

View file

@ -523,6 +523,9 @@ STATIC_URL = "public/"
# {% public_site_url subdir/path %} template tag # {% public_site_url subdir/path %} template tag
GETGOV_PUBLIC_SITE_URL = env_getgov_public_site_url GETGOV_PUBLIC_SITE_URL = env_getgov_public_site_url
# Base URL of application
BASE_URL = env_base_url
# endregion # endregion
# region: Registry----------------------------------------------------------### # region: Registry----------------------------------------------------------###

View file

@ -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()

View file

@ -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()

View file

@ -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.

View file

@ -0,0 +1 @@
You are invited to manage {% if domains|length > 1 %}multiple domains{% else %}{{ domains.0 }}{% endif %} on get.gov