This commit is contained in:
Rachid Mrad 2024-02-27 13:14:01 -05:00
commit d208241d2a
No known key found for this signature in database
19 changed files with 2016 additions and 1028 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
@ -27,6 +27,7 @@ from django.utils.safestring import mark_safe
from django.utils.html import escape
from django.contrib.auth.forms import UserChangeForm, UsernameField
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
@ -838,6 +839,14 @@ class DomainInformationAdmin(ListHeaderAdmin):
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)
autocomplete_fields = [
"creator",
"domain_application",
"authorizing_official",
"domain",
"submitter",
]
# Table ordering
ordering = ["domain__name"]
@ -881,13 +890,19 @@ class DomainApplicationAdmin(ListHeaderAdmin):
)
# Annotate the full name and return a values list that lookups can use
privileged_users_annotated = privileged_users.annotate(
full_name=Coalesce(
Concat("investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()),
"investigator__email",
output_field=CharField(),
privileged_users_annotated = (
privileged_users.annotate(
full_name=Coalesce(
Concat(
"investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()
),
"investigator__email",
output_field=CharField(),
)
)
).values_list("investigator__id", "full_name")
.values_list("investigator__id", "full_name")
.distinct()
)
return privileged_users_annotated
@ -898,11 +913,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",
@ -914,8 +953,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 = [
@ -927,7 +979,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
search_help_text = "Search by domain or submitter."
fieldsets = [
(None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}),
(None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}),
(
"Type of organization",
{
@ -1028,6 +1080,23 @@ class DomainApplicationAdmin(ListHeaderAdmin):
"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:
status_method_mapping = {
@ -1131,6 +1200,14 @@ class DomainInformationInline(admin.StackedInline):
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)
autocomplete_fields = [
"creator",
"domain_application",
"authorizing_official",
"domain",
"submitter",
]
def formfield_for_manytomany(self, db_field, request, **kwargs):
"""customize the behavior of formfields with manytomany relationships. the customized
behavior includes sorting of objects in lists as well as customizing helper text"""
@ -1160,12 +1237,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",
@ -1189,8 +1291,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."
@ -1210,7 +1346,14 @@ class DomainAdmin(ListHeaderAdmin):
if object_id is not None:
domain = Domain.objects.get(pk=object_id)
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
curr_exp_date = domain.registry_expiration_date
try:
curr_exp_date = domain.registry_expiration_date
except KeyError:
# No expiration date was found. Return none.
extra_context["extended_expiration_date"] = None
return super().changeform_view(request, object_id, form_url, extra_context)
if curr_exp_date < date.today():
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
else:

View file

@ -343,3 +343,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

@ -13,6 +13,7 @@ from typing import Any
from registrar.models.host import Host
from registrar.models.host_ip import HostIP
from registrar.utility.enums import DefaultEmail
from registrar.utility import errors
from registrar.utility.errors import (
ActionNotAllowed,
@ -192,9 +193,17 @@ class Domain(TimeStampedModel, DomainHelper):
@classmethod
def available(cls, domain: str) -> bool:
"""Check if a domain is available."""
"""Check if a domain is available.
This is called by the availablility api and
is called in the validate function on the request/domain page
throws- RegistryError or InvalidDomainError"""
if not cls.string_could_be_domain(domain):
raise ValueError("Not a valid domain: %s" % str(domain))
logger.warning("Not a valid domain: %s" % str(domain))
# throw invalid domain error so that it can be caught in
# validate_and_handle_errors in domain_helper
raise errors.InvalidDomainError()
domain_name = domain.lower()
req = commands.CheckDomain([domain_name])
return registry.send(req, cleaned=True).res_data[0].avail
@ -429,7 +438,6 @@ class Domain(TimeStampedModel, DomainHelper):
raise NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=nameserver)
elif cls.isSubdomain(name, nameserver) and (ip is None or ip == []):
raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != []):
raise NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip)
elif ip is not None and ip != []:
@ -1780,6 +1788,10 @@ class Domain(TimeStampedModel, DomainHelper):
for cleaned_host in cleaned_hosts:
# Check if the cleaned_host already exists
host_in_db, host_created = Host.objects.get_or_create(domain=self, name=cleaned_host["name"])
# Check if the nameserver is a subdomain of the current domain
# If it is NOT a subdomain, we remove the IP address
if not Domain.isSubdomain(self.name, cleaned_host["name"]):
cleaned_host["addrs"] = []
# Get cleaned list of ips for update
cleaned_ips = cleaned_host["addrs"]
if not host_created:

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
@ -350,12 +351,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(
@ -363,6 +386,7 @@ class DomainApplication(TimeStampedModel):
on_delete=models.PROTECT,
related_name="applications_created",
)
investigator = models.ForeignKey(
"registrar.User",
null=True,
@ -588,7 +612,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
@ -613,6 +639,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:
@ -654,11 +681,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(
@ -678,12 +711,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"
@ -705,12 +743,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"
@ -729,6 +772,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
@ -751,6 +796,9 @@ class DomainApplication(TimeStampedModel):
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER
)
if self.status == self.ApplicationStatus.REJECTED:
self.rejection_reason = None
self._send_status_update_email(
"application approved",
"emails/status_change_approved.txt",

View file

@ -33,11 +33,12 @@ class DomainHelper:
# Split into pieces for the linter
domain = cls._validate_domain_string(domain, blank_ok)
try:
if not check_domain_available(domain):
raise errors.DomainUnavailableError()
except RegistryError as err:
raise errors.RegistrySystemError() from err
if domain != "":
try:
if not check_domain_available(domain):
raise errors.DomainUnavailableError()
except RegistryError as err:
raise errors.RegistrySystemError() from err
return domain
@staticmethod

View file

@ -15,7 +15,15 @@
{% if filters %}
filtered by
{% for filter_param in filters %}
{{ filter_param.parameter_name }} = {{ filter_param.parameter_value }}
{% 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

@ -692,6 +692,56 @@ class MockEppLib(TestCase):
],
ex_date=datetime.date(2023, 5, 25),
)
mockDataInfoDomainSubdomain = fakedEppObject(
"fakePw",
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
hosts=["fake.meoward.gov"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
ex_date=datetime.date(2023, 5, 25),
)
mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject(
"fakePw",
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
hosts=["fake.meow.gov"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
ex_date=datetime.date(2023, 5, 25),
addrs=[common.Ip(addr="2.0.0.8")],
)
mockDataInfoDomainNotSubdomainNoIP = fakedEppObject(
"fakePw",
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
hosts=["fake.meow.com"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
ex_date=datetime.date(2023, 5, 25),
)
mockDataInfoDomainSubdomainNoIP = fakedEppObject(
"fakePw",
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
hosts=["fake.subdomainwoip.gov"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
ex_date=datetime.date(2023, 5, 25),
)
mockDataExtensionDomain = fakedEppObject(
"fakePw",
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
@ -829,6 +879,24 @@ class MockEppLib(TestCase):
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
)
mockDataInfoHosts1IP = fakedEppObject(
"lastPw",
cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
addrs=[common.Ip(addr="2.0.0.8")],
)
mockDataInfoHostsNotSubdomainNoIP = fakedEppObject(
"lastPw",
cr_date=make_aware(datetime.datetime(2023, 8, 26, 19, 45, 35)),
addrs=[],
)
mockDataInfoHostsSubdomainNoIP = fakedEppObject(
"lastPw",
cr_date=make_aware(datetime.datetime(2023, 8, 27, 19, 45, 35)),
addrs=[],
)
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)))
addDsData1 = {
"keyTag": 1234,
@ -995,6 +1063,8 @@ class MockEppLib(TestCase):
return self.mockDeleteDomainCommands(_request, cleaned)
case commands.RenewDomain:
return self.mockRenewDomainCommand(_request, cleaned)
case commands.InfoHost:
return self.mockInfoHostCommmands(_request, cleaned)
case _:
return MagicMock(res_data=[self.mockDataInfoHosts])
@ -1009,6 +1079,25 @@ class MockEppLib(TestCase):
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
def mockInfoHostCommmands(self, _request, cleaned):
request_name = getattr(_request, "name", None)
# Define a dictionary to map request names to data and extension values
request_mappings = {
"fake.meow.gov": (self.mockDataInfoHosts1IP, None), # is subdomain and has ip
"fake.meow.com": (self.mockDataInfoHostsNotSubdomainNoIP, None), # not subdomain w no ip
"fake.subdomainwoip.gov": (self.mockDataInfoHostsSubdomainNoIP, None), # subdomain w no ip
}
# Retrieve the corresponding values from the dictionary
default_mapping = (self.mockDataInfoHosts, None)
res_data, extensions = request_mappings.get(request_name, default_mapping)
return MagicMock(
res_data=[res_data],
extensions=[extensions] if extensions is not None else [],
)
def mockUpdateHostCommands(self, _request, cleaned):
test_ws_ip = common.Ip(addr="1.1. 1.1")
addrs_submitted = getattr(_request, "addrs", [])
@ -1097,6 +1186,10 @@ class MockEppLib(TestCase):
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
"justnameserver.com": (self.justNameserver, None),
"meoward.gov": (self.mockDataInfoDomainSubdomain, None),
"meow.gov": (self.mockDataInfoDomainSubdomainAndIPAddress, None),
"fakemeow.gov": (self.mockDataInfoDomainNotSubdomainNoIP, None),
"subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None),
}
# Retrieve the corresponding values from the dictionary

File diff suppressed because it is too large Load diff

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

@ -574,6 +574,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

@ -20,7 +20,7 @@ from registrar.models.user import User
from registrar.utility.errors import ActionNotAllowed, NameserverError
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
from registrar.utility import errors
from django_fsm import TransitionNotAllowed # type: ignore
from epplibwrapper import (
@ -96,7 +96,7 @@ class TestDomainCache(MockEppLib):
self.mockedSendFunction.assert_has_calls(expectedCalls)
def test_cache_nested_elements(self):
def test_cache_nested_elements_not_subdomain(self):
"""Cache works correctly with the nested objects cache and hosts"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
@ -113,7 +113,7 @@ class TestDomainCache(MockEppLib):
}
expectedHostsDict = {
"name": self.mockDataInfoDomain.hosts[0],
"addrs": [item.addr for item in self.mockDataInfoHosts.addrs],
"addrs": [], # should return empty bc fake.host.com is not a subdomain of igorville.gov
"cr_date": self.mockDataInfoHosts.cr_date,
}
@ -138,6 +138,59 @@ class TestDomainCache(MockEppLib):
# invalidate cache
domain._cache = {}
# get host
domain._get_property("hosts")
# Should return empty bc fake.host.com is not a subdomain of igorville.gov
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
# get contacts
domain._get_property("contacts")
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
def test_cache_nested_elements_is_subdomain(self):
"""Cache works correctly with the nested objects cache and hosts"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="meoward.gov")
# The contact list will initially contain objects of type 'DomainContact'
# this is then transformed into PublicContact, and cache should NOT
# hold onto the DomainContact object
expectedUnfurledContactsList = [
common.DomainContact(contact="123", type="security"),
]
expectedContactsDict = {
PublicContact.ContactTypeChoices.ADMINISTRATIVE: None,
PublicContact.ContactTypeChoices.SECURITY: "123",
PublicContact.ContactTypeChoices.TECHNICAL: None,
}
expectedHostsDict = {
"name": self.mockDataInfoDomainSubdomain.hosts[0],
"addrs": [item.addr for item in self.mockDataInfoHosts.addrs],
"cr_date": self.mockDataInfoHosts.cr_date,
}
# this can be changed when the getter for contacts is implemented
domain._get_property("contacts")
# check domain info is still correct and not overridden
self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomainSubdomain.auth_info)
self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomainSubdomain.cr_date)
# check contacts
self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomainSubdomain.contacts)
# The contact list should not contain what is sent by the registry by default,
# as _fetch_cache will transform the type to PublicContact
self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList)
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
# get and check hosts is set correctly
domain._get_property("hosts")
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
# invalidate cache
domain._cache = {}
# get host
domain._get_property("hosts")
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
@ -268,21 +321,21 @@ class TestDomainCreation(MockEppLib):
Then a Domain exists in the database with the same `name`
But a domain object does not exist in the registry
"""
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
with less_console_noise():
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
# skip using the submit method
application.status = DomainApplication.ApplicationStatus.SUBMITTED
# transition to approve state
application.approve()
# should have information present for this domain
domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(domain)
self.mockedSendFunction.assert_not_called()
# should have information present for this domain
domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(domain)
self.mockedSendFunction.assert_not_called()
def test_accessing_domain_properties_creates_domain_in_registry(self):
"""
@ -293,33 +346,34 @@ class TestDomainCreation(MockEppLib):
And `domain.state` is set to `UNKNOWN`
And `domain.is_active()` returns False
"""
domain = Domain.objects.create(name="beef-tongue.gov")
# trigger getter
_ = domain.statuses
with less_console_noise():
domain = Domain.objects.create(name="beef-tongue.gov")
# trigger getter
_ = domain.statuses
# contacts = PublicContact.objects.filter(domain=domain,
# type=PublicContact.ContactTypeChoices.REGISTRANT).get()
# contacts = PublicContact.objects.filter(domain=domain,
# type=PublicContact.ContactTypeChoices.REGISTRANT).get()
# Called in _fetch_cache
self.mockedSendFunction.assert_has_calls(
[
# TODO: due to complexity of the test, will return to it in
# a future ticket
# call(
# commands.CreateDomain(name="beef-tongue.gov",
# id=contact.registry_id, auth_info=None),
# cleaned=True,
# ),
call(
commands.InfoDomain(name="beef-tongue.gov", auth_info=None),
cleaned=True,
),
],
any_order=False, # Ensure calls are in the specified order
)
# Called in _fetch_cache
self.mockedSendFunction.assert_has_calls(
[
# TODO: due to complexity of the test, will return to it in
# a future ticket
# call(
# commands.CreateDomain(name="beef-tongue.gov",
# id=contact.registry_id, auth_info=None),
# cleaned=True,
# ),
call(
commands.InfoDomain(name="beef-tongue.gov", auth_info=None),
cleaned=True,
),
],
any_order=False, # Ensure calls are in the specified order
)
self.assertEqual(domain.state, Domain.State.UNKNOWN)
self.assertEqual(domain.is_active(), False)
self.assertEqual(domain.state, Domain.State.UNKNOWN)
self.assertEqual(domain.is_active(), False)
@skip("assertion broken with mock addition")
def test_empty_domain_creation(self):
@ -329,7 +383,8 @@ class TestDomainCreation(MockEppLib):
def test_minimal_creation(self):
"""Can create with just a name."""
Domain.objects.create(name="igorville.gov")
with less_console_noise():
Domain.objects.create(name="igorville.gov")
@skip("assertion broken with mock addition")
def test_duplicate_creation(self):
@ -451,23 +506,24 @@ class TestDomainAvailable(MockEppLib):
res_data=[responses.check.CheckDomainResultData(name="available.gov", avail=True, reason=None)],
)
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
available = Domain.available("available.gov")
mocked_send.assert_has_calls(
[
call(
commands.CheckDomain(
["available.gov"],
),
cleaned=True,
)
]
)
self.assertTrue(available)
patcher.stop()
available = Domain.available("available.gov")
mocked_send.assert_has_calls(
[
call(
commands.CheckDomain(
["available.gov"],
),
cleaned=True,
)
]
)
self.assertTrue(available)
patcher.stop()
def test_domain_unavailable(self):
"""
@ -484,33 +540,46 @@ class TestDomainAvailable(MockEppLib):
res_data=[responses.check.CheckDomainResultData(name="unavailable.gov", avail=False, reason="In Use")],
)
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
available = Domain.available("unavailable.gov")
mocked_send.assert_has_calls(
[
call(
commands.CheckDomain(
["unavailable.gov"],
),
cleaned=True,
)
]
)
self.assertFalse(available)
patcher.stop()
available = Domain.available("unavailable.gov")
mocked_send.assert_has_calls(
[
call(
commands.CheckDomain(
["unavailable.gov"],
),
cleaned=True,
)
]
)
self.assertFalse(available)
patcher.stop()
def test_domain_available_with_value_error(self):
def test_domain_available_with_invalid_error(self):
"""
Scenario: Testing whether an invalid domain is available
Should throw ValueError
Should throw InvalidDomainError
Validate ValueError is raised
Validate InvalidDomainError is raised
"""
with self.assertRaises(ValueError):
Domain.available("invalid-string")
with less_console_noise():
with self.assertRaises(errors.InvalidDomainError):
Domain.available("invalid-string")
def test_domain_available_with_empty_string(self):
"""
Scenario: Testing whether an empty string domain name is available
Should throw InvalidDomainError
Validate InvalidDomainError is raised
"""
with less_console_noise():
with self.assertRaises(errors.InvalidDomainError):
Domain.available("")
def test_domain_available_unsuccessful(self):
"""
@ -522,13 +591,14 @@ class TestDomainAvailable(MockEppLib):
def side_effect(_request, cleaned):
raise RegistryError(code=ErrorCode.COMMAND_SYNTAX_ERROR)
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
with self.assertRaises(RegistryError):
Domain.available("raises-error.gov")
patcher.stop()
with self.assertRaises(RegistryError):
Domain.available("raises-error.gov")
patcher.stop()
class TestRegistrantContacts(MockEppLib):
@ -1572,31 +1642,100 @@ class TestRegistrantNameservers(MockEppLib):
self.assertEqual(nameservers[0][1], ["1.1.1.1"])
patcher.stop()
def test_nameservers_stored_on_fetch_cache(self):
def test_nameservers_stored_on_fetch_cache_a_subdomain_with_ip(self):
"""
#1: Nameserver is a subdomain, and has an IP address
referenced by mockDataInfoDomainSubdomainAndIPAddress
"""
with less_console_noise():
# make the domain
domain, _ = Domain.objects.get_or_create(name="meow.gov", state=Domain.State.READY)
# mock the get_or_create methods for Host and HostIP
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
HostIP.objects, "get_or_create"
) as mock_host_ip_get_or_create:
mock_host_get_or_create.return_value = (Host(domain=domain), True)
mock_host_ip_get_or_create.return_value = (HostIP(), True)
# force fetch_cache to be called, which will return above documented mocked hosts
domain.nameservers
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.gov")
# Retrieve the mocked_host from the return value of the mock
actual_mocked_host, _ = mock_host_get_or_create.return_value
mock_host_ip_get_or_create.assert_called_with(address="2.0.0.8", host=actual_mocked_host)
self.assertEqual(mock_host_ip_get_or_create.call_count, 1)
def test_nameservers_stored_on_fetch_cache_a_subdomain_without_ip(self):
"""
#2: Nameserver is a subdomain, but doesn't have an IP address associated
referenced by mockDataInfoDomainSubdomainNoIP
"""
with less_console_noise():
# make the domain
domain, _ = Domain.objects.get_or_create(name="subdomainwoip.gov", state=Domain.State.READY)
# mock the get_or_create methods for Host and HostIP
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
HostIP.objects, "get_or_create"
) as mock_host_ip_get_or_create:
mock_host_get_or_create.return_value = (Host(domain=domain), True)
mock_host_ip_get_or_create.return_value = (HostIP(), True)
# force fetch_cache to be called, which will return above documented mocked hosts
domain.nameservers
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.subdomainwoip.gov")
mock_host_ip_get_or_create.assert_not_called()
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
def test_nameservers_stored_on_fetch_cache_not_subdomain_with_ip(self):
"""
Scenario: Nameservers are stored in db when they are retrieved from fetch_cache.
Verify the success of this by asserting get_or_create calls to db.
The mocked data for the EPP calls returns a host name
of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5
from InfoHost
#3: Nameserver is not a subdomain, but it does have an IP address returned
due to how we set up our defaults
"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# mock the get_or_create methods for Host and HostIP
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
HostIP.objects, "get_or_create"
) as mock_host_ip_get_or_create:
# Set the return value for the mocks
mock_host_get_or_create.return_value = (Host(), True)
mock_host_get_or_create.return_value = (Host(domain=domain), True)
mock_host_ip_get_or_create.return_value = (HostIP(), True)
# force fetch_cache to be called, which will return above documented mocked hosts
domain.nameservers
# assert that the mocks are called
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com")
# Retrieve the mocked_host from the return value of the mock
actual_mocked_host, _ = mock_host_get_or_create.return_value
mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host)
self.assertEqual(mock_host_ip_get_or_create.call_count, 2)
mock_host_ip_get_or_create.assert_not_called()
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
def test_nameservers_stored_on_fetch_cache_not_subdomain_without_ip(self):
"""
#4: Nameserver is not a subdomain and doesn't have an associated IP address
referenced by self.mockDataInfoDomainNotSubdomainNoIP
"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="fakemeow.gov", state=Domain.State.READY)
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
HostIP.objects, "get_or_create"
) as mock_host_ip_get_or_create:
mock_host_get_or_create.return_value = (Host(domain=domain), True)
mock_host_ip_get_or_create.return_value = (HostIP(), True)
# force fetch_cache to be called, which will return above documented mocked hosts
domain.nameservers
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.com")
mock_host_ip_get_or_create.assert_not_called()
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
@skip("not implemented yet")
def test_update_is_unsuccessful(self):

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

@ -821,14 +821,15 @@ class TestDomainNameservers(TestDomainOverview):
nameserver1 = "ns1.igorville.gov"
nameserver2 = "ns2.igorville.gov"
valid_ip = "1.1. 1.1"
# initial nameservers page has one server with two ips
valid_ip_2 = "2.2. 2.2"
# have to throw an error in order to test that the whitespace has been stripped from ip
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# attempt to submit the form without one host and an ip with whitespace
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-1-ip"] = valid_ip
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-1-server"] = nameserver2
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
@ -937,15 +938,14 @@ class TestDomainNameservers(TestDomainOverview):
nameserver1 = "ns1.igorville.gov"
nameserver2 = "ns2.igorville.gov"
valid_ip = "127.0.0.1"
# initial nameservers page has one server with two ips
valid_ip_2 = "128.0.0.2"
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# attempt to submit the form without two hosts, both subdomains,
# only one has ips
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip
nameservers_page.form["form-1-ip"] = valid_ip_2
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302

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() != "":