merge main->branch

This commit is contained in:
CocoByte 2023-09-25 22:48:55 -06:00
commit 7eba613af8
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
12 changed files with 308 additions and 10 deletions

View file

@ -639,6 +639,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."
class DomainInformationInline(admin.StackedInline):
"""Edit a domain information on the domain page.
We had issues inheriting from both StackedInline
@ -801,4 +816,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)

View file

@ -0,0 +1,65 @@
"""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.
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(",")
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: list):
"""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,142 @@
"""Data migration: Send domain invitations once to existing customers."""
import logging
import copy
from django.core.management import BaseCommand
from registrar.models import TransitionDomain
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.info("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.info("about to send emails")
self.send_emails()
logger.info("done sending emails")
self.update_domains_as_sent()
logger.info("done sending emails and updating transition_domains")
else:
logger.info("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:
# 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(transition_domain.domain_name)
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 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):
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:
send_templated_email(
"emails/transition_domain_invitation.txt",
"emails/transition_domain_invitation_subject.txt",
to_address=email_data["email"],
context={
"domains": email_data["domains"],
},
)
# success message is logged
logger.info(
f"email sent successfully to {email_data['email']} for "
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 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

@ -26,7 +26,7 @@
<li>Domain meets our naming requirements</li>
</ul>
<p> You can <a href="{% url 'todo' %}"><del>check the status</del></a>
<p> You can <a href="{% url 'home' %}">check the status</a>
of your request at any time. We'll email you with any questions or when we
complete our review.</p>

View file

@ -14,8 +14,6 @@
<p>Before your domain can be used we'll need information about your domain
name servers.</p>
<p><a class="usa-link" href="{% url "todo" %}">Get help with domain servers.</a></p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">

View file

@ -1,6 +1,32 @@
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.
To accept your invitation, go to <{{ domain_url }}>.
{{ full_name }} has added you as a manager on {{ domain.name }}.
You will need to log in with a Login.gov account using this email address.
YOU NEED A LOGIN.GOV ACCOUNT
Youll 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 dont already have one, follow these steps to create your
Login.gov account <https://login.gov/help/get-started/create-your-account/>.
DOMAIN MANAGEMENT
As a .gov domain manager you can add or update information about your domain.
Youll also serve as a contact for your .gov domain. Please keep your contact
information updated. Learn more about domain management <https://get.gov/help/>.
SOMETHING WRONG?
If youre not affiliated with {{ domain.name }} or think you received this
message in error, contact the .gov team <https://get.gov/help/#contact-us>.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
using a .gov domain.
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}

View file

@ -1 +1 @@
You are invited to manage {{ domain.name }} on get.gov
Youve been added to a .gov domain

View file

@ -0,0 +1,29 @@
{% 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 }}{% endif %}.
YOU NEED A LOGIN.GOV ACCOUNT
Youll 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 dont already have one, follow these steps to create your Login.gov account <https://login.gov/help/get-started/create-your-account/>.
DOMAIN MANAGEMENT
As a .gov domain manager you can add or update information about your domain{% if domains|length > 1 %}s{% endif %}. Youll 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 <https://get.gov/help/>.
{% if domains|length > 1 %}
DOMAINS
{% for domain in domains %} {{ domain }}
{% endfor %}{% else %}
{% endif %}
SOMETHING WRONG?
If youre not affiliated with {{ domain }} or think you received this message in error, contact the .gov team <https://get.gov/help/#contact-us>.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}

View file

@ -0,0 +1 @@
You've been added to a .gov domain

View file

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

View file

@ -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,10 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain
)
add_page = self.app.get(
reverse("domain-users-add", kwargs={"pk": self.domain.id})
)
@ -1218,6 +1223,10 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain
)
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
@ -1270,6 +1279,11 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
add_page = self.app.get(
reverse("domain-users-add", kwargs={"pk": self.domain.id})
)
self.domain_information, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = EMAIL
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)

View file

@ -16,6 +16,7 @@ from django.views.generic.edit import FormMixin
from registrar.models import (
Domain,
DomainInformation,
DomainInvitation,
User,
UserDomainRole,
@ -335,6 +336,11 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
)
else:
# created a new invitation in the database, so send an email
domaininfo = DomainInformation.objects.filter(domain=self.object)
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",
@ -343,6 +349,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
context={
"domain_url": self._domain_abs_url(),
"domain": self.object,
"full_name": full_name,
},
)
except EmailSendingError: