Merge branch 'main' into za/1484-domain-manager-delete

This commit is contained in:
zandercymatics 2024-01-26 10:43:33 -07:00
commit 12c8cd7c19
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
26 changed files with 230 additions and 66 deletions

View file

@ -26,7 +26,6 @@ on:
- rb - rb
- ko - ko
- ab - ab
- bl
- rjm - rjm
- dk - dk

View file

@ -26,7 +26,6 @@ on:
- rb - rb
- ko - ko
- ab - ab
- bl
- rjm - rjm
- dk - dk

View file

@ -1,32 +0,0 @@
---
applications:
- name: getgov-bl
buildpacks:
- python_buildpack
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http
health-check-http-endpoint: /health
health-check-invocation-timeout: 40
env:
# Send stdout and stderr straight to the terminal without buffering
PYTHONUNBUFFERED: yup
# Tell Django where to find its configuration
DJANGO_SETTINGS_MODULE: registrar.config.settings
# Tell Django where it is being hosted
DJANGO_BASE_URL: https://getgov-bl.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# default public site location
GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments
IS_PRODUCTION: False
routes:
- route: getgov-bl.app.cloud.gov
services:
- getgov-credentials
- getgov-bl-database

View file

@ -33,6 +33,10 @@ class AuthenticationFailed(OIDCException):
friendly_message = "This login attempt didn't work." friendly_message = "This login attempt didn't work."
class NoStateDefined(OIDCException):
friendly_message = "The session state is None."
class InternalError(OIDCException): class InternalError(OIDCException):
status = status.INTERNAL_SERVER_ERROR status = status.INTERNAL_SERVER_ERROR
friendly_message = "The system broke while trying to log you in." friendly_message = "The system broke while trying to log you in."

View file

@ -183,6 +183,8 @@ class Client(oic.Client):
if authn_response["state"] != session.get("state", None): if authn_response["state"] != session.get("state", None):
# this most likely means the user's Django session vanished # this most likely means the user's Django session vanished
logger.error("Received state not the same as expected for %s" % state) logger.error("Received state not the same as expected for %s" % state)
if session.get("state", None) is None:
raise o_e.NoStateDefined()
raise o_e.AuthenticationFailed(locator=state) raise o_e.AuthenticationFailed(locator=state)
if self.behaviour.get("response_type") == "code": if self.behaviour.get("response_type") == "code":
@ -272,6 +274,11 @@ class Client(oic.Client):
super(Client, self).store_response(resp, info) super(Client, self).store_response(resp, info)
def get_default_acr_value(self):
"""returns the acr_value from settings
this helper function is called from djangooidc views"""
return self.behaviour.get("acr_value")
def get_step_up_acr_value(self): def get_step_up_acr_value(self):
"""returns the step_up_acr_value from settings """returns the step_up_acr_value from settings
this helper function is called from djangooidc views""" this helper function is called from djangooidc views"""

View file

@ -3,6 +3,8 @@ from unittest.mock import MagicMock, patch
from django.http import HttpResponse from django.http import HttpResponse
from django.test import Client, TestCase, RequestFactory from django.test import Client, TestCase, RequestFactory
from django.urls import reverse from django.urls import reverse
from djangooidc.exceptions import NoStateDefined
from ..views import login_callback from ..views import login_callback
from .common import less_console_noise from .common import less_console_noise
@ -17,6 +19,9 @@ class ViewsTest(TestCase):
def say_hi(*args): def say_hi(*args):
return HttpResponse("Hi") return HttpResponse("Hi")
def create_acr(*args):
return "any string"
def user_info(*args): def user_info(*args):
return { return {
"sub": "TEST", "sub": "TEST",
@ -34,6 +39,7 @@ class ViewsTest(TestCase):
callback_url = reverse("openid_login_callback") callback_url = reverse("openid_login_callback")
# mock # mock
mock_client.create_authn_request.side_effect = self.say_hi mock_client.create_authn_request.side_effect = self.say_hi
mock_client.get_default_acr_value.side_effect = self.create_acr
# test # test
response = self.client.get(reverse("login"), {"next": callback_url}) response = self.client.get(reverse("login"), {"next": callback_url})
# assert # assert
@ -53,6 +59,19 @@ class ViewsTest(TestCase):
self.assertTemplateUsed(response, "500.html") self.assertTemplateUsed(response, "500.html")
self.assertIn("Server error", response.content.decode("utf-8")) self.assertIn("Server error", response.content.decode("utf-8"))
def test_callback_with_no_session_state(self, mock_client):
"""If the local session is None (ie the server restarted while user was logged out),
we do not throw an exception. Rather, we attempt to login again."""
# mock
mock_client.get_default_acr_value.side_effect = self.create_acr
mock_client.callback.side_effect = NoStateDefined()
# test
with less_console_noise():
response = self.client.get(reverse("openid_login_callback"))
# assert
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
def test_login_callback_reads_next(self, mock_client): def test_login_callback_reads_next(self, mock_client):
# setup # setup
session = self.client.session session = self.client.session

View file

@ -55,6 +55,10 @@ def error_page(request, error):
def openid(request): def openid(request):
"""Redirect the user to an authentication provider (OP).""" """Redirect the user to an authentication provider (OP)."""
# If the session reset because of a server restart, attempt to login again
request.session["acr_value"] = CLIENT.get_default_acr_value()
request.session["next"] = request.GET.get("next", "/") request.session["next"] = request.GET.get("next", "/")
try: try:
@ -78,9 +82,13 @@ def login_callback(request):
if user: if user:
login(request, user) login(request, user)
logger.info("Successfully logged in user %s" % user) logger.info("Successfully logged in user %s" % user)
# Double login bug (1507)?
return redirect(request.session.get("next", "/")) return redirect(request.session.get("next", "/"))
else: else:
raise o_e.BannedUser() raise o_e.BannedUser()
except o_e.NoStateDefined as nsd_err:
logger.warning(f"No State Defined: {nsd_err}")
return redirect(request.session.get("next", "/"))
except Exception as err: except Exception as err:
return error_page(request, err) return error_page(request, err)

View file

@ -610,7 +610,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
), ),
("Anything else?", {"fields": ["anything_else"]}), ("Anything else?", {"fields": ["anything_else"]}),
( (
"Requirements for operating .gov domains", "Requirements for operating a .gov domain",
{"fields": ["is_policy_acknowledged"]}, {"fields": ["is_policy_acknowledged"]},
), ),
] ]
@ -779,7 +779,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
), ),
("Anything else?", {"fields": ["anything_else"]}), ("Anything else?", {"fields": ["anything_else"]}),
( (
"Requirements for operating .gov domains", "Requirements for operating a .gov domain",
{"fields": ["is_policy_acknowledged"]}, {"fields": ["is_policy_acknowledged"]},
), ),
] ]
@ -1239,6 +1239,29 @@ class DraftDomainAdmin(ListHeaderAdmin):
search_help_text = "Search by draft domain name." search_help_text = "Search by draft domain name."
class VeryImportantPersonAdmin(ListHeaderAdmin):
list_display = ("email", "requestor", "truncated_notes", "created_at")
search_fields = ["email"]
search_help_text = "Search by email."
list_filter = [
"requestor",
]
readonly_fields = [
"requestor",
]
def truncated_notes(self, obj):
# Truncate the 'notes' field to 50 characters
return str(obj.notes)[:50]
truncated_notes.short_description = "Notes (Truncated)" # type: ignore
def save_model(self, request, obj, form, change):
# Set the user field to the current admin user
obj.requestor = request.user if request.user.is_authenticated else None
super().save_model(request, obj, form, change)
admin.site.unregister(LogEntry) # Unregister the default registration admin.site.unregister(LogEntry) # Unregister the default registration
admin.site.register(LogEntry, CustomLogEntryAdmin) admin.site.register(LogEntry, CustomLogEntryAdmin)
admin.site.register(models.User, MyUserAdmin) admin.site.register(models.User, MyUserAdmin)
@ -1259,3 +1282,4 @@ admin.site.register(models.Website, WebsiteAdmin)
admin.site.register(models.PublicContact, AuditedAdmin) admin.site.register(models.PublicContact, AuditedAdmin)
admin.site.register(models.DomainApplication, DomainApplicationAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin)
admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin)

View file

@ -660,7 +660,6 @@ ALLOWED_HOSTS = [
"getgov-rb.app.cloud.gov", "getgov-rb.app.cloud.gov",
"getgov-ko.app.cloud.gov", "getgov-ko.app.cloud.gov",
"getgov-ab.app.cloud.gov", "getgov-ab.app.cloud.gov",
"getgov-bl.app.cloud.gov",
"getgov-rjm.app.cloud.gov", "getgov-rjm.app.cloud.gov",
"getgov-dk.app.cloud.gov", "getgov-dk.app.cloud.gov",
"manage.get.gov", "manage.get.gov",

View file

@ -838,8 +838,8 @@ class AnythingElseForm(RegistrarForm):
class RequirementsForm(RegistrarForm): class RequirementsForm(RegistrarForm):
is_policy_acknowledged = forms.BooleanField( is_policy_acknowledged = forms.BooleanField(
label="I read and agree to the requirements for operating .gov domains.", label="I read and agree to the requirements for operating a .gov domain.",
error_messages={ error_messages={
"required": ("Check the box if you read and agree to the requirements for operating .gov domains.") "required": ("Check the box if you read and agree to the requirements for operating a .gov domain.")
}, },
) )

View file

@ -0,0 +1,37 @@
# Generated by Django 4.2.7 on 2024-01-23 23:51
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0062_alter_host_name"),
]
operations = [
migrations.CreateModel(
name="VeryImportantPerson",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("email", models.EmailField(db_index=True, help_text="Email", max_length=254)),
("notes", models.TextField(help_text="Notes")),
(
"requestor",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="verifiedby_user",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 4.2.7 on 2024-01-23 22:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0063_veryimportantperson"),
]
operations = [
migrations.AlterField(
model_name="domainapplication",
name="address_line1",
field=models.TextField(blank=True, help_text="Street address", null=True, verbose_name="Address line 1"),
),
migrations.AlterField(
model_name="domainapplication",
name="address_line2",
field=models.TextField(
blank=True, help_text="Street address line 2 (optional)", null=True, verbose_name="Address line 2"
),
),
]

View file

@ -13,6 +13,7 @@ from .user import User
from .user_group import UserGroup from .user_group import UserGroup
from .website import Website from .website import Website
from .transition_domain import TransitionDomain from .transition_domain import TransitionDomain
from .very_important_person import VeryImportantPerson
__all__ = [ __all__ = [
"Contact", "Contact",
@ -29,6 +30,7 @@ __all__ = [
"UserGroup", "UserGroup",
"Website", "Website",
"TransitionDomain", "TransitionDomain",
"VeryImportantPerson",
] ]
auditlog.register(Contact) auditlog.register(Contact)
@ -45,3 +47,4 @@ auditlog.register(User, m2m_fields=["user_permissions", "groups"])
auditlog.register(UserGroup, m2m_fields=["permissions"]) auditlog.register(UserGroup, m2m_fields=["permissions"])
auditlog.register(Website) auditlog.register(Website)
auditlog.register(TransitionDomain) auditlog.register(TransitionDomain)
auditlog.register(VeryImportantPerson)

View file

@ -431,11 +431,13 @@ class DomainApplication(TimeStampedModel):
null=True, null=True,
blank=True, blank=True,
help_text="Street address", help_text="Street address",
verbose_name="Address line 1",
) )
address_line2 = models.TextField( address_line2 = models.TextField(
null=True, null=True,
blank=True, blank=True,
help_text="Street address line 2 (optional)", help_text="Street address line 2 (optional)",
verbose_name="Address line 2",
) )
city = models.TextField( city = models.TextField(
null=True, null=True,

View file

@ -7,6 +7,7 @@ from registrar.models.user_domain_role import UserDomainRole
from .domain_invitation import DomainInvitation from .domain_invitation import DomainInvitation
from .transition_domain import TransitionDomain from .transition_domain import TransitionDomain
from .very_important_person import VeryImportantPerson
from .domain import Domain from .domain import Domain
from phonenumber_field.modelfields import PhoneNumberField # type: ignore from phonenumber_field.modelfields import PhoneNumberField # type: ignore
@ -89,6 +90,10 @@ class User(AbstractUser):
if TransitionDomain.objects.filter(username=email).exists(): if TransitionDomain.objects.filter(username=email).exists():
return False return False
# New users flagged by Staff to bypass ial2
if VeryImportantPerson.objects.filter(email=email).exists():
return False
# A new incoming user who is being invited to be a domain manager (that is, # A new incoming user who is being invited to be a domain manager (that is,
# their email address is in DomainInvitation for an invitation that is not yet "retrieved"). # their email address is in DomainInvitation for an invitation that is not yet "retrieved").
invited = DomainInvitation.DomainInvitationStatus.INVITED invited = DomainInvitation.DomainInvitationStatus.INVITED

View file

@ -0,0 +1,32 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
class VeryImportantPerson(TimeStampedModel):
"""emails that get added to this table will bypass ial2 on login."""
email = models.EmailField(
null=False,
blank=False,
help_text="Email",
db_index=True,
)
requestor = models.ForeignKey(
"registrar.User",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="verifiedby_user",
)
notes = models.TextField(
null=False,
blank=False,
help_text="Notes",
)
def __str__(self):
return self.email

View file

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static form_helpers url_helpers %} {% load static form_helpers url_helpers %}
{% block title %}Apply for a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %} {% block title %}Request a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %}
{% block content %} {% block content %}
<div class="grid-container"> <div class="grid-container">
<div class="grid-row grid-gap"> <div class="grid-row grid-gap">

View file

@ -2,7 +2,7 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.</p> <p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating a .gov domain.</p>
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we dont review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p> <p>The .gov domain space exists to support a broad diversity of government missions. Generally, we dont review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>

View file

@ -10,7 +10,7 @@
{% if widget.attrs.required %} {% if widget.attrs.required %}
<!--Don't add asterisk to one-field forms --> <!--Don't add asterisk to one-field forms -->
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating .gov domains." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %} {% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
{% else %} {% else %}
<abbr class="usa-hint usa-hint--required" title="required">*</abbr> <abbr class="usa-hint usa-hint--required" title="required">*</abbr>
{% endif %} {% endif %}

View file

@ -1,7 +1,7 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi. Hi.
{{ requester_email }} has added you as a manager on {{ domain.name }}. {{ requestor_email }} has added you as a manager on {{ domain.name }}.
You can manage this domain on the .gov registrar <https://manage.get.gov>. You can manage this domain on the .gov registrar <https://manage.get.gov>.

View file

@ -14,9 +14,11 @@ from registrar.admin import (
ContactAdmin, ContactAdmin,
DomainInformationAdmin, DomainInformationAdmin,
UserDomainRoleAdmin, UserDomainRoleAdmin,
VeryImportantPersonAdmin,
) )
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from registrar.models.very_important_person import VeryImportantPerson
from .common import ( from .common import (
MockSESClient, MockSESClient,
AuditedAdminMockData, AuditedAdminMockData,
@ -1737,3 +1739,28 @@ class ContactAdminTest(TestCase):
def tearDown(self): def tearDown(self):
User.objects.all().delete() User.objects.all().delete()
class VeryImportantPersonAdminTestCase(TestCase):
def setUp(self):
self.superuser = create_superuser()
self.factory = RequestFactory()
def test_save_model_sets_user_field(self):
self.client.force_login(self.superuser)
# Create an instance of the admin class
admin_instance = VeryImportantPersonAdmin(model=VeryImportantPerson, admin_site=None)
# Create a VeryImportantPerson instance
vip_instance = VeryImportantPerson(email="test@example.com", notes="Test Notes")
# Create a request object
request = self.factory.post("/admin/yourapp/veryimportantperson/add/")
request.user = self.superuser
# Call the save_model method
admin_instance.save_model(request, vip_instance, None, None)
# Check that the user field is set to the request.user
self.assertEqual(vip_instance.requestor, self.superuser)

View file

@ -338,7 +338,7 @@ class TestFormValidation(MockEppLib):
form = RequirementsForm(data={}) form = RequirementsForm(data={})
self.assertEqual( self.assertEqual(
form.errors["is_policy_acknowledged"], form.errors["is_policy_acknowledged"],
["Check the box if you read and agree to the requirements for operating .gov domains."], ["Check the box if you read and agree to the requirements for operating a .gov domain."],
) )
def test_requirements_form_unchecked(self): def test_requirements_form_unchecked(self):
@ -346,7 +346,7 @@ class TestFormValidation(MockEppLib):
form = RequirementsForm(data={"is_policy_acknowledged": False}) form = RequirementsForm(data={"is_policy_acknowledged": False})
self.assertEqual( self.assertEqual(
form.errors["is_policy_acknowledged"], form.errors["is_policy_acknowledged"],
["Check the box if you read and agree to the requirements for operating .gov domains."], ["Check the box if you read and agree to the requirements for operating a .gov domain."],
) )
def test_tribal_government_unrecognized(self): def test_tribal_government_unrecognized(self):

View file

@ -15,7 +15,8 @@ from registrar.models import (
) )
import boto3_mocking import boto3_mocking
from registrar.models.transition_domain import TransitionDomain # type: ignore from registrar.models.transition_domain import TransitionDomain
from registrar.models.very_important_person import VeryImportantPerson # type: ignore
from .common import MockSESClient, less_console_noise, completed_application from .common import MockSESClient, less_console_noise, completed_application
from django_fsm import TransitionNotAllowed from django_fsm import TransitionNotAllowed
@ -652,6 +653,12 @@ class TestUser(TestCase):
TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name) TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name)
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username)) self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
def test_identity_verification_with_very_important_person(self):
"""A Very Important Person should return False
when tested with class method needs_identity_verification"""
VeryImportantPerson.objects.get_or_create(email=self.user.email)
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
def test_identity_verification_with_invited_user(self): def test_identity_verification_with_invited_user(self):
"""An invited user should return False when tested with class """An invited user should return False when tested with class
method needs_identity_verification""" method needs_identity_verification"""

View file

@ -2908,7 +2908,7 @@ class TestDomainManagers(TestDomainOverview):
) )
@boto3_mocking.patching @boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester_non_existent(self): def test_domain_invitation_email_has_email_as_requestor_non_existent(self):
"""Inviting a non existent user sends them an email, with email as the name.""" """Inviting a non existent user sends them an email, with email as the name."""
# make sure there is no user with this email # make sure there is no user with this email
email_address = "mayor@igorville.gov" email_address = "mayor@igorville.gov"
@ -2941,13 +2941,13 @@ class TestDomainManagers(TestDomainOverview):
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("info@example.com", email_content) self.assertIn("info@example.com", email_content)
# Check that the requesters first/last name do not exist # Check that the requestors first/last name do not exist
self.assertNotIn("First", email_content) self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content) self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content) self.assertNotIn("First Last", email_content)
@boto3_mocking.patching @boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester(self): def test_domain_invitation_email_has_email_as_requestor(self):
"""Inviting a user sends them an email, with email as the name.""" """Inviting a user sends them an email, with email as the name."""
# Create a fake user object # Create a fake user object
email_address = "mayor@igorville.gov" email_address = "mayor@igorville.gov"
@ -2980,13 +2980,13 @@ class TestDomainManagers(TestDomainOverview):
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("info@example.com", email_content) self.assertIn("info@example.com", email_content)
# Check that the requesters first/last name do not exist # Check that the requestors first/last name do not exist
self.assertNotIn("First", email_content) self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content) self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content) self.assertNotIn("First Last", email_content)
@boto3_mocking.patching @boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester_staff(self): def test_domain_invitation_email_has_email_as_requestor_staff(self):
"""Inviting a user sends them an email, with email as the name.""" """Inviting a user sends them an email, with email as the name."""
# Create a fake user object # Create a fake user object
email_address = "mayor@igorville.gov" email_address = "mayor@igorville.gov"
@ -3023,7 +3023,7 @@ class TestDomainManagers(TestDomainOverview):
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("help@get.gov", email_content) self.assertIn("help@get.gov", email_content)
# Check that the requesters first/last name do not exist # Check that the requestors first/last name do not exist
self.assertNotIn("First", email_content) self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content) self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content) self.assertNotIn("First Last", email_content)

View file

@ -92,7 +92,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
Step.YOUR_CONTACT: _("Your contact information"), Step.YOUR_CONTACT: _("Your contact information"),
Step.OTHER_CONTACTS: _("Other employees from your organization"), Step.OTHER_CONTACTS: _("Other employees from your organization"),
Step.ANYTHING_ELSE: _("Anything else?"), Step.ANYTHING_ELSE: _("Anything else?"),
Step.REQUIREMENTS: _("Requirements for operating .gov domains"), Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
Step.REVIEW: _("Review and submit your domain request"), Step.REVIEW: _("Review and submit your domain request"),
} }

View file

@ -711,7 +711,7 @@ class DomainAddUserView(DomainFormBaseView):
"""Get an absolute URL for this domain.""" """Get an absolute URL for this domain."""
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id})) return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
def _send_domain_invitation_email(self, email: str, requester: User, add_success=True): def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True):
"""Performs the sending of the domain invitation email, """Performs the sending of the domain invitation email,
does not make a domain information object does not make a domain information object
email: string- email to send to email: string- email to send to
@ -719,16 +719,16 @@ class DomainAddUserView(DomainFormBaseView):
adding a success message to the view if the email sending succeeds""" adding a success message to the view if the email sending succeeds"""
# Set a default email address to send to for staff # Set a default email address to send to for staff
requester_email = "help@get.gov" requestor_email = "help@get.gov"
# Check if the email requester has a valid email address # Check if the email requestor has a valid email address
if not requester.is_staff and requester.email is not None and requester.email.strip() != "": if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
requester_email = requester.email requestor_email = requestor.email
elif not requester.is_staff: elif not requestor.is_staff:
messages.error(self.request, "Can't send invitation email. No email is associated with your account.") messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
logger.error( logger.error(
f"Can't send email to '{email}' on domain '{self.object}'." f"Can't send email to '{email}' on domain '{self.object}'."
f"No email exists for the requester '{requester.username}'.", f"No email exists for the requestor '{requestor.username}'.",
exc_info=True, exc_info=True,
) )
return None return None
@ -741,7 +741,7 @@ class DomainAddUserView(DomainFormBaseView):
context={ context={
"domain_url": self._domain_abs_url(), "domain_url": self._domain_abs_url(),
"domain": self.object, "domain": self.object,
"requester_email": requester_email, "requestor_email": requestor_email,
}, },
) )
except EmailSendingError: except EmailSendingError:
@ -756,7 +756,7 @@ class DomainAddUserView(DomainFormBaseView):
if add_success: if add_success:
messages.success(self.request, f"{email} has been invited to this domain.") messages.success(self.request, f"{email} has been invited to this domain.")
def _make_invitation(self, email_address: str, requester: User): def _make_invitation(self, email_address: str, requestor: User):
"""Make a Domain invitation for this email and redirect with a message.""" """Make a Domain invitation for this email and redirect with a message."""
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object) invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
if not created: if not created:
@ -766,22 +766,22 @@ class DomainAddUserView(DomainFormBaseView):
f"{email_address} has already been invited to this domain.", f"{email_address} has already been invited to this domain.",
) )
else: else:
self._send_domain_invitation_email(email=email_address, requester=requester) self._send_domain_invitation_email(email=email_address, requestor=requestor)
return redirect(self.get_success_url()) return redirect(self.get_success_url())
def form_valid(self, form): def form_valid(self, form):
"""Add the specified user on this domain.""" """Add the specified user on this domain."""
requested_email = form.cleaned_data["email"] requested_email = form.cleaned_data["email"]
requester = self.request.user requestor = self.request.user
# look up a user with that email # look up a user with that email
try: try:
requested_user = User.objects.get(email=requested_email) requested_user = User.objects.get(email=requested_email)
except User.DoesNotExist: except User.DoesNotExist:
# no matching user, go make an invitation # no matching user, go make an invitation
return self._make_invitation(requested_email, requester) return self._make_invitation(requested_email, requestor)
else: else:
# if user already exists then just send an email # if user already exists then just send an email
self._send_domain_invitation_email(requested_email, requester, add_success=False) self._send_domain_invitation_email(requested_email, requestor, add_success=False)
try: try:
UserDomainRole.objects.create( UserDomainRole.objects.create(