Merge branch 'main' into za/1676-require-investigator-da

This commit is contained in:
zandercymatics 2024-02-27 08:43:30 -07:00
commit ef240c59d6
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
15 changed files with 1721 additions and 1002 deletions

View file

@ -3,7 +3,7 @@ import logging
from django import forms
from django.db.models.functions import Concat, Coalesce
from django.db.models import Value, CharField
from django.db.models import Value, CharField, Q
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions
@ -25,7 +25,7 @@ from auditlog.admin import LogEntryAdmin # type: ignore
from django_fsm import TransitionNotAllowed # type: ignore
from django.utils.safestring import mark_safe
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
@ -927,11 +927,35 @@ class DomainApplicationAdmin(ListHeaderAdmin):
else:
return queryset.filter(investigator__id__exact=self.value())
class ElectionOfficeFilter(admin.SimpleListFilter):
"""Define a custom filter for is_election_board"""
title = _("election office")
parameter_name = "is_election_board"
def lookups(self, request, model_admin):
return (
("1", _("Yes")),
("0", _("No")),
)
def queryset(self, request, queryset):
if self.value() == "1":
return queryset.filter(is_election_board=True)
if self.value() == "0":
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
# Columns
list_display = [
"requested_domain",
"status",
"organization_type",
"federal_type",
"federal_agency",
"organization_name",
"custom_election_board",
"city",
"state_territory",
"created_at",
"submitter",
"investigator",
@ -943,8 +967,21 @@ class DomainApplicationAdmin(ListHeaderAdmin):
("investigator", ["first_name", "last_name"]),
]
def custom_election_board(self, obj):
return "Yes" if obj.is_election_board else "No"
custom_election_board.admin_order_field = "is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore
# Filters
list_filter = ("status", "organization_type", InvestigatorFilter)
list_filter = (
"status",
"organization_type",
"federal_type",
ElectionOfficeFilter,
"rejection_reason",
InvestigatorFilter,
)
# Search
search_fields = [
@ -958,7 +995,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Detail view
form = DomainApplicationAdminForm
fieldsets = [
(None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}),
(None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}),
(
"Type of organization",
{
@ -1073,6 +1110,22 @@ class DomainApplicationAdmin(ListHeaderAdmin):
request,
"This action is not permitted. The domain is already active.",
)
elif (
obj
and obj.status == models.DomainApplication.ApplicationStatus.REJECTED
and not obj.rejection_reason
):
# This condition should never be triggered.
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
# because we clean up the rejection reason in the transition in the model.
# Clear the success message
messages.set_level(request, messages.ERROR)
messages.error(
request,
"A rejection reason is required.",
)
else:
if obj.status != original_obj.status:
@ -1217,12 +1270,37 @@ class DomainInformationInline(admin.StackedInline):
class DomainAdmin(ListHeaderAdmin):
"""Custom domain admin class to add extra buttons."""
class ElectionOfficeFilter(admin.SimpleListFilter):
"""Define a custom filter for is_election_board"""
title = _("election office")
parameter_name = "is_election_board"
def lookups(self, request, model_admin):
return (
("1", _("Yes")),
("0", _("No")),
)
def queryset(self, request, queryset):
logger.debug(self.value())
if self.value() == "1":
return queryset.filter(domain_info__is_election_board=True)
if self.value() == "0":
return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None))
inlines = [DomainInformationInline]
# Columns
list_display = [
"name",
"organization_type",
"federal_type",
"federal_agency",
"organization_name",
"custom_election_board",
"city",
"state_territory",
"state",
"expiration_date",
"created_at",
@ -1246,8 +1324,42 @@ class DomainAdmin(ListHeaderAdmin):
organization_type.admin_order_field = "domain_info__organization_type" # type: ignore
def federal_agency(self, obj):
return obj.domain_info.federal_agency if obj.domain_info else None
federal_agency.admin_order_field = "domain_info__federal_agency" # type: ignore
def federal_type(self, obj):
return obj.domain_info.federal_type if obj.domain_info else None
federal_type.admin_order_field = "domain_info__federal_type" # type: ignore
def organization_name(self, obj):
return obj.domain_info.organization_name if obj.domain_info else None
organization_name.admin_order_field = "domain_info__organization_name" # type: ignore
def custom_election_board(self, obj):
domain_info = getattr(obj, "domain_info", None)
if domain_info:
return "Yes" if domain_info.is_election_board else "No"
return "No"
custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore
def city(self, obj):
return obj.domain_info.city if obj.domain_info else None
city.admin_order_field = "domain_info__city" # type: ignore
def state_territory(self, obj):
return obj.domain_info.state_territory if obj.domain_info else None
state_territory.admin_order_field = "domain_info__state_territory" # type: ignore
# Filters
list_filter = ["domain_info__organization_type", "state"]
list_filter = ["domain_info__organization_type", "domain_info__federal_type", ElectionOfficeFilter, "state"]
search_fields = ["name"]
search_help_text = "Search by domain name."

View file

@ -339,3 +339,46 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
}
})();
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
* status select amd to show/hide the rejection reason
*/
(function (){
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
if (rejectionReasonFormGroup) {
let statusSelect = document.getElementById('id_status')
// Initial handling of rejectionReasonFormGroup display
if (statusSelect.value != 'rejected')
rejectionReasonFormGroup.style.display = 'none';
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
statusSelect.addEventListener('change', function() {
if (statusSelect.value == 'rejected') {
rejectionReasonFormGroup.style.display = 'block';
sessionStorage.removeItem('hideRejectionReason');
} else {
rejectionReasonFormGroup.style.display = 'none';
sessionStorage.setItem('hideRejectionReason', 'true');
}
});
}
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
// accurately for this edge case, we use cache and test for the back/forward navigation.
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.type === "back_forward") {
if (sessionStorage.getItem('hideRejectionReason'))
document.querySelector('.field-rejection_reason').style.display = 'none';
else
document.querySelector('.field-rejection_reason').style.display = 'block';
}
});
});
observer.observe({ type: "navigation" });
})();

View file

@ -286,6 +286,7 @@ AWS_MAX_ATTEMPTS = 3
BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS})
# email address to use for various automated correspondence
# also used as a default to and bcc email
DEFAULT_FROM_EMAIL = "help@get.gov <help@get.gov>"
# connect to an (external) SMTP server for sending email

View file

@ -284,6 +284,7 @@ class OrganizationContactForm(RegistrarForm):
message="Enter a zip code in the form of 12345 or 12345-6789.",
)
],
error_messages={"required": ("Enter a zip code in the form of 12345 or 12345-6789.")},
)
urbanization = forms.CharField(
required=False,

View file

@ -0,0 +1,29 @@
# Generated by Django 4.2.7 on 2024-02-26 22:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0069_alter_contact_email_alter_contact_first_name_and_more"),
]
operations = [
migrations.AddField(
model_name="domainapplication",
name="rejection_reason",
field=models.TextField(
blank=True,
choices=[
("purpose_not_met", "Purpose requirements not met"),
("requestor_not_eligible", "Requestor not eligible to make request"),
("org_has_domain", "Org already has a .gov domain"),
("contacts_not_verified", "Org contacts couldn't be verified"),
("org_not_eligible", "Org not eligible for a .gov domain"),
("naming_not_met", "Naming requirements not met"),
("other", "Other/Unspecified"),
],
null=True,
),
),
]

View file

@ -4,6 +4,7 @@ from typing import Union
import logging
from django.apps import apps
from django.conf import settings
from django.db import models
from django_fsm import FSMField, transition # type: ignore
from django.utils import timezone
@ -351,12 +352,34 @@ class DomainApplication(TimeStampedModel):
]
AGENCY_CHOICES = [(v, v) for v in AGENCIES]
class RejectionReasons(models.TextChoices):
DOMAIN_PURPOSE = "purpose_not_met", "Purpose requirements not met"
REQUESTOR = "requestor_not_eligible", "Requestor not eligible to make request"
SECOND_DOMAIN_REASONING = (
"org_has_domain",
"Org already has a .gov domain",
)
CONTACTS_OR_ORGANIZATION_LEGITIMACY = (
"contacts_not_verified",
"Org contacts couldn't be verified",
)
ORGANIZATION_ELIGIBILITY = "org_not_eligible", "Org not eligible for a .gov domain"
NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met"
OTHER = "other", "Other/Unspecified"
# #### Internal fields about the application #####
status = FSMField(
choices=ApplicationStatus.choices, # possible states as an array of constants
default=ApplicationStatus.STARTED, # sensible default
protected=False, # can change state directly, particularly in Django admin
)
rejection_reason = models.TextField(
choices=RejectionReasons.choices,
null=True,
blank=True,
)
# This is the application user who created this application. The contact
# information that they gave is in the `submitter` field
creator = models.ForeignKey(
@ -364,6 +387,7 @@ class DomainApplication(TimeStampedModel):
on_delete=models.PROTECT,
related_name="applications_created",
)
investigator = models.ForeignKey(
"registrar.User",
null=True,
@ -589,7 +613,9 @@ class DomainApplication(TimeStampedModel):
logger.error(err)
logger.error(f"Can't query an approved domain while attempting {called_from}")
def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True):
def _send_status_update_email(
self, new_status, email_template, email_template_subject, send_email=True, bcc_address=""
):
"""Send a status update email to the submitter.
The email goes to the email address that the submitter gave as their
@ -614,6 +640,7 @@ class DomainApplication(TimeStampedModel):
email_template_subject,
self.submitter.email,
context={"application": self},
bcc_address=bcc_address,
)
logger.info(f"The {new_status} email sent to: {self.submitter.email}")
except EmailSendingError:
@ -660,11 +687,17 @@ class DomainApplication(TimeStampedModel):
# Limit email notifications to transitions from Started and Withdrawn
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
bcc_address = ""
if settings.IS_PRODUCTION:
bcc_address = settings.DEFAULT_FROM_EMAIL
if self.status in limited_statuses:
self._send_status_update_email(
"submission confirmation",
"emails/submission_confirmation.txt",
"emails/submission_confirmation_subject.txt",
True,
bcc_address,
)
@transition(
@ -684,12 +717,17 @@ class DomainApplication(TimeStampedModel):
This action is logged.
This action cleans up the rejection status if moving away from rejected.
As side effects this will delete the domain and domain_information
(will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED:
self.delete_and_clean_up_domain("in_review")
if self.status == self.ApplicationStatus.REJECTED:
self.rejection_reason = None
literal = DomainApplication.ApplicationStatus.IN_REVIEW
# Check if the tuple exists, then grab its value
in_review = literal if literal is not None else "In Review"
@ -711,12 +749,17 @@ class DomainApplication(TimeStampedModel):
This action is logged.
This action cleans up the rejection status if moving away from rejected.
As side effects this will delete the domain and domain_information
(will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED:
self.delete_and_clean_up_domain("reject_with_prejudice")
if self.status == self.ApplicationStatus.REJECTED:
self.rejection_reason = None
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
# Check if the tuple is setup correctly, then grab its value
action_needed = literal if literal is not None else "Action Needed"
@ -736,6 +779,8 @@ class DomainApplication(TimeStampedModel):
def approve(self, send_email=True):
"""Approve an application that has been submitted.
This action cleans up the rejection status if moving away from rejected.
This has substantial side-effects because it creates another database
object for the approved Domain and makes the user who created the
application into an admin on that domain. It also triggers an email
@ -762,6 +807,9 @@ class DomainApplication(TimeStampedModel):
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER
)
if self.status == self.ApplicationStatus.REJECTED:
self.rejection_reason = None
# == Send out an email == #
self._send_status_update_email(
"application approved",

View file

@ -15,7 +15,15 @@
{% if filters %}
filtered by
{% for filter_param in filters %}
{% if filter_param.parameter_name == 'is_election_board' %}
{%if filter_param.parameter_value == '0' %}
election office = No
{% else %}
election office = Yes
{% endif %}
{% else %}
{{ filter_param.parameter_name }} = {{ filter_param.parameter_value }}
{% endif %}
{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endif %}

View file

@ -8,7 +8,56 @@ REQUEST RECEIVED ON: {{ application.submission_date|date }}
STATUS: Rejected
----------------------------------------------------------------
{% if application.rejection_reason != 'other' %}
REJECTION REASON{% endif %}{% if application.rejection_reason == 'purpose_not_met' %}
Your domain request was rejected because the purpose you provided did not meet our
requirements. You didnt provide enough information about how you intend to use the
domain.
Learn more about:
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
- What you can and cant do with .gov domains <https://get.gov/domains/requirements/>
If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'requestor_not_eligible' %}
Your domain request was rejected because we dont believe youre eligible to request a
.gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be
working on behalf of a government organization, to request a .gov domain.
DEMONSTRATE ELIGIBILITY
If you can provide more information that demonstrates your eligibility, or you want to
discuss further, reply to this email.{% elif application.rejection_reason == 'org_has_domain' %}
Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our
practice is to approve one domain per online service per government organization. We
evaluate additional requests on a case-by-case basis. You did not provide sufficient
justification for an additional domain.
Read more about our practice of approving one domain per online service
<https://get.gov/domains/before/#one-domain-per-service>.
If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'contacts_not_verified' %}
Your domain request was rejected because we could not verify the organizational
contacts you provided. If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'org_not_eligible' %}
Your domain request was rejected because we determined that {{ application.organization_name }} is not
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
government organizations.
DEMONSTRATE ELIGIBILITY
If you can provide documentation that demonstrates your eligibility, reply to this email.
This can include links to (or copies of) your authorizing legislation, your founding
charter or bylaws, or other similar documentation. Without this, we cant approve a
.gov domain for your organization. Learn more about eligibility for .gov domains
<https://get.gov/domains/eligibility/>.{% elif application.rejection_reason == 'naming_not_met' %}
Your domain request was rejected because it does not meet our naming requirements.
Domains should uniquely identify a government organization and be clear to the
general public. Learn more about naming requirements for your type of organization
<https://get.gov/domains/choosing/>.
YOU CAN SUBMIT A NEW REQUEST
We encourage you to request a domain that meets our requirements. If you have
questions or want to discuss potential domain names, reply to this email.{% elif application.rejection_reason == 'other' %}
YOU CAN SUBMIT A NEW REQUEST
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
@ -19,7 +68,7 @@ Learn more about:
NEED ASSISTANCE?
If you have questions about this domain request or need help choosing a new domain name, reply to this email.
{% endif %}
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.

View file

@ -1,5 +1,5 @@
from datetime import date
from django.test import TestCase, RequestFactory, Client
from django.test import TestCase, RequestFactory, Client, override_settings
from django.contrib.admin.sites import AdminSite
from contextlib import ExitStack
from django_webtest import WebTest # type: ignore
@ -243,9 +243,9 @@ class TestDomainAdmin(MockEppLib, WebTest):
response = self.client.get("/admin/registrar/domain/")
# There are 3 template references to Federal (3) plus one reference in the table
# There are 4 template references to Federal (4) plus four references in the table
# for our actual application
self.assertContains(response, "Federal", count=4)
self.assertContains(response, "Federal", count=8)
# This may be a bit more robust
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist
@ -445,6 +445,7 @@ class TestDomainApplicationAdminForm(TestCase):
self.application = completed_application()
def test_form_choices(self):
with less_console_noise():
# Create a form instance with the test application
form = DomainApplicationAdminForm(instance=self.application)
@ -453,6 +454,7 @@ class TestDomainApplicationAdminForm(TestCase):
self.assertEqual(form.fields["status"].widget.choices, expected_choices)
def test_form_choices_when_no_instance(self):
with less_console_noise():
# Create a form instance without an instance
form = DomainApplicationAdminForm()
@ -467,6 +469,7 @@ class TestDomainApplicationAdminForm(TestCase):
)
def test_form_choices_when_ineligible(self):
with less_console_noise():
# Create a form instance with a domain application with ineligible status
ineligible_application = DomainApplication(status="ineligible")
@ -502,6 +505,7 @@ class TestDomainApplicationAdmin(MockEppLib):
def test_domain_sortable(self):
"""Tests if the DomainApplication sorts by domain correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -515,6 +519,7 @@ class TestDomainApplicationAdmin(MockEppLib):
def test_submitter_sortable(self):
"""Tests if the DomainApplication sorts by domain correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -527,7 +532,7 @@ class TestDomainApplicationAdmin(MockEppLib):
# Assert that our sort works correctly
self.test_helper.assert_table_sorted(
"5",
"11",
(
"submitter__first_name",
"submitter__last_name",
@ -536,7 +541,7 @@ class TestDomainApplicationAdmin(MockEppLib):
# Assert that sorting in reverse works correctly
self.test_helper.assert_table_sorted(
"-5",
"-11",
(
"-submitter__first_name",
"-submitter__last_name",
@ -545,6 +550,7 @@ class TestDomainApplicationAdmin(MockEppLib):
def test_investigator_sortable(self):
"""Tests if the DomainApplication sorts by domain correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -576,18 +582,19 @@ class TestDomainApplicationAdmin(MockEppLib):
"""
Make sure the short name is displaying in admin on the list page
"""
with less_console_noise():
self.client.force_login(self.superuser)
completed_application()
response = self.client.get("/admin/registrar/domainapplication/")
# There are 3 template references to Federal (3) plus one reference in the table
# There are 4 template references to Federal (4) plus two references in the table
# for our actual application
self.assertContains(response, "Federal", count=4)
self.assertContains(response, "Federal", count=6)
# This may be a bit more robust
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist
self.assertNotContains(response, "Federal: an agency of the U.S. government")
def transition_state_and_send_email(self, application, status):
def transition_state_and_send_email(self, application, status, rejection_reason=None):
"""Helper method for the email test cases."""
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
@ -595,16 +602,20 @@ class TestDomainApplicationAdmin(MockEppLib):
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
# Modify the application's properties
application.status = status
application.rejection_reason = rejection_reason
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
def assert_email_is_accurate(self, expected_string, email_index, email_address):
def assert_email_is_accurate(
self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address=""
):
"""Helper method for the email test cases.
email_index is the index of the email in mock_client."""
with less_console_noise():
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[email_index]["kwargs"]
@ -620,13 +631,28 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(to_email, email_address)
self.assertIn(expected_string, email_body)
if test_that_no_bcc:
_ = ""
with self.assertRaises(KeyError):
with less_console_noise():
_ = kwargs["Destination"]["BccAddresses"][0]
self.assertEqual(_, "")
if bcc_email_address:
bcc_email = kwargs["Destination"]["BccAddresses"][0]
self.assertEqual(bcc_email, bcc_email_address)
def test_save_model_sends_submitted_email(self):
"""When transitioning to submitted from started or withdrawn on a domain request,
an email is sent out.
When transitioning to submitted from dns needed or in review on a domain request,
no email is sent out."""
no email is sent out.
Also test that the default email set in settings is NOT BCCd on non-prod whenever
an email does go out."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
@ -636,13 +662,13 @@ class TestDomainApplicationAdmin(MockEppLib):
# Test Submitted Status from started
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL, True
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@ -670,10 +696,69 @@ class TestDomainApplicationAdmin(MockEppLib):
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
@override_settings(IS_PRODUCTION=True)
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
"""When transitioning to submitted from started or withdrawn on a domain request,
an email is sent out.
When transitioning to submitted from dns needed or in review on a domain request,
no email is sent out.
Also test that the default email set in settings IS BCCd on prod whenever
an email does go out."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
# Create a sample application
application = completed_application()
# Test Submitted Status from started
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (from withdrawn)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test Submitted Status Again from in IN_REVIEW, no new email should be sent
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to ACTION_NEEDED
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_approved_email(self):
"""When transitioning to approved on a domain request,
an email is sent out every time."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
@ -687,7 +772,11 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.transition_state_and_send_email(
application,
DomainApplication.ApplicationStatus.REJECTED,
DomainApplication.RejectionReasons.DOMAIN_PURPOSE,
)
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@ -695,10 +784,11 @@ class TestDomainApplicationAdmin(MockEppLib):
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_rejected_email(self):
"""When transitioning to rejected on a domain request,
an email is sent out every time."""
def test_save_model_sends_rejected_email_purpose_not_met(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is domain purpose."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
@ -706,24 +796,256 @@ class TestDomainApplicationAdmin(MockEppLib):
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Test Submitted Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 0, EMAIL)
# Reject for reason DOMAIN_PURPOSE and test email
self.transition_state_and_send_email(
application,
DomainApplication.ApplicationStatus.REJECTED,
DomainApplication.RejectionReasons.DOMAIN_PURPOSE,
)
self.assert_email_is_accurate(
"Your domain request was rejected because the purpose you provided did not meet our \nrequirements.",
0,
EMAIL,
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
# Approve
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_rejected_email_requestor(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is requestor."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Reject for reason REQUESTOR and test email including dynamic organization name
self.transition_state_and_send_email(
application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR
)
self.assert_email_is_accurate(
"Your domain request was rejected because we dont believe youre eligible to request a \n.gov "
"domain on behalf of Testorg",
0,
EMAIL,
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
def test_save_model_sends_rejected_email_org_has_domain(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is second domain."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name
self.transition_state_and_send_email(
application,
DomainApplication.ApplicationStatus.REJECTED,
DomainApplication.RejectionReasons.SECOND_DOMAIN_REASONING,
)
self.assert_email_is_accurate(
"Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is contacts or org legitimacy."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name
self.transition_state_and_send_email(
application,
DomainApplication.ApplicationStatus.REJECTED,
DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY,
)
self.assert_email_is_accurate(
"Your domain request was rejected because we could not verify the organizational \n"
"contacts you provided. If you have questions or comments, reply to this email.",
0,
EMAIL,
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
def test_save_model_sends_rejected_email_org_eligibility(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is org eligibility."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name
self.transition_state_and_send_email(
application,
DomainApplication.ApplicationStatus.REJECTED,
DomainApplication.RejectionReasons.ORGANIZATION_ELIGIBILITY,
)
self.assert_email_is_accurate(
"Your domain request was rejected because we determined that Testorg is not \neligible for "
"a .gov domain.",
0,
EMAIL,
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
def test_save_model_sends_rejected_email_naming(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is naming."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
self.transition_state_and_send_email(
application,
DomainApplication.ApplicationStatus.REJECTED,
DomainApplication.RejectionReasons.NAMING_REQUIREMENTS,
)
self.assert_email_is_accurate(
"Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
def test_save_model_sends_rejected_email_other(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is other."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
self.transition_state_and_send_email(
application,
DomainApplication.ApplicationStatus.REJECTED,
DomainApplication.RejectionReasons.OTHER,
)
self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self):
"""
When transitioning to rejected without a rejection reason, admin throws a user friendly message.
The transition fails.
"""
with less_console_noise():
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
# Create a request object with a superuser
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
request.user = self.superuser
with ExitStack() as stack:
stack.enter_context(patch.object(messages, "error"))
application.status = DomainApplication.ApplicationStatus.REJECTED
self.admin.save_model(request, application, None, True)
messages.error.assert_called_once_with(
request,
"A rejection reason is required.",
)
application.refresh_from_db()
self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED)
def test_transition_to_rejected_with_rejection_reason_does_not_trigger_error(self):
"""
When transitioning to rejected with a rejection reason, admin does not throw an error alert.
The transition is successful.
"""
with less_console_noise():
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
# Create a request object with a superuser
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
request.user = self.superuser
with ExitStack() as stack:
stack.enter_context(patch.object(messages, "error"))
application.status = DomainApplication.ApplicationStatus.REJECTED
application.rejection_reason = DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY
self.admin.save_model(request, application, None, True)
messages.error.assert_not_called()
application.refresh_from_db()
self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED)
def test_save_model_sends_withdrawn_email(self):
"""When transitioning to withdrawn on a domain request,
an email is sent out every time."""
with less_console_noise():
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
@ -748,6 +1070,7 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sets_approved_domain(self):
with less_console_noise():
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
@ -759,7 +1082,6 @@ class TestDomainApplicationAdmin(MockEppLib):
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.APPROVED
@ -770,6 +1092,7 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(application.requested_domain.name, application.approved_domain.name)
def test_save_model_sets_restricted_status_on_user(self):
with less_console_noise():
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
@ -781,7 +1104,6 @@ class TestDomainApplicationAdmin(MockEppLib):
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
@ -792,9 +1114,9 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(application.creator.status, "restricted")
def test_readonly_when_restricted_creator(self):
with less_console_noise():
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
application.creator.status = User.RESTRICTED
application.creator.save()
@ -808,6 +1130,7 @@ class TestDomainApplicationAdmin(MockEppLib):
"created_at",
"updated_at",
"status",
"rejection_reason",
"creator",
"investigator",
"organization_type",
@ -843,6 +1166,7 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_analyst(self):
with less_console_noise():
request = self.factory.get("/") # Use the correct method and path
request.user = self.staffuser
@ -864,6 +1188,7 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_superuser(self):
with less_console_noise():
request = self.factory.get("/") # Use the correct method and path
request.user = self.superuser
@ -874,10 +1199,10 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(readonly_fields, expected_fields)
def test_saving_when_restricted_creator(self):
with less_console_noise():
# Create an instance of the model
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
application.creator.status = User.RESTRICTED
application.creator.save()
@ -899,10 +1224,10 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW)
def test_change_view_with_restricted_creator(self):
with less_console_noise():
# Create an instance of the model
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
application.creator.status = User.RESTRICTED
application.creator.save()
@ -919,13 +1244,14 @@ class TestDomainApplicationAdmin(MockEppLib):
"Cannot edit an application with a restricted creator.",
)
def trigger_saving_approved_to_another_state(self, domain_is_active, another_state):
def trigger_saving_approved_to_another_state(self, domain_is_active, another_state, rejection_reason=None):
"""Helper method that triggers domain request state changes from approved to another state,
with an associated domain that can be either active (READY) or not.
Used to test errors when saving a change with an active domain, also used to test side effects
when saving a change goes through."""
with less_console_noise():
# Create an instance of the model
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
@ -948,6 +1274,8 @@ class TestDomainApplicationAdmin(MockEppLib):
stack.enter_context(patch.object(messages, "error"))
application.status = another_state
application.rejection_reason = rejection_reason
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
@ -989,7 +1317,11 @@ class TestDomainApplicationAdmin(MockEppLib):
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED)
def test_side_effects_when_saving_approved_to_rejected(self):
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED)
self.trigger_saving_approved_to_another_state(
False,
DomainApplication.ApplicationStatus.REJECTED,
DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY,
)
def test_side_effects_when_saving_approved_to_ineligible(self):
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE)
@ -1001,12 +1333,20 @@ class TestDomainApplicationAdmin(MockEppLib):
It retrieves the current list of filters from DomainApplicationAdmin
and checks that it matches the expected list of filters.
"""
with less_console_noise():
request = self.factory.get("/")
request.user = self.superuser
# Grab the current list of table filters
readonly_fields = self.admin.get_list_filter(request)
expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter)
expected_fields = (
"status",
"organization_type",
"federal_type",
DomainApplicationAdmin.ElectionOfficeFilter,
"rejection_reason",
DomainApplicationAdmin.InvestigatorFilter,
)
self.assertEqual(readonly_fields, expected_fields)
@ -1020,6 +1360,7 @@ class TestDomainApplicationAdmin(MockEppLib):
that it matches the expected queryset,
which is sorted alphabetically by the 'requested_domain__name' field.
"""
with less_console_noise():
# Creates a list of DomainApplications in scrambled order
multiple_unalphabetical_domain_objects("application")
@ -1052,6 +1393,7 @@ class TestDomainApplicationAdmin(MockEppLib):
the filter displays correctly, when the filter isn't filtering correctly.
"""
with less_console_noise():
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
@ -1095,6 +1437,8 @@ class TestDomainApplicationAdmin(MockEppLib):
It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin
and checks that it matches the expected queryset, which only includes staff users.
"""
with less_console_noise():
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
@ -1138,6 +1482,7 @@ class TestDomainApplicationAdmin(MockEppLib):
This test verifies that filter list for the 'investigator'
is displayed alphabetically
"""
with less_console_noise():
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
@ -1200,6 +1545,7 @@ class DomainInvitationAdminTest(TestCase):
def test_get_filters(self):
"""Ensures that our filters are displaying correctly"""
with less_console_noise():
# Have to get creative to get past linter
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1278,6 +1624,7 @@ class TestDomainInformationAdmin(TestCase):
def test_readonly_fields_for_analyst(self):
"""Ensures that analysts have their permissions setup correctly"""
with less_console_noise():
request = self.factory.get("/")
request.user = self.staffuser
@ -1299,6 +1646,7 @@ class TestDomainInformationAdmin(TestCase):
def test_domain_sortable(self):
"""Tests if DomainInformation sorts by domain correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1310,6 +1658,7 @@ class TestDomainInformationAdmin(TestCase):
def test_submitter_sortable(self):
"""Tests if DomainInformation sorts by submitter correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1347,6 +1696,7 @@ class UserDomainRoleAdminTest(TestCase):
def test_domain_sortable(self):
"""Tests if the UserDomainrole sorts by domain correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1368,6 +1718,7 @@ class UserDomainRoleAdminTest(TestCase):
def test_user_sortable(self):
"""Tests if the UserDomainrole sorts by user correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1390,6 +1741,7 @@ class UserDomainRoleAdminTest(TestCase):
def test_email_not_in_search(self):
"""Tests the search bar in Django Admin for UserDomainRoleAdmin.
Should return no results for an invalid email."""
with less_console_noise():
# Have to get creative to get past linter
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1422,6 +1774,7 @@ class UserDomainRoleAdminTest(TestCase):
def test_email_in_search(self):
"""Tests the search bar in Django Admin for UserDomainRoleAdmin.
Should return results for an valid email."""
with less_console_noise():
# Have to get creative to get past linter
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1531,6 +1884,7 @@ class MyUserAdminTest(TestCase):
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
def test_list_display_without_username(self):
with less_console_noise():
request = self.client.request().wsgi_request
request.user = create_user()
@ -1547,6 +1901,7 @@ class MyUserAdminTest(TestCase):
self.assertNotIn("username", list_display)
def test_get_fieldsets_superuser(self):
with less_console_noise():
request = self.client.request().wsgi_request
request.user = create_superuser()
fieldsets = self.admin.get_fieldsets(request)
@ -1554,6 +1909,7 @@ class MyUserAdminTest(TestCase):
self.assertEqual(fieldsets, expected_fieldsets)
def test_get_fieldsets_cisa_analyst(self):
with less_console_noise():
request = self.client.request().wsgi_request
request.user = create_user()
fieldsets = self.admin.get_fieldsets(request)
@ -1576,6 +1932,7 @@ class AuditedAdminTest(TestCase):
self.client = Client(HTTP_HOST="localhost:8080")
def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names):
with less_console_noise():
formatted_sort_fields = []
for obj in obj_names:
formatted_sort_fields.append("{}__{}".format(field_name, obj))
@ -1622,6 +1979,7 @@ class AuditedAdminTest(TestCase):
# This test case should be refactored in general, as it is too overly specific and engineered
def test_alphabetically_sorted_fk_fields_domain_application(self):
with less_console_noise():
tested_fields = [
DomainApplication.authorizing_official.field,
DomainApplication.submitter.field,
@ -1679,6 +2037,7 @@ class AuditedAdminTest(TestCase):
)
def test_alphabetically_sorted_fk_fields_domain_information(self):
with less_console_noise():
tested_fields = [
DomainInformation.authorizing_official.field,
DomainInformation.submitter.field,
@ -1738,6 +2097,7 @@ class AuditedAdminTest(TestCase):
)
def test_alphabetically_sorted_fk_fields_domain_invitation(self):
with less_console_noise():
tested_fields = [DomainInvitation.domain.field]
# Creates multiple domain applications - review status does not matter
@ -1811,6 +2171,7 @@ class DomainSessionVariableTest(TestCase):
def test_session_vars_set_correctly(self):
"""Checks if session variables are being set correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1826,6 +2187,7 @@ class DomainSessionVariableTest(TestCase):
def test_session_vars_set_correctly_hardcoded_domain(self):
"""Checks if session variables are being set correctly"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1840,6 +2202,7 @@ class DomainSessionVariableTest(TestCase):
def test_session_variables_reset_correctly(self):
"""Checks if incorrect session variables get overridden"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1857,6 +2220,7 @@ class DomainSessionVariableTest(TestCase):
def test_session_variables_retain_information(self):
"""Checks to see if session variables retain old information"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1871,6 +2235,7 @@ class DomainSessionVariableTest(TestCase):
def test_session_variables_concurrent_requests(self):
"""Simulates two requests at once"""
with less_console_noise():
p = "adminpass"
self.client.login(username="superuser", password=p)
@ -1930,6 +2295,7 @@ class ContactAdminTest(TestCase):
self.staffuser = create_user()
def test_readonly_when_restricted_staffuser(self):
with less_console_noise():
request = self.factory.get("/")
request.user = self.staffuser
@ -1942,6 +2308,7 @@ class ContactAdminTest(TestCase):
self.assertEqual(readonly_fields, expected_fields)
def test_readonly_when_restricted_superuser(self):
with less_console_noise():
request = self.factory.get("/")
request.user = self.superuser
@ -1954,7 +2321,7 @@ class ContactAdminTest(TestCase):
def test_change_view_for_joined_contact_five_or_less(self):
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
Assert that the warning on the contact form lists 5 joins."""
with less_console_noise():
self.client.force_login(self.superuser)
# Create an instance of the model
@ -2036,6 +2403,7 @@ class VerifiedByStaffAdminTestCase(TestCase):
self.factory = RequestFactory()
def test_save_model_sets_user_field(self):
with less_console_noise():
self.client.force_login(self.superuser)
# Create an instance of the admin class

View file

@ -1,31 +0,0 @@
from django.test import Client, TestCase, override_settings
from django.contrib.auth import get_user_model
class MyTestCase(TestCase):
def setUp(self):
self.client = Client()
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
self.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
)
self.client.force_login(self.user)
def tearDown(self):
super().tearDown()
self.user.delete()
@override_settings(IS_PRODUCTION=True)
def test_production_environment(self):
"""No banner on prod."""
home_page = self.client.get("/")
self.assertNotContains(home_page, "You are on a test site.")
@override_settings(IS_PRODUCTION=False)
def test_non_production_environment(self):
"""Banner on non-prod."""
home_page = self.client.get("/")
self.assertContains(home_page, "You are on a test site.")

View file

@ -789,6 +789,56 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
self.approved_application.reject_with_prejudice()
def test_approve_from_rejected_clears_rejection_reason(self):
"""When transitioning from rejected to approved on a domain request,
the rejection_reason is cleared."""
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE
# Approve
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
application.approve()
self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED)
self.assertEqual(application.rejection_reason, None)
def test_in_review_from_rejected_clears_rejection_reason(self):
"""When transitioning from rejected to in_review on a domain request,
the rejection_reason is cleared."""
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
application.domain_is_not_active = True
application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE
# Approve
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
application.in_review()
self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW)
self.assertEqual(application.rejection_reason, None)
def test_action_needed_from_rejected_clears_rejection_reason(self):
"""When transitioning from rejected to action_needed on a domain request,
the rejection_reason is cleared."""
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
application.domain_is_not_active = True
application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE
# Approve
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
application.action_needed()
self.assertEqual(application.status, DomainApplication.ApplicationStatus.ACTION_NEEDED)
self.assertEqual(application.rejection_reason, None)
def test_has_rationale_returns_true(self):
"""has_rationale() returns true when an application has no_other_contacts_rationale"""
with less_console_noise():

View file

@ -321,6 +321,7 @@ class TestDomainCreation(MockEppLib):
Then a Domain exists in the database with the same `name`
But a domain object does not exist in the registry
"""
with less_console_noise():
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
investigator, _ = User.objects.get_or_create(username="frenchtoast", is_staff=True)
@ -330,7 +331,6 @@ class TestDomainCreation(MockEppLib):
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
# skip using the submit method
application.status = DomainApplication.ApplicationStatus.SUBMITTED
# transition to approve state
@ -349,6 +349,7 @@ class TestDomainCreation(MockEppLib):
And `domain.state` is set to `UNKNOWN`
And `domain.is_active()` returns False
"""
with less_console_noise():
domain = Domain.objects.create(name="beef-tongue.gov")
# trigger getter
_ = domain.statuses
@ -385,6 +386,7 @@ class TestDomainCreation(MockEppLib):
def test_minimal_creation(self):
"""Can create with just a name."""
with less_console_noise():
Domain.objects.create(name="igorville.gov")
@skip("assertion broken with mock addition")
@ -507,6 +509,7 @@ class TestDomainAvailable(MockEppLib):
res_data=[responses.check.CheckDomainResultData(name="available.gov", avail=True, reason=None)],
)
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
@ -540,6 +543,7 @@ class TestDomainAvailable(MockEppLib):
res_data=[responses.check.CheckDomainResultData(name="unavailable.gov", avail=False, reason="In Use")],
)
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
@ -565,6 +569,7 @@ class TestDomainAvailable(MockEppLib):
Validate InvalidDomainError is raised
"""
with less_console_noise():
with self.assertRaises(errors.InvalidDomainError):
Domain.available("invalid-string")
@ -575,6 +580,7 @@ class TestDomainAvailable(MockEppLib):
Validate InvalidDomainError is raised
"""
with less_console_noise():
with self.assertRaises(errors.InvalidDomainError):
Domain.available("")
@ -588,6 +594,7 @@ class TestDomainAvailable(MockEppLib):
def side_effect(_request, cleaned):
raise RegistryError(code=ErrorCode.COMMAND_SYNTAX_ERROR)
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect

View file

@ -1,4 +1,4 @@
from django.test import Client, TestCase
from django.test import Client, TestCase, override_settings
from django.contrib.auth import get_user_model
from .common import MockEppLib # type: ignore
@ -50,3 +50,32 @@ class TestWithUser(MockEppLib):
DomainApplication.objects.all().delete()
DomainInformation.objects.all().delete()
self.user.delete()
class TestEnvironmentVariablesEffects(TestCase):
def setUp(self):
self.client = Client()
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
self.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
)
self.client.force_login(self.user)
def tearDown(self):
super().tearDown()
self.user.delete()
@override_settings(IS_PRODUCTION=True)
def test_production_environment(self):
"""No banner on prod."""
home_page = self.client.get("/")
self.assertNotContains(home_page, "You are on a test site.")
@override_settings(IS_PRODUCTION=False)
def test_non_production_environment(self):
"""Banner on non-prod."""
home_page = self.client.get("/")
self.assertContains(home_page, "You are on a test site.")

View file

@ -15,7 +15,7 @@ class EmailSendingError(RuntimeError):
pass
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}):
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}):
"""Send an email built from a template to one email address.
template_name and subject_template_name are relative to the same template
@ -40,10 +40,14 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
except Exception as exc:
raise EmailSendingError("Could not access the SES client.") from exc
destination = {"ToAddresses": [to_address]}
if bcc_address:
destination["BccAddresses"] = [bcc_address]
try:
ses_client.send_email(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [to_address]},
Destination=destination,
Content={
"Simple": {
"Subject": {"Data": subject},

View file

@ -14,6 +14,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic.edit import FormMixin
from django.conf import settings
from registrar.models import (
Domain,
@ -707,7 +708,7 @@ class DomainAddUserView(DomainFormBaseView):
adding a success message to the view if the email sending succeeds"""
# Set a default email address to send to for staff
requestor_email = "help@get.gov"
requestor_email = settings.DEFAULT_FROM_EMAIL
# Check if the email requestor has a valid email address
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":