mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-25 03:58:39 +02:00
Merge branch 'main' into za/1676-require-investigator-da
This commit is contained in:
commit
ef240c59d6
15 changed files with 1721 additions and 1002 deletions
|
@ -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,7 +1110,23 @@ 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:
|
||||
status_method_mapping = {
|
||||
|
@ -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."
|
||||
|
|
|
@ -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" });
|
||||
})();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -8,9 +8,58 @@ 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 didn’t 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 can’t 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 don’t believe you’re 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 can’t 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
|
||||
If your organization is eligible for a .gov domain and you meet our other requirements, 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.
|
||||
|
||||
Learn more about:
|
||||
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
||||
|
@ -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.
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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.")
|
|
@ -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():
|
||||
|
|
|
@ -321,24 +321,24 @@ 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()
|
||||
investigator, _ = User.objects.get_or_create(username="frenchtoast", is_staff=True)
|
||||
application = DomainApplication.objects.create(
|
||||
creator=user, requested_domain=draft_domain, investigator=investigator
|
||||
)
|
||||
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)
|
||||
application = DomainApplication.objects.create(
|
||||
creator=user, requested_domain=draft_domain, investigator=investigator
|
||||
)
|
||||
|
||||
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
|
||||
application.approve()
|
||||
# should have information present for this domain
|
||||
domain = Domain.objects.get(name="igorville.gov")
|
||||
self.assertTrue(domain)
|
||||
self.mockedSendFunction.assert_not_called()
|
||||
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()
|
||||
|
||||
def test_accessing_domain_properties_creates_domain_in_registry(self):
|
||||
"""
|
||||
|
@ -349,33 +349,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):
|
||||
|
@ -385,7 +386,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):
|
||||
|
@ -507,23 +509,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):
|
||||
"""
|
||||
|
@ -540,23 +543,24 @@ 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_invalid_error(self):
|
||||
"""
|
||||
|
@ -565,8 +569,9 @@ class TestDomainAvailable(MockEppLib):
|
|||
|
||||
Validate InvalidDomainError is raised
|
||||
"""
|
||||
with self.assertRaises(errors.InvalidDomainError):
|
||||
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):
|
||||
"""
|
||||
|
@ -575,8 +580,9 @@ class TestDomainAvailable(MockEppLib):
|
|||
|
||||
Validate InvalidDomainError is raised
|
||||
"""
|
||||
with self.assertRaises(errors.InvalidDomainError):
|
||||
Domain.available("")
|
||||
with less_console_noise():
|
||||
with self.assertRaises(errors.InvalidDomainError):
|
||||
Domain.available("")
|
||||
|
||||
def test_domain_available_unsuccessful(self):
|
||||
"""
|
||||
|
@ -588,13 +594,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):
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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() != "":
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue