mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 01:27:03 +02:00
Merge branch 'main' into dk/1352-nameservers
This commit is contained in:
commit
1d2bb4a3e9
29 changed files with 1057 additions and 598 deletions
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.db.models.functions import Concat
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django_fsm import get_available_FIELD_transitions
|
from django_fsm import get_available_FIELD_transitions
|
||||||
|
@ -11,7 +12,7 @@ from django.http.response import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
from registrar.models.user import User
|
||||||
from registrar.utility import csv_export
|
from registrar.utility import csv_export
|
||||||
from . import models
|
from . import models
|
||||||
from auditlog.models import LogEntry # type: ignore
|
from auditlog.models import LogEntry # type: ignore
|
||||||
|
@ -45,7 +46,44 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
||||||
add_form_template = "admin/change_form_no_submit.html"
|
add_form_template = "admin/change_form_no_submit.html"
|
||||||
|
|
||||||
|
|
||||||
class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
|
class AdminSortFields:
|
||||||
|
def get_queryset(db_field):
|
||||||
|
"""This is a helper function for formfield_for_manytomany and formfield_for_foreignkey"""
|
||||||
|
# customize sorting
|
||||||
|
if db_field.name in (
|
||||||
|
"other_contacts",
|
||||||
|
"authorizing_official",
|
||||||
|
"submitter",
|
||||||
|
):
|
||||||
|
# Sort contacts by first_name, then last_name, then email
|
||||||
|
return models.Contact.objects.all().order_by(Concat("first_name", "last_name", "email"))
|
||||||
|
elif db_field.name in ("current_websites", "alternative_domains"):
|
||||||
|
# sort web sites
|
||||||
|
return models.Website.objects.all().order_by("website")
|
||||||
|
elif db_field.name in (
|
||||||
|
"creator",
|
||||||
|
"user",
|
||||||
|
"investigator",
|
||||||
|
):
|
||||||
|
# Sort users by first_name, then last_name, then email
|
||||||
|
return models.User.objects.all().order_by(Concat("first_name", "last_name", "email"))
|
||||||
|
elif db_field.name in (
|
||||||
|
"domain",
|
||||||
|
"approved_domain",
|
||||||
|
):
|
||||||
|
# Sort domains by name
|
||||||
|
return models.Domain.objects.all().order_by("name")
|
||||||
|
elif db_field.name in ("requested_domain",):
|
||||||
|
# Sort draft domains by name
|
||||||
|
return models.DraftDomain.objects.all().order_by("name")
|
||||||
|
elif db_field.name in ("domain_application",):
|
||||||
|
# Sort domain applications by name
|
||||||
|
return models.DomainApplication.objects.all().order_by("requested_domain__name")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class AuditedAdmin(admin.ModelAdmin):
|
||||||
"""Custom admin to make auditing easier."""
|
"""Custom admin to make auditing easier."""
|
||||||
|
|
||||||
def history_view(self, request, object_id, extra_context=None):
|
def history_view(self, request, object_id, extra_context=None):
|
||||||
|
@ -58,10 +96,27 @@ class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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"""
|
||||||
|
queryset = AdminSortFields.get_queryset(db_field)
|
||||||
|
if queryset:
|
||||||
|
kwargs["queryset"] = queryset
|
||||||
|
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
|
# customize the help text for all formfields for manytomany
|
||||||
|
formfield.help_text = (
|
||||||
|
formfield.help_text
|
||||||
|
+ " If more than one value is selected, the change/delete/view actions will be disabled."
|
||||||
|
)
|
||||||
|
return formfield
|
||||||
|
|
||||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
"""Used to sort dropdown fields alphabetically but can be expanded upon"""
|
"""customize the behavior of formfields with foreign key relationships. this will customize
|
||||||
form_field = super().formfield_for_foreignkey(db_field, request, **kwargs)
|
the behavior of selects. customized behavior includes sorting of objects in list"""
|
||||||
return self.form_field_order_helper(form_field, db_field)
|
queryset = AdminSortFields.get_queryset(db_field)
|
||||||
|
if queryset:
|
||||||
|
kwargs["queryset"] = queryset
|
||||||
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ListHeaderAdmin(AuditedAdmin):
|
class ListHeaderAdmin(AuditedAdmin):
|
||||||
|
@ -118,15 +173,6 @@ class ListHeaderAdmin(AuditedAdmin):
|
||||||
)
|
)
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
# customize the help_text for all formfields for manytomany
|
|
||||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
|
||||||
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
|
|
||||||
formfield.help_text = (
|
|
||||||
formfield.help_text
|
|
||||||
+ " If more than one value is selected, the change/delete/view actions will be disabled."
|
|
||||||
)
|
|
||||||
return formfield
|
|
||||||
|
|
||||||
|
|
||||||
class UserContactInline(admin.StackedInline):
|
class UserContactInline(admin.StackedInline):
|
||||||
"""Edit a user's profile on the user page."""
|
"""Edit a user's profile on the user page."""
|
||||||
|
@ -221,6 +267,10 @@ class MyUserAdmin(BaseUserAdmin):
|
||||||
"groups",
|
"groups",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# this ordering effects the ordering of results
|
||||||
|
# in autocomplete_fields for user
|
||||||
|
ordering = ["first_name", "last_name", "email"]
|
||||||
|
|
||||||
# Let's define First group
|
# Let's define First group
|
||||||
# (which should in theory be the ONLY group)
|
# (which should in theory be the ONLY group)
|
||||||
def group(self, obj):
|
def group(self, obj):
|
||||||
|
@ -489,12 +539,8 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
# to activate the edit/delete/view buttons
|
# to activate the edit/delete/view buttons
|
||||||
filter_horizontal = ("other_contacts",)
|
filter_horizontal = ("other_contacts",)
|
||||||
|
|
||||||
# lists in filter_horizontal are not sorted properly, sort them
|
# Table ordering
|
||||||
# by first_name
|
ordering = ["domain__name"]
|
||||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
|
||||||
if db_field.name in ("other_contacts",):
|
|
||||||
kwargs["queryset"] = models.Contact.objects.all().order_by("first_name") # Sort contacts
|
|
||||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
"""Set the read-only state on form elements.
|
"""Set the read-only state on form elements.
|
||||||
|
@ -547,6 +593,27 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
"""Custom domain applications admin class."""
|
"""Custom domain applications admin class."""
|
||||||
|
|
||||||
|
class InvestigatorFilter(admin.SimpleListFilter):
|
||||||
|
"""Custom investigator filter that only displays users with the manager role"""
|
||||||
|
|
||||||
|
title = "investigator"
|
||||||
|
# Match the old param name to avoid unnecessary refactoring
|
||||||
|
parameter_name = "investigator__id__exact"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
"""Lookup reimplementation, gets users of is_staff.
|
||||||
|
Returns a list of tuples consisting of (user.id, user)
|
||||||
|
"""
|
||||||
|
privileged_users = User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")
|
||||||
|
return [(user.id, user) for user in privileged_users]
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
"""Custom queryset implementation, filters by investigator"""
|
||||||
|
if self.value() is None:
|
||||||
|
return queryset
|
||||||
|
else:
|
||||||
|
return queryset.filter(investigator__id__exact=self.value())
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
|
@ -558,7 +625,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
]
|
]
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = ("status", "organization_type", "investigator")
|
list_filter = ("status", "organization_type", InvestigatorFilter)
|
||||||
|
|
||||||
# Search
|
# Search
|
||||||
search_fields = [
|
search_fields = [
|
||||||
|
@ -634,6 +701,9 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
||||||
|
|
||||||
|
# Table ordering
|
||||||
|
ordering = ["requested_domain__name"]
|
||||||
|
|
||||||
# lists in filter_horizontal are not sorted properly, sort them
|
# lists in filter_horizontal are not sorted properly, sort them
|
||||||
# by website
|
# by website
|
||||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||||
|
@ -641,6 +711,13 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
kwargs["queryset"] = models.Website.objects.all().order_by("website") # Sort websites
|
kwargs["queryset"] = models.Website.objects.all().order_by("website") # Sort websites
|
||||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
|
# Removes invalid investigator options from the investigator dropdown
|
||||||
|
if db_field.name == "investigator":
|
||||||
|
kwargs["queryset"] = User.objects.filter(is_staff=True)
|
||||||
|
return db_field.formfield(**kwargs)
|
||||||
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
# Trigger action when a fieldset is changed
|
# Trigger action when a fieldset is changed
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
if obj and obj.creator.status != models.User.RESTRICTED:
|
if obj and obj.creator.status != models.User.RESTRICTED:
|
||||||
|
@ -774,12 +851,27 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
# to activate the edit/delete/view buttons
|
# to activate the edit/delete/view buttons
|
||||||
filter_horizontal = ("other_contacts",)
|
filter_horizontal = ("other_contacts",)
|
||||||
|
|
||||||
# lists in filter_horizontal are not sorted properly, sort them
|
|
||||||
# by first_name
|
|
||||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||||
if db_field.name in ("other_contacts",):
|
"""customize the behavior of formfields with manytomany relationships. the customized
|
||||||
kwargs["queryset"] = models.Contact.objects.all().order_by("first_name") # Sort contacts
|
behavior includes sorting of objects in lists as well as customizing helper text"""
|
||||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
queryset = AdminSortFields.get_queryset(db_field)
|
||||||
|
if queryset:
|
||||||
|
kwargs["queryset"] = queryset
|
||||||
|
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
|
# customize the help text for all formfields for manytomany
|
||||||
|
formfield.help_text = (
|
||||||
|
formfield.help_text
|
||||||
|
+ " If more than one value is selected, the change/delete/view actions will be disabled."
|
||||||
|
)
|
||||||
|
return formfield
|
||||||
|
|
||||||
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
|
"""customize the behavior of formfields with foreign key relationships. this will customize
|
||||||
|
the behavior of selects. customized behavior includes sorting of objects in list"""
|
||||||
|
queryset = AdminSortFields.get_queryset(db_field)
|
||||||
|
if queryset:
|
||||||
|
kwargs["queryset"] = queryset
|
||||||
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
||||||
|
@ -797,6 +889,10 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
"state",
|
"state",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# this ordering effects the ordering of results
|
||||||
|
# in autocomplete_fields for domain
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
def organization_type(self, obj):
|
def organization_type(self, obj):
|
||||||
return obj.domain_info.get_organization_type_display()
|
return obj.domain_info.get_organization_type_display()
|
||||||
|
|
||||||
|
@ -811,6 +907,9 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
change_list_template = "django/admin/domain_change_list.html"
|
change_list_template = "django/admin/domain_change_list.html"
|
||||||
readonly_fields = ["state", "expiration_date"]
|
readonly_fields = ["state", "expiration_date"]
|
||||||
|
|
||||||
|
# Table ordering
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
def export_data_type(self, request):
|
def export_data_type(self, request):
|
||||||
# match the CSV example with all the fields
|
# match the CSV example with all the fields
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
|
|
@ -329,10 +329,6 @@ class AuthorizingOfficialForm(RegistrarForm):
|
||||||
label="First name / given name",
|
label="First name / given name",
|
||||||
error_messages={"required": ("Enter the first name / given name of your authorizing official.")},
|
error_messages={"required": ("Enter the first name / given name of your authorizing official.")},
|
||||||
)
|
)
|
||||||
middle_name = forms.CharField(
|
|
||||||
required=False,
|
|
||||||
label="Middle name (optional)",
|
|
||||||
)
|
|
||||||
last_name = forms.CharField(
|
last_name = forms.CharField(
|
||||||
label="Last name / family name",
|
label="Last name / family name",
|
||||||
error_messages={"required": ("Enter the last name / family name of your authorizing official.")},
|
error_messages={"required": ("Enter the last name / family name of your authorizing official.")},
|
||||||
|
@ -350,10 +346,6 @@ class AuthorizingOfficialForm(RegistrarForm):
|
||||||
label="Email",
|
label="Email",
|
||||||
error_messages={"invalid": ("Enter an email address in the required format, like name@example.com.")},
|
error_messages={"invalid": ("Enter an email address in the required format, like name@example.com.")},
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
|
||||||
label="Phone",
|
|
||||||
error_messages={"required": "Enter the phone number for your authorizing official."},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CurrentSitesForm(RegistrarForm):
|
class CurrentSitesForm(RegistrarForm):
|
||||||
|
@ -618,6 +610,12 @@ class NoOtherContactsForm(RegistrarForm):
|
||||||
"we can contact to help us assess your eligibility for a .gov domain."
|
"we can contact to help us assess your eligibility for a .gov domain."
|
||||||
),
|
),
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(),
|
||||||
|
validators=[
|
||||||
|
MaxLengthValidator(
|
||||||
|
1000,
|
||||||
|
message="Response must be less than 1000 characters.",
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,9 @@ class AuthorizingOfficialContactForm(ContactForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Overriding bc phone not required in this form
|
||||||
|
self.fields["phone"] = forms.IntegerField(required=False)
|
||||||
|
|
||||||
# Set custom error messages
|
# Set custom error messages
|
||||||
self.fields["first_name"].error_messages = {
|
self.fields["first_name"].error_messages = {
|
||||||
"required": "Enter the first name / given name of your authorizing official."
|
"required": "Enter the first name / given name of your authorizing official."
|
||||||
|
@ -227,7 +230,6 @@ class AuthorizingOfficialContactForm(ContactForm):
|
||||||
self.fields["email"].error_messages = {
|
self.fields["email"].error_messages = {
|
||||||
"required": "Enter an email address in the required format, like name@example.com."
|
"required": "Enter an email address in the required format, like name@example.com."
|
||||||
}
|
}
|
||||||
self.fields["phone"].error_messages["required"] = "Enter a phone number for your authorizing official."
|
|
||||||
|
|
||||||
|
|
||||||
class DomainSecurityEmailForm(forms.Form):
|
class DomainSecurityEmailForm(forms.Form):
|
||||||
|
|
|
@ -64,7 +64,7 @@ class Contact(TimeStampedModel):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Update the related User object's first_name and last_name
|
# Update the related User object's first_name and last_name
|
||||||
if self.user:
|
if self.user and (not self.user.first_name or not self.user.last_name):
|
||||||
self.user.first_name = self.first_name
|
self.user.first_name = self.first_name
|
||||||
self.user.last_name = self.last_name
|
self.user.last_name = self.last_name
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|
|
@ -203,7 +203,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
return self._get_property("cr_date")
|
return self._get_property("cr_date")
|
||||||
|
|
||||||
@creation_date.setter # type: ignore
|
@creation_date.setter # type: ignore
|
||||||
def creation_date(self, ex_date: date):
|
def creation_date(self, cr_date: date):
|
||||||
"""
|
"""
|
||||||
Direct setting of the creation date in the registry is not implemented.
|
Direct setting of the creation date in the registry is not implemented.
|
||||||
|
|
||||||
|
|
|
@ -594,7 +594,12 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[ApplicationStatus.STARTED, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.WITHDRAWN],
|
source=[
|
||||||
|
ApplicationStatus.STARTED,
|
||||||
|
ApplicationStatus.IN_REVIEW,
|
||||||
|
ApplicationStatus.ACTION_NEEDED,
|
||||||
|
ApplicationStatus.WITHDRAWN,
|
||||||
|
],
|
||||||
target=ApplicationStatus.SUBMITTED,
|
target=ApplicationStatus.SUBMITTED,
|
||||||
)
|
)
|
||||||
def submit(self):
|
def submit(self):
|
||||||
|
@ -626,7 +631,17 @@ class DomainApplication(TimeStampedModel):
|
||||||
"emails/submission_confirmation_subject.txt",
|
"emails/submission_confirmation_subject.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
@transition(field="status", source=ApplicationStatus.SUBMITTED, target=ApplicationStatus.IN_REVIEW)
|
@transition(
|
||||||
|
field="status",
|
||||||
|
source=[
|
||||||
|
ApplicationStatus.SUBMITTED,
|
||||||
|
ApplicationStatus.ACTION_NEEDED,
|
||||||
|
ApplicationStatus.APPROVED,
|
||||||
|
ApplicationStatus.REJECTED,
|
||||||
|
ApplicationStatus.INELIGIBLE,
|
||||||
|
],
|
||||||
|
target=ApplicationStatus.IN_REVIEW,
|
||||||
|
)
|
||||||
def in_review(self):
|
def in_review(self):
|
||||||
"""Investigate an application that has been submitted.
|
"""Investigate an application that has been submitted.
|
||||||
|
|
||||||
|
@ -640,7 +655,12 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.REJECTED],
|
source=[
|
||||||
|
ApplicationStatus.IN_REVIEW,
|
||||||
|
ApplicationStatus.APPROVED,
|
||||||
|
ApplicationStatus.REJECTED,
|
||||||
|
ApplicationStatus.INELIGIBLE,
|
||||||
|
],
|
||||||
target=ApplicationStatus.ACTION_NEEDED,
|
target=ApplicationStatus.ACTION_NEEDED,
|
||||||
)
|
)
|
||||||
def action_needed(self):
|
def action_needed(self):
|
||||||
|
@ -659,8 +679,8 @@ class DomainApplication(TimeStampedModel):
|
||||||
source=[
|
source=[
|
||||||
ApplicationStatus.SUBMITTED,
|
ApplicationStatus.SUBMITTED,
|
||||||
ApplicationStatus.IN_REVIEW,
|
ApplicationStatus.IN_REVIEW,
|
||||||
|
ApplicationStatus.ACTION_NEEDED,
|
||||||
ApplicationStatus.REJECTED,
|
ApplicationStatus.REJECTED,
|
||||||
ApplicationStatus.INELIGIBLE,
|
|
||||||
],
|
],
|
||||||
target=ApplicationStatus.APPROVED,
|
target=ApplicationStatus.APPROVED,
|
||||||
)
|
)
|
||||||
|
@ -697,7 +717,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW],
|
source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW, ApplicationStatus.ACTION_NEEDED],
|
||||||
target=ApplicationStatus.WITHDRAWN,
|
target=ApplicationStatus.WITHDRAWN,
|
||||||
)
|
)
|
||||||
def withdraw(self):
|
def withdraw(self):
|
||||||
|
@ -710,7 +730,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.APPROVED],
|
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.APPROVED],
|
||||||
target=ApplicationStatus.REJECTED,
|
target=ApplicationStatus.REJECTED,
|
||||||
conditions=[domain_is_not_active],
|
conditions=[domain_is_not_active],
|
||||||
)
|
)
|
||||||
|
@ -720,12 +740,16 @@ class DomainApplication(TimeStampedModel):
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade), and send an email notification."""
|
(will cascade), and send an email notification."""
|
||||||
if self.status == self.ApplicationStatus.APPROVED:
|
if self.status == self.ApplicationStatus.APPROVED:
|
||||||
|
try:
|
||||||
domain_state = self.approved_domain.state
|
domain_state = self.approved_domain.state
|
||||||
# Only reject if it exists on EPP
|
# Only reject if it exists on EPP
|
||||||
if domain_state != Domain.State.UNKNOWN:
|
if domain_state != Domain.State.UNKNOWN:
|
||||||
self.approved_domain.deletedInEpp()
|
self.approved_domain.deletedInEpp()
|
||||||
self.approved_domain.delete()
|
self.approved_domain.delete()
|
||||||
self.approved_domain = None
|
self.approved_domain = None
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(err)
|
||||||
|
logger.error("Can't query an approved domain while attempting a DA reject()")
|
||||||
|
|
||||||
self._send_status_update_email(
|
self._send_status_update_email(
|
||||||
"action needed",
|
"action needed",
|
||||||
|
@ -735,7 +759,12 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.APPROVED],
|
source=[
|
||||||
|
ApplicationStatus.IN_REVIEW,
|
||||||
|
ApplicationStatus.ACTION_NEEDED,
|
||||||
|
ApplicationStatus.APPROVED,
|
||||||
|
ApplicationStatus.REJECTED,
|
||||||
|
],
|
||||||
target=ApplicationStatus.INELIGIBLE,
|
target=ApplicationStatus.INELIGIBLE,
|
||||||
conditions=[domain_is_not_active],
|
conditions=[domain_is_not_active],
|
||||||
)
|
)
|
||||||
|
@ -749,12 +778,16 @@ class DomainApplication(TimeStampedModel):
|
||||||
and domain_information (will cascade) when they exist."""
|
and domain_information (will cascade) when they exist."""
|
||||||
|
|
||||||
if self.status == self.ApplicationStatus.APPROVED:
|
if self.status == self.ApplicationStatus.APPROVED:
|
||||||
|
try:
|
||||||
domain_state = self.approved_domain.state
|
domain_state = self.approved_domain.state
|
||||||
# Only reject if it exists on EPP
|
# Only reject if it exists on EPP
|
||||||
if domain_state != Domain.State.UNKNOWN:
|
if domain_state != Domain.State.UNKNOWN:
|
||||||
self.approved_domain.deletedInEpp()
|
self.approved_domain.deletedInEpp()
|
||||||
self.approved_domain.delete()
|
self.approved_domain.delete()
|
||||||
self.approved_domain = None
|
self.approved_domain = None
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(err)
|
||||||
|
logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()")
|
||||||
|
|
||||||
self.creator.restrict_user()
|
self.creator.restrict_user()
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,7 @@ class UserGroup(Group):
|
||||||
)
|
)
|
||||||
|
|
||||||
cisa_analysts_group.save()
|
cisa_analysts_group.save()
|
||||||
logger.debug("CISA Analyt permissions added to group " + cisa_analysts_group.name)
|
logger.debug("CISA Analyst permissions added to group " + cisa_analysts_group.name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating analyst permissions group: {e}")
|
logger.error(f"Error creating analyst permissions group: {e}")
|
||||||
|
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
import logging
|
|
||||||
from typing import Dict
|
|
||||||
from django.forms import ModelChoiceField
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SortingDict:
|
|
||||||
"""Stores a sorting dictionary object"""
|
|
||||||
|
|
||||||
_sorting_dict: Dict[type, type] = {}
|
|
||||||
|
|
||||||
def __init__(self, model_list, sort_list):
|
|
||||||
self._sorting_dict = {
|
|
||||||
"dropDownSelected": self.convert_list_to_dict(model_list),
|
|
||||||
"sortBy": sort_list,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Used in __init__ for model_list for performance reasons
|
|
||||||
def convert_list_to_dict(self, value_list):
|
|
||||||
"""Used internally to convert model_list to a dictionary"""
|
|
||||||
return {item: item for item in value_list}
|
|
||||||
|
|
||||||
def get_dict(self):
|
|
||||||
"""Grabs the associated dictionary item,
|
|
||||||
has two fields: 'dropDownSelected': model_list and 'sortBy': sort_list"""
|
|
||||||
# This should never happen so we need to log this
|
|
||||||
if self._sorting_dict is None:
|
|
||||||
raise ValueError("_sorting_dict was None")
|
|
||||||
return self._sorting_dict
|
|
||||||
|
|
||||||
|
|
||||||
class AdminFormOrderHelper:
|
|
||||||
"""A helper class to order a dropdown field in Django Admin,
|
|
||||||
takes the fields you want to order by as an array"""
|
|
||||||
|
|
||||||
# Used to keep track of how we want to order_by certain FKs
|
|
||||||
_sorting_list: list[SortingDict] = []
|
|
||||||
|
|
||||||
def __init__(self, sort: list[SortingDict]):
|
|
||||||
self._sorting_list = sort
|
|
||||||
|
|
||||||
def get_ordered_form_field(self, form_field, db_field) -> ModelChoiceField | None:
|
|
||||||
"""Orders the queryset for a ModelChoiceField
|
|
||||||
based on the order_by_dict dictionary"""
|
|
||||||
_order_by_list = []
|
|
||||||
|
|
||||||
for item in self._sorting_list:
|
|
||||||
item_dict = item.get_dict()
|
|
||||||
drop_down_selected = item_dict.get("dropDownSelected")
|
|
||||||
sort_by = item_dict.get("sortBy")
|
|
||||||
|
|
||||||
if db_field.name in drop_down_selected:
|
|
||||||
_order_by_list = sort_by
|
|
||||||
# Exit loop when order_by_list is found
|
|
||||||
break
|
|
||||||
|
|
||||||
# Only order if we choose to do so
|
|
||||||
# noqa for the linter... reduces readability otherwise
|
|
||||||
if _order_by_list is not None and _order_by_list != []: # noqa
|
|
||||||
form_field.queryset = form_field.queryset.order_by(*_order_by_list)
|
|
||||||
|
|
||||||
return form_field
|
|
|
@ -1,27 +0,0 @@
|
||||||
from registrar.models.utility.admin_form_order_helper import (
|
|
||||||
AdminFormOrderHelper,
|
|
||||||
SortingDict,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AdminSortFields:
|
|
||||||
# Used to keep track of how we want to order_by certain FKs
|
|
||||||
foreignkey_orderby_dict: list[SortingDict] = [
|
|
||||||
# foreign_key - order_by
|
|
||||||
# Handles fields that are sorted by 'first_name / last_name
|
|
||||||
SortingDict(
|
|
||||||
["submitter", "authorizing_official", "investigator", "creator", "user"],
|
|
||||||
["first_name", "last_name"],
|
|
||||||
),
|
|
||||||
# Handles fields that are sorted by 'name'
|
|
||||||
SortingDict(["domain", "requested_domain"], ["name"]),
|
|
||||||
SortingDict(["domain_application"], ["requested_domain__name"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
# For readability purposes, but can be replaced with a one liner
|
|
||||||
def form_field_order_helper(self, form_field, db_field):
|
|
||||||
"""A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict)
|
|
||||||
.get_ordered_form_field(form_field, db_field)"""
|
|
||||||
|
|
||||||
form = AdminFormOrderHelper(self.foreignkey_orderby_dict)
|
|
||||||
return form.get_ordered_form_field(form_field, db_field)
|
|
|
@ -23,7 +23,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
You must be an authorized user and need to be signed in to view this page.
|
You must be an authorized user and need to be signed in to view this page.
|
||||||
Would you like to <a href="{% url 'login' %}"> try logging in again</a>?
|
<a href="{% url 'login' %}"> Try logging in again</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you'd like help with this error <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">contact us</a>.
|
If you'd like help with this error <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">contact us</a>.
|
||||||
|
|
|
@ -25,17 +25,11 @@
|
||||||
|
|
||||||
{% input_with_errors forms.0.first_name %}
|
{% input_with_errors forms.0.first_name %}
|
||||||
|
|
||||||
{% input_with_errors forms.0.middle_name %}
|
|
||||||
|
|
||||||
{% input_with_errors forms.0.last_name %}
|
{% input_with_errors forms.0.last_name %}
|
||||||
|
|
||||||
{% input_with_errors forms.0.title %}
|
{% input_with_errors forms.0.title %}
|
||||||
|
|
||||||
{% input_with_errors forms.0.email %}
|
{% input_with_errors forms.0.email %}
|
||||||
|
|
||||||
{% with add_class="usa-input--medium" %}
|
|
||||||
{% input_with_errors forms.0.phone %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load url_helpers %}
|
||||||
|
|
||||||
{% block title %}Thanks for your domain request! | {% endblock %}
|
{% block title %}Thanks for your domain request! | {% endblock %}
|
||||||
|
|
||||||
|
@ -14,21 +15,22 @@
|
||||||
/>
|
/>
|
||||||
<h1>Thanks for your domain request!</h1>
|
<h1>Thanks for your domain request!</h1>
|
||||||
</span>
|
</span>
|
||||||
<p>We'll email a copy of your request to you.</p>
|
<p>We’ll email a copy of your request to you.</p>
|
||||||
|
|
||||||
<h2>Next steps in this process</h2>
|
<h2>Next steps in this process</h2>
|
||||||
|
|
||||||
<p> We'll review your request. This usually takes 20 business days. During
|
<p> We’ll review your request. This usually takes 20 business days. During
|
||||||
this review we'll verify that your:</p>
|
this review we’ll verify that:</p>
|
||||||
<ul class="usa-list">
|
<ul class="usa-list">
|
||||||
<li>Organization is eligible for a .gov domain</li>
|
<li>Your organization is eligible for a .gov domain.</li>
|
||||||
<li>Authorizing official approves your request</li>
|
<li>You work at the organization and/or can make requests on its behalf.</li>
|
||||||
<li>Domain meets our naming requirements</li>
|
<li>Your requested domain meets our naming requirements.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p> You can <a href="{% url 'home' %}">check the status</a>
|
<p> We’ll email you if we have questions and when we complete our review. You can <a href="{% url 'home' %}">check the status</a>
|
||||||
of your request at any time. We'll email you with any questions or when we
|
of your request at any time on the registrar homepage.</p>
|
||||||
complete our review.</p>
|
|
||||||
|
<p> <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us if you need help during this process</a>.</p>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -105,7 +105,7 @@
|
||||||
aria-describedby="Are you sure you want to submit a domain request?"
|
aria-describedby="Are you sure you want to submit a domain request?"
|
||||||
data-force-action
|
data-force-action
|
||||||
>
|
>
|
||||||
{% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you won’t be able to make further edits until it’s reviewed by our staff. You’ll only be able to withdraw your request." modal_button=modal_button|safe %}
|
{% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you won’t be able to edit it until we review it. You’ll only be able to withdraw your request." modal_button=modal_button|safe %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block after_form_content %}{% endblock %}
|
{% block after_form_content %}{% endblock %}
|
||||||
|
|
|
@ -2,5 +2,7 @@
|
||||||
{% load static field_helpers %}
|
{% load static field_helpers %}
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
|
{% with attr_maxlength=1000 %}
|
||||||
{% input_with_errors forms.0.no_other_contacts_rationale %}
|
{% input_with_errors forms.0.no_other_contacts_rationale %}
|
||||||
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -19,18 +19,12 @@
|
||||||
|
|
||||||
{% input_with_errors form.first_name %}
|
{% input_with_errors form.first_name %}
|
||||||
|
|
||||||
{% input_with_errors form.middle_name %}
|
|
||||||
|
|
||||||
{% input_with_errors form.last_name %}
|
{% input_with_errors form.last_name %}
|
||||||
|
|
||||||
{% input_with_errors form.title %}
|
{% input_with_errors form.title %}
|
||||||
|
|
||||||
{% input_with_errors form.email %}
|
{% input_with_errors form.email %}
|
||||||
|
|
||||||
{% input_with_errors form.phone %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="usa-button"
|
class="usa-button"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
Hi.
|
Hi.
|
||||||
|
|
||||||
{{ full_name }} has added you as a manager on {{ domain.name }}.
|
{{ requester_email }} has added you as a manager on {{ domain.name }}.
|
||||||
|
|
||||||
YOU NEED A LOGIN.GOV ACCOUNT
|
YOU NEED A LOGIN.GOV ACCOUNT
|
||||||
You’ll need a Login.gov account to manage your .gov domain. Login.gov provides
|
You’ll need a Login.gov account to manage your .gov domain. Login.gov provides
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
{% load url_helpers %}
|
||||||
|
|
||||||
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold" >
|
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold" >
|
||||||
Next steps
|
Next steps in this process
|
||||||
</h2>
|
</h2>
|
||||||
<p>We received your .gov domain request. Our next step is to review your request. This usually takes two weeks. We’ll email you with questions or when we complete our review. Contact us with any questions.</p>
|
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 20 business days. We’ll email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
|
||||||
|
|
||||||
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
|
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
|
||||||
Need to make changes?
|
Need to make changes?
|
||||||
|
|
|
@ -462,7 +462,7 @@ def completed_application(
|
||||||
):
|
):
|
||||||
"""A completed domain application."""
|
"""A completed domain application."""
|
||||||
if not user:
|
if not user:
|
||||||
user = get_user_model().objects.create(username="username")
|
user = get_user_model().objects.create(username="username" + str(uuid.uuid4())[:8])
|
||||||
ao, _ = Contact.objects.get_or_create(
|
ao, _ = Contact.objects.get_or_create(
|
||||||
first_name="Testy",
|
first_name="Testy",
|
||||||
last_name="Tester",
|
last_name="Tester",
|
||||||
|
|
|
@ -15,13 +15,7 @@ from registrar.admin import (
|
||||||
ContactAdmin,
|
ContactAdmin,
|
||||||
UserDomainRoleAdmin,
|
UserDomainRoleAdmin,
|
||||||
)
|
)
|
||||||
from registrar.models import (
|
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
|
||||||
Domain,
|
|
||||||
DomainApplication,
|
|
||||||
DomainInformation,
|
|
||||||
User,
|
|
||||||
DomainInvitation,
|
|
||||||
)
|
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from .common import (
|
from .common import (
|
||||||
completed_application,
|
completed_application,
|
||||||
|
@ -322,6 +316,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site)
|
self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site)
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
self.staffuser = create_user()
|
self.staffuser = create_user()
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
|
||||||
def test_short_org_name_in_applications_list(self):
|
def test_short_org_name_in_applications_list(self):
|
||||||
"""
|
"""
|
||||||
|
@ -843,12 +838,224 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
with self.assertRaises(DomainInformation.DoesNotExist):
|
with self.assertRaises(DomainInformation.DoesNotExist):
|
||||||
domain_information.refresh_from_db()
|
domain_information.refresh_from_db()
|
||||||
|
|
||||||
|
def test_has_correct_filters(self):
|
||||||
|
"""
|
||||||
|
This test verifies that DomainApplicationAdmin has the correct filters set up.
|
||||||
|
|
||||||
|
It retrieves the current list of filters from DomainApplicationAdmin
|
||||||
|
and checks that it matches the expected list of filters.
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
def test_table_sorted_alphabetically(self):
|
||||||
|
"""
|
||||||
|
This test verifies that the DomainApplicationAdmin table is sorted alphabetically
|
||||||
|
by the 'requested_domain__name' field.
|
||||||
|
|
||||||
|
It creates a list of DomainApplication instances in a non-alphabetical order,
|
||||||
|
then retrieves the queryset from the DomainApplicationAdmin and checks
|
||||||
|
that it matches the expected queryset,
|
||||||
|
which is sorted alphabetically by the 'requested_domain__name' field.
|
||||||
|
"""
|
||||||
|
# Creates a list of DomainApplications in scrambled order
|
||||||
|
multiple_unalphabetical_domain_objects("application")
|
||||||
|
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
# Get the expected list of alphabetically sorted DomainApplications
|
||||||
|
expected_order = DomainApplication.objects.order_by("requested_domain__name")
|
||||||
|
|
||||||
|
# Get the returned queryset
|
||||||
|
queryset = self.admin.get_queryset(request)
|
||||||
|
|
||||||
|
# Check the order
|
||||||
|
self.assertEqual(
|
||||||
|
list(queryset),
|
||||||
|
list(expected_order),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_displays_investigator_filter(self):
|
||||||
|
"""
|
||||||
|
This test verifies that the investigator filter in the admin interface for
|
||||||
|
the DomainApplication model displays correctly.
|
||||||
|
|
||||||
|
It creates two DomainApplication instances, each with a different investigator.
|
||||||
|
It then simulates a staff user logging in and applying the investigator filter
|
||||||
|
on the DomainApplication admin page.
|
||||||
|
|
||||||
|
We then test if the page displays the filter we expect, but we do not test
|
||||||
|
if we get back the correct response in the table. This is to isolate if
|
||||||
|
the filter displays correctly, when the filter isn't filtering correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
investigator_user.is_staff = True
|
||||||
|
investigator_user.save()
|
||||||
|
|
||||||
|
p = "userpass"
|
||||||
|
self.client.login(username="staffuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainapplication/",
|
||||||
|
{
|
||||||
|
"investigator__id__exact": investigator_user.id,
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then, test if the filter actually exists
|
||||||
|
self.assertIn("filters", response.context)
|
||||||
|
|
||||||
|
# Assert the content of filters and search_query
|
||||||
|
filters = response.context["filters"]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
filters,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"parameter_name": "investigator",
|
||||||
|
"parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_investigator_filter_filters_correctly(self):
|
||||||
|
"""
|
||||||
|
This test verifies that the investigator filter in the admin interface for
|
||||||
|
the DomainApplication model works correctly.
|
||||||
|
|
||||||
|
It creates two DomainApplication instances, each with a different investigator.
|
||||||
|
It then simulates a staff user logging in and applying the investigator filter
|
||||||
|
on the DomainApplication admin page.
|
||||||
|
|
||||||
|
It then verifies that it was applied correctly.
|
||||||
|
The test checks that the response contains the expected DomainApplication pbjects
|
||||||
|
in the table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
investigator_user.is_staff = True
|
||||||
|
investigator_user.save()
|
||||||
|
|
||||||
|
# Create a second mock DomainApplication object, to test filtering
|
||||||
|
application: DomainApplication = generic_domain_object("application", "BadGuy")
|
||||||
|
another_user = User.objects.filter(username=application.investigator.username).get()
|
||||||
|
another_user.is_staff = True
|
||||||
|
another_user.save()
|
||||||
|
|
||||||
|
p = "userpass"
|
||||||
|
self.client.login(username="staffuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainapplication/",
|
||||||
|
{
|
||||||
|
"investigator__id__exact": investigator_user.id,
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_name = "SomeGuy first_name:investigator SomeGuy last_name:investigator"
|
||||||
|
# We expect to see this four times, two of them are from the html for the filter,
|
||||||
|
# and the other two are the html from the list entry in the table.
|
||||||
|
self.assertContains(response, expected_name, count=4)
|
||||||
|
|
||||||
|
# Check that we don't also get the thing we aren't filtering for.
|
||||||
|
# We expect to see this two times in the filter
|
||||||
|
unexpected_name = "BadGuy first_name:investigator BadGuy last_name:investigator"
|
||||||
|
self.assertContains(response, unexpected_name, count=2)
|
||||||
|
|
||||||
|
def test_investigator_dropdown_displays_only_staff(self):
|
||||||
|
"""
|
||||||
|
This test verifies that the dropdown for the 'investigator' field in the DomainApplicationAdmin
|
||||||
|
interface only displays users who are marked as staff.
|
||||||
|
|
||||||
|
It creates two DomainApplication instances, one with an investigator
|
||||||
|
who is a staff user and another with an investigator who is not a staff user.
|
||||||
|
|
||||||
|
It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin
|
||||||
|
and checks that it matches the expected queryset, which only includes staff users.
|
||||||
|
"""
|
||||||
|
# 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()
|
||||||
|
investigator_user.is_staff = True
|
||||||
|
investigator_user.save()
|
||||||
|
|
||||||
|
# Create a mock DomainApplication object, with a user that is not staff
|
||||||
|
application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy")
|
||||||
|
investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get()
|
||||||
|
investigator_user_2.is_staff = False
|
||||||
|
investigator_user_2.save()
|
||||||
|
|
||||||
|
p = "userpass"
|
||||||
|
self.client.login(username="staffuser", password=p)
|
||||||
|
|
||||||
|
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||||
|
|
||||||
|
# Get the actual field from the model's meta information
|
||||||
|
investigator_field = DomainApplication._meta.get_field("investigator")
|
||||||
|
|
||||||
|
# We should only be displaying staff users, in alphabetical order
|
||||||
|
expected_dropdown = list(User.objects.filter(is_staff=True))
|
||||||
|
current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset)
|
||||||
|
|
||||||
|
self.assertEqual(expected_dropdown, current_dropdown)
|
||||||
|
|
||||||
|
# Non staff users should not be in the list
|
||||||
|
self.assertNotIn(application_2, current_dropdown)
|
||||||
|
|
||||||
|
def test_investigator_list_is_alphabetically_sorted(self):
|
||||||
|
"""
|
||||||
|
This test verifies that filter list for the 'investigator'
|
||||||
|
is displayed alphabetically
|
||||||
|
"""
|
||||||
|
# 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()
|
||||||
|
investigator_user.is_staff = True
|
||||||
|
investigator_user.save()
|
||||||
|
|
||||||
|
application_2: DomainApplication = generic_domain_object("application", "AGuy")
|
||||||
|
investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get()
|
||||||
|
investigator_user_2.first_name = "AGuy"
|
||||||
|
investigator_user_2.is_staff = True
|
||||||
|
investigator_user_2.save()
|
||||||
|
|
||||||
|
application_3: DomainApplication = generic_domain_object("application", "FinalGuy")
|
||||||
|
investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get()
|
||||||
|
investigator_user_3.first_name = "FinalGuy"
|
||||||
|
investigator_user_3.is_staff = True
|
||||||
|
investigator_user_3.save()
|
||||||
|
|
||||||
|
p = "userpass"
|
||||||
|
self.client.login(username="staffuser", password=p)
|
||||||
|
request = RequestFactory().get("/")
|
||||||
|
|
||||||
|
expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email"))
|
||||||
|
|
||||||
|
# Get the actual sorted list of investigators from the lookups method
|
||||||
|
actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)]
|
||||||
|
|
||||||
|
self.assertEqual(expected_list, actual_list)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
Website.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
class DomainInvitationAdminTest(TestCase):
|
class DomainInvitationAdminTest(TestCase):
|
||||||
|
@ -1111,8 +1318,8 @@ class AuditedAdminTest(TestCase):
|
||||||
tested_fields = [
|
tested_fields = [
|
||||||
DomainApplication.authorizing_official.field,
|
DomainApplication.authorizing_official.field,
|
||||||
DomainApplication.submitter.field,
|
DomainApplication.submitter.field,
|
||||||
# DomainApplication.investigator.field,
|
DomainApplication.investigator.field,
|
||||||
# DomainApplication.creator.field,
|
DomainApplication.creator.field,
|
||||||
DomainApplication.requested_domain.field,
|
DomainApplication.requested_domain.field,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -196,11 +196,6 @@ class TestFormValidation(MockEppLib):
|
||||||
["Response must be less than 1000 characters."],
|
["Response must be less than 1000 characters."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_authorizing_official_phone_invalid(self):
|
|
||||||
"""Must be a valid phone number."""
|
|
||||||
form = AuthorizingOfficialForm(data={"phone": "boss@boss"})
|
|
||||||
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid phone number "))
|
|
||||||
|
|
||||||
def test_your_contact_email_invalid(self):
|
def test_your_contact_email_invalid(self):
|
||||||
"""must be a valid email address."""
|
"""must be a valid email address."""
|
||||||
form = YourContactForm(data={"email": "boss@boss"})
|
form = YourContactForm(data={"email": "boss@boss"})
|
||||||
|
|
|
@ -26,20 +26,52 @@ boto3_mocking.clients.register_handler("sesv2", MockSESClient)
|
||||||
# with AWS SES, so mock that out in all of these test cases
|
# with AWS SES, so mock that out in all of these test cases
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
class TestDomainApplication(TestCase):
|
class TestDomainApplication(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.started_application = completed_application(
|
||||||
|
status=DomainApplication.ApplicationStatus.STARTED, name="started.gov"
|
||||||
|
)
|
||||||
|
self.submitted_application = completed_application(
|
||||||
|
status=DomainApplication.ApplicationStatus.SUBMITTED, name="submitted.gov"
|
||||||
|
)
|
||||||
|
self.in_review_application = completed_application(
|
||||||
|
status=DomainApplication.ApplicationStatus.IN_REVIEW, name="in-review.gov"
|
||||||
|
)
|
||||||
|
self.action_needed_application = completed_application(
|
||||||
|
status=DomainApplication.ApplicationStatus.ACTION_NEEDED, name="action-needed.gov"
|
||||||
|
)
|
||||||
|
self.approved_application = completed_application(
|
||||||
|
status=DomainApplication.ApplicationStatus.APPROVED, name="approved.gov"
|
||||||
|
)
|
||||||
|
self.withdrawn_application = completed_application(
|
||||||
|
status=DomainApplication.ApplicationStatus.WITHDRAWN, name="withdrawn.gov"
|
||||||
|
)
|
||||||
|
self.rejected_application = completed_application(
|
||||||
|
status=DomainApplication.ApplicationStatus.REJECTED, name="rejected.gov"
|
||||||
|
)
|
||||||
|
self.ineligible_application = completed_application(
|
||||||
|
status=DomainApplication.ApplicationStatus.INELIGIBLE, name="ineligible.gov"
|
||||||
|
)
|
||||||
|
|
||||||
|
def assertNotRaises(self, exception_type):
|
||||||
|
"""Helper method for testing allowed transitions."""
|
||||||
|
return self.assertRaises(Exception, None, exception_type)
|
||||||
|
|
||||||
def test_empty_create_fails(self):
|
def test_empty_create_fails(self):
|
||||||
"""Can't create a completely empty domain application."""
|
"""Can't create a completely empty domain application.
|
||||||
|
NOTE: something about theexception this test raises messes up with the
|
||||||
|
atomic block in a custom tearDown method for the parent test class."""
|
||||||
with self.assertRaisesRegex(IntegrityError, "creator"):
|
with self.assertRaisesRegex(IntegrityError, "creator"):
|
||||||
DomainApplication.objects.create()
|
DomainApplication.objects.create()
|
||||||
|
|
||||||
def test_minimal_create(self):
|
def test_minimal_create(self):
|
||||||
"""Can create with just a creator."""
|
"""Can create with just a creator."""
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
application = DomainApplication.objects.create(creator=user)
|
application = DomainApplication.objects.create(creator=user)
|
||||||
self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED)
|
self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED)
|
||||||
|
|
||||||
def test_full_create(self):
|
def test_full_create(self):
|
||||||
"""Can create with all fields."""
|
"""Can create with all fields."""
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
contact = Contact.objects.create()
|
contact = Contact.objects.create()
|
||||||
com_website, _ = Website.objects.get_or_create(website="igorville.com")
|
com_website, _ = Website.objects.get_or_create(website="igorville.com")
|
||||||
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
|
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
|
||||||
|
@ -69,7 +101,7 @@ class TestDomainApplication(TestCase):
|
||||||
|
|
||||||
def test_domain_info(self):
|
def test_domain_info(self):
|
||||||
"""Can create domain info with all fields."""
|
"""Can create domain info with all fields."""
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
contact = Contact.objects.create()
|
contact = Contact.objects.create()
|
||||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
information = DomainInformation.objects.create(
|
information = DomainInformation.objects.create(
|
||||||
|
@ -95,14 +127,14 @@ class TestDomainApplication(TestCase):
|
||||||
self.assertEqual(information.id, domain.domain_info.id)
|
self.assertEqual(information.id, domain.domain_info.id)
|
||||||
|
|
||||||
def test_status_fsm_submit_fail(self):
|
def test_status_fsm_submit_fail(self):
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
application = DomainApplication.objects.create(creator=user)
|
application = DomainApplication.objects.create(creator=user)
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
# can't submit an application with a null domain name
|
# can't submit an application with a null domain name
|
||||||
application.submit()
|
application.submit()
|
||||||
|
|
||||||
def test_status_fsm_submit_succeed(self):
|
def test_status_fsm_submit_succeed(self):
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
application = DomainApplication.objects.create(creator=user, requested_domain=site)
|
application = DomainApplication.objects.create(creator=user, requested_domain=site)
|
||||||
# no submitter email so this emits a log warning
|
# no submitter email so this emits a log warning
|
||||||
|
@ -112,7 +144,7 @@ class TestDomainApplication(TestCase):
|
||||||
|
|
||||||
def test_submit_sends_email(self):
|
def test_submit_sends_email(self):
|
||||||
"""Create an application and submit it and see if email was sent."""
|
"""Create an application and submit it and see if email was sent."""
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
contact = Contact.objects.create(email="test@test.gov")
|
contact = Contact.objects.create(email="test@test.gov")
|
||||||
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||||
application = DomainApplication.objects.create(
|
application = DomainApplication.objects.create(
|
||||||
|
@ -135,320 +167,251 @@ class TestDomainApplication(TestCase):
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_transition_not_allowed_submitted_submitted(self):
|
def test_submit_transition_allowed(self):
|
||||||
"""Create an application with status submitted and call submit
|
"""
|
||||||
against transition rules"""
|
Test that calling submit from allowable statuses does raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.started_application, TransitionNotAllowed),
|
||||||
|
(self.in_review_application, TransitionNotAllowed),
|
||||||
|
(self.action_needed_application, TransitionNotAllowed),
|
||||||
|
(self.withdrawn_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
for application, exception_type in test_cases:
|
||||||
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
|
try:
|
||||||
|
application.submit()
|
||||||
|
except TransitionNotAllowed:
|
||||||
|
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
def test_submit_transition_not_allowed(self):
|
||||||
|
"""
|
||||||
|
Test that calling submit against transition rules raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.submitted_application, TransitionNotAllowed),
|
||||||
|
(self.approved_application, TransitionNotAllowed),
|
||||||
|
(self.rejected_application, TransitionNotAllowed),
|
||||||
|
(self.ineligible_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
|
for application, exception_type in test_cases:
|
||||||
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
|
with self.assertRaises(exception_type):
|
||||||
application.submit()
|
application.submit()
|
||||||
|
|
||||||
def test_transition_not_allowed_in_review_submitted(self):
|
def test_in_review_transition_allowed(self):
|
||||||
"""Create an application with status in review and call submit
|
"""
|
||||||
against transition rules"""
|
Test that calling in_review from allowable statuses does raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.submitted_application, TransitionNotAllowed),
|
||||||
|
(self.action_needed_application, TransitionNotAllowed),
|
||||||
|
(self.approved_application, TransitionNotAllowed),
|
||||||
|
(self.rejected_application, TransitionNotAllowed),
|
||||||
|
(self.ineligible_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
for application, exception_type in test_cases:
|
||||||
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
|
try:
|
||||||
|
application.in_review()
|
||||||
|
except TransitionNotAllowed:
|
||||||
|
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
def test_in_review_transition_not_allowed(self):
|
||||||
application.submit()
|
"""
|
||||||
|
Test that calling in_review against transition rules raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.started_application, TransitionNotAllowed),
|
||||||
|
(self.in_review_application, TransitionNotAllowed),
|
||||||
|
(self.withdrawn_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_submitted(self):
|
for application, exception_type in test_cases:
|
||||||
"""Create an application with status approved and call submit
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
against transition rules"""
|
with self.assertRaises(exception_type):
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.submit()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_rejected_submitted(self):
|
|
||||||
"""Create an application with status rejected and call submit
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.submit()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_ineligible_submitted(self):
|
|
||||||
"""Create an application with status ineligible and call submit
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.submit()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_started_in_review(self):
|
|
||||||
"""Create an application with status started and call in_review
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.in_review()
|
application.in_review()
|
||||||
|
|
||||||
def test_transition_not_allowed_in_review_in_review(self):
|
def test_action_needed_transition_allowed(self):
|
||||||
"""Create an application with status in review and call in_review
|
"""
|
||||||
against transition rules"""
|
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.in_review_application, TransitionNotAllowed),
|
||||||
|
(self.approved_application, TransitionNotAllowed),
|
||||||
|
(self.rejected_application, TransitionNotAllowed),
|
||||||
|
(self.ineligible_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
for application, exception_type in test_cases:
|
||||||
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
|
try:
|
||||||
|
application.action_needed()
|
||||||
|
except TransitionNotAllowed:
|
||||||
|
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
def test_action_needed_transition_not_allowed(self):
|
||||||
application.in_review()
|
"""
|
||||||
|
Test that calling action_needed against transition rules raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.started_application, TransitionNotAllowed),
|
||||||
|
(self.submitted_application, TransitionNotAllowed),
|
||||||
|
(self.action_needed_application, TransitionNotAllowed),
|
||||||
|
(self.withdrawn_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_in_review(self):
|
for application, exception_type in test_cases:
|
||||||
"""Create an application with status approved and call in_review
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
against transition rules"""
|
with self.assertRaises(exception_type):
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.in_review()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_action_needed_in_review(self):
|
|
||||||
"""Create an application with status action needed and call in_review
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.in_review()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_rejected_in_review(self):
|
|
||||||
"""Create an application with status rejected and call in_review
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.in_review()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_in_review(self):
|
|
||||||
"""Create an application with status withdrawn and call in_review
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.in_review()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_ineligible_in_review(self):
|
|
||||||
"""Create an application with status ineligible and call in_review
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.in_review()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_started_action_needed(self):
|
|
||||||
"""Create an application with status started and call action_needed
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.action_needed()
|
application.action_needed()
|
||||||
|
|
||||||
def test_transition_not_allowed_submitted_action_needed(self):
|
def test_approved_transition_allowed(self):
|
||||||
"""Create an application with status submitted and call action_needed
|
"""
|
||||||
against transition rules"""
|
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.submitted_application, TransitionNotAllowed),
|
||||||
|
(self.in_review_application, TransitionNotAllowed),
|
||||||
|
(self.action_needed_application, TransitionNotAllowed),
|
||||||
|
(self.rejected_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
for application, exception_type in test_cases:
|
||||||
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
|
try:
|
||||||
|
application.approve()
|
||||||
|
except TransitionNotAllowed:
|
||||||
|
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
def test_approved_transition_not_allowed(self):
|
||||||
application.action_needed()
|
"""
|
||||||
|
Test that calling action_needed against transition rules raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.started_application, TransitionNotAllowed),
|
||||||
|
(self.approved_application, TransitionNotAllowed),
|
||||||
|
(self.withdrawn_application, TransitionNotAllowed),
|
||||||
|
(self.ineligible_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
def test_transition_not_allowed_action_needed_action_needed(self):
|
for application, exception_type in test_cases:
|
||||||
"""Create an application with status action needed and call action_needed
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
against transition rules"""
|
with self.assertRaises(exception_type):
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.action_needed()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_action_needed(self):
|
|
||||||
"""Create an application with status approved and call action_needed
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.action_needed()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_action_needed(self):
|
|
||||||
"""Create an application with status withdrawn and call action_needed
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.action_needed()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_ineligible_action_needed(self):
|
|
||||||
"""Create an application with status ineligible and call action_needed
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.action_needed()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_started_approved(self):
|
|
||||||
"""Create an application with status started and call approve
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.approve()
|
application.approve()
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_approved(self):
|
def test_withdraw_transition_allowed(self):
|
||||||
"""Create an application with status approved and call approve
|
"""
|
||||||
against transition rules"""
|
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.submitted_application, TransitionNotAllowed),
|
||||||
|
(self.in_review_application, TransitionNotAllowed),
|
||||||
|
(self.action_needed_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
for application, exception_type in test_cases:
|
||||||
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
|
try:
|
||||||
|
application.withdraw()
|
||||||
|
except TransitionNotAllowed:
|
||||||
|
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
def test_withdraw_transition_not_allowed(self):
|
||||||
application.approve()
|
"""
|
||||||
|
Test that calling action_needed against transition rules raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.started_application, TransitionNotAllowed),
|
||||||
|
(self.approved_application, TransitionNotAllowed),
|
||||||
|
(self.withdrawn_application, TransitionNotAllowed),
|
||||||
|
(self.rejected_application, TransitionNotAllowed),
|
||||||
|
(self.ineligible_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
def test_transition_not_allowed_action_needed_approved(self):
|
for application, exception_type in test_cases:
|
||||||
"""Create an application with status action needed and call approve
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
against transition rules"""
|
with self.assertRaises(exception_type):
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.approve()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_approved(self):
|
|
||||||
"""Create an application with status withdrawn and call approve
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.approve()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_started_withdrawn(self):
|
|
||||||
"""Create an application with status started and call withdraw
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.withdraw()
|
application.withdraw()
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_withdrawn(self):
|
def test_reject_transition_allowed(self):
|
||||||
"""Create an application with status approved and call withdraw
|
"""
|
||||||
against transition rules"""
|
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.in_review_application, TransitionNotAllowed),
|
||||||
|
(self.action_needed_application, TransitionNotAllowed),
|
||||||
|
(self.approved_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
for application, exception_type in test_cases:
|
||||||
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
|
try:
|
||||||
|
application.reject()
|
||||||
|
except TransitionNotAllowed:
|
||||||
|
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
def test_reject_transition_not_allowed(self):
|
||||||
application.withdraw()
|
"""
|
||||||
|
Test that calling action_needed against transition rules raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.started_application, TransitionNotAllowed),
|
||||||
|
(self.submitted_application, TransitionNotAllowed),
|
||||||
|
(self.withdrawn_application, TransitionNotAllowed),
|
||||||
|
(self.rejected_application, TransitionNotAllowed),
|
||||||
|
(self.ineligible_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
def test_transition_not_allowed_action_needed_withdrawn(self):
|
for application, exception_type in test_cases:
|
||||||
"""Create an application with status action needed and call withdraw
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
against transition rules"""
|
with self.assertRaises(exception_type):
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.withdraw()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_rejected_withdrawn(self):
|
|
||||||
"""Create an application with status rejected and call withdraw
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.withdraw()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_withdrawn(self):
|
|
||||||
"""Create an application with status withdrawn and call withdraw
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.withdraw()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_ineligible_withdrawn(self):
|
|
||||||
"""Create an application with status ineligible and call withdraw
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.withdraw()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_started_rejected(self):
|
|
||||||
"""Create an application with status started and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject()
|
application.reject()
|
||||||
|
|
||||||
def test_transition_not_allowed_submitted_rejected(self):
|
def test_reject_with_prejudice_transition_allowed(self):
|
||||||
"""Create an application with status submitted and call reject
|
"""
|
||||||
against transition rules"""
|
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.in_review_application, TransitionNotAllowed),
|
||||||
|
(self.action_needed_application, TransitionNotAllowed),
|
||||||
|
(self.approved_application, TransitionNotAllowed),
|
||||||
|
(self.rejected_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
for application, exception_type in test_cases:
|
||||||
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
|
try:
|
||||||
|
application.reject_with_prejudice()
|
||||||
|
except TransitionNotAllowed:
|
||||||
|
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
def test_reject_with_prejudice_transition_not_allowed(self):
|
||||||
application.reject()
|
"""
|
||||||
|
Test that calling action_needed against transition rules raises TransitionNotAllowed.
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
(self.started_application, TransitionNotAllowed),
|
||||||
|
(self.submitted_application, TransitionNotAllowed),
|
||||||
|
(self.withdrawn_application, TransitionNotAllowed),
|
||||||
|
(self.ineligible_application, TransitionNotAllowed),
|
||||||
|
]
|
||||||
|
|
||||||
def test_transition_not_allowed_action_needed_rejected(self):
|
for application, exception_type in test_cases:
|
||||||
"""Create an application with status action needed and call reject
|
with self.subTest(application=application, exception_type=exception_type):
|
||||||
against transition rules"""
|
with self.assertRaises(exception_type):
|
||||||
|
application.reject_with_prejudice()
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_rejected(self):
|
|
||||||
"""Create an application with status withdrawn and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_rejected_rejected(self):
|
|
||||||
"""Create an application with status rejected and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_ineligible_rejected(self):
|
|
||||||
"""Create an application with status ineligible and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
|
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
|
||||||
"""Create an application with status approved, create a matching domain that
|
"""Create an application with status approved, create a matching domain that
|
||||||
is active, and call reject against transition rules"""
|
is active, and call reject against transition rules"""
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
|
||||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
self.approved_application.approved_domain = domain
|
||||||
application.approved_domain = domain
|
self.approved_application.save()
|
||||||
application.save()
|
|
||||||
|
|
||||||
# Define a custom implementation for is_active
|
# Define a custom implementation for is_active
|
||||||
def custom_is_active(self):
|
def custom_is_active(self):
|
||||||
|
@ -458,70 +421,15 @@ class TestDomainApplication(TestCase):
|
||||||
with patch.object(Domain, "is_active", custom_is_active):
|
with patch.object(Domain, "is_active", custom_is_active):
|
||||||
# Now, when you call is_active on Domain, it will return True
|
# Now, when you call is_active on Domain, it will return True
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.reject()
|
self.approved_application.reject()
|
||||||
|
|
||||||
def test_transition_not_allowed_started_ineligible(self):
|
|
||||||
"""Create an application with status started and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject_with_prejudice()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_submitted_ineligible(self):
|
|
||||||
"""Create an application with status submitted and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject_with_prejudice()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_action_needed_ineligible(self):
|
|
||||||
"""Create an application with status action needed and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject_with_prejudice()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_ineligible(self):
|
|
||||||
"""Create an application with status withdrawn and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject_with_prejudice()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_rejected_ineligible(self):
|
|
||||||
"""Create an application with status rejected and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject_with_prejudice()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_ineligible_ineligible(self):
|
|
||||||
"""Create an application with status ineligible and call reject
|
|
||||||
against transition rules"""
|
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
|
||||||
application.reject_with_prejudice()
|
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_ineligible_when_domain_is_active(self):
|
def test_transition_not_allowed_approved_ineligible_when_domain_is_active(self):
|
||||||
"""Create an application with status approved, create a matching domain that
|
"""Create an application with status approved, create a matching domain that
|
||||||
is active, and call reject_with_prejudice against transition rules"""
|
is active, and call reject_with_prejudice against transition rules"""
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
|
||||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
self.approved_application.approved_domain = domain
|
||||||
application.approved_domain = domain
|
self.approved_application.save()
|
||||||
application.save()
|
|
||||||
|
|
||||||
# Define a custom implementation for is_active
|
# Define a custom implementation for is_active
|
||||||
def custom_is_active(self):
|
def custom_is_active(self):
|
||||||
|
@ -531,7 +439,7 @@ class TestDomainApplication(TestCase):
|
||||||
with patch.object(Domain, "is_active", custom_is_active):
|
with patch.object(Domain, "is_active", custom_is_active):
|
||||||
# Now, when you call is_active on Domain, it will return True
|
# Now, when you call is_active on Domain, it will return True
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.reject_with_prejudice()
|
self.approved_application.reject_with_prejudice()
|
||||||
|
|
||||||
|
|
||||||
class TestPermissions(TestCase):
|
class TestPermissions(TestCase):
|
||||||
|
@ -672,6 +580,12 @@ class TestUser(TestCase):
|
||||||
|
|
||||||
class TestContact(TestCase):
|
class TestContact(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.email_for_invalid = "intern@igorville.gov"
|
||||||
|
self.invalid_user, _ = User.objects.get_or_create(
|
||||||
|
username=self.email_for_invalid, email=self.email_for_invalid, first_name="", last_name=""
|
||||||
|
)
|
||||||
|
self.invalid_contact, _ = Contact.objects.get_or_create(user=self.invalid_user)
|
||||||
|
|
||||||
self.email = "mayor@igorville.gov"
|
self.email = "mayor@igorville.gov"
|
||||||
self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski")
|
self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski")
|
||||||
self.contact, _ = Contact.objects.get_or_create(user=self.user)
|
self.contact, _ = Contact.objects.get_or_create(user=self.user)
|
||||||
|
@ -683,6 +597,31 @@ class TestContact(TestCase):
|
||||||
|
|
||||||
def test_saving_contact_updates_user_first_last_names(self):
|
def test_saving_contact_updates_user_first_last_names(self):
|
||||||
"""When a contact is updated, we propagate the changes to the linked user if it exists."""
|
"""When a contact is updated, we propagate the changes to the linked user if it exists."""
|
||||||
|
|
||||||
|
# User and Contact are created and linked as expected.
|
||||||
|
# An empty User object should create an empty contact.
|
||||||
|
self.assertEqual(self.invalid_contact.first_name, "")
|
||||||
|
self.assertEqual(self.invalid_contact.last_name, "")
|
||||||
|
self.assertEqual(self.invalid_user.first_name, "")
|
||||||
|
self.assertEqual(self.invalid_user.last_name, "")
|
||||||
|
|
||||||
|
# Manually update the contact - mimicking production (pre-existing data)
|
||||||
|
self.invalid_contact.first_name = "Joey"
|
||||||
|
self.invalid_contact.last_name = "Baloney"
|
||||||
|
self.invalid_contact.save()
|
||||||
|
|
||||||
|
# Refresh the user object to reflect the changes made in the database
|
||||||
|
self.invalid_user.refresh_from_db()
|
||||||
|
|
||||||
|
# Updating the contact's first and last names propagate to the user
|
||||||
|
self.assertEqual(self.invalid_contact.first_name, "Joey")
|
||||||
|
self.assertEqual(self.invalid_contact.last_name, "Baloney")
|
||||||
|
self.assertEqual(self.invalid_user.first_name, "Joey")
|
||||||
|
self.assertEqual(self.invalid_user.last_name, "Baloney")
|
||||||
|
|
||||||
|
def test_saving_contact_does_not_update_user_first_last_names(self):
|
||||||
|
"""When a contact is updated, we avoid propagating the changes to the linked user if it already has a value"""
|
||||||
|
|
||||||
# User and Contact are created and linked as expected
|
# User and Contact are created and linked as expected
|
||||||
self.assertEqual(self.contact.first_name, "Jeff")
|
self.assertEqual(self.contact.first_name, "Jeff")
|
||||||
self.assertEqual(self.contact.last_name, "Lebowski")
|
self.assertEqual(self.contact.last_name, "Lebowski")
|
||||||
|
@ -699,11 +638,11 @@ class TestContact(TestCase):
|
||||||
# Updating the contact's first and last names propagate to the user
|
# Updating the contact's first and last names propagate to the user
|
||||||
self.assertEqual(self.contact.first_name, "Joey")
|
self.assertEqual(self.contact.first_name, "Joey")
|
||||||
self.assertEqual(self.contact.last_name, "Baloney")
|
self.assertEqual(self.contact.last_name, "Baloney")
|
||||||
self.assertEqual(self.user.first_name, "Joey")
|
self.assertEqual(self.user.first_name, "Jeff")
|
||||||
self.assertEqual(self.user.last_name, "Baloney")
|
self.assertEqual(self.user.last_name, "Lebowski")
|
||||||
|
|
||||||
def test_saving_contact_does_not_update_user_email(self):
|
def test_saving_contact_does_not_update_user_email(self):
|
||||||
"""When a contact's email is updated, the change is not propagated to the lined user."""
|
"""When a contact's email is updated, the change is not propagated to the user."""
|
||||||
self.contact.email = "joey.baloney@diaperville.com"
|
self.contact.email = "joey.baloney@diaperville.com"
|
||||||
self.contact.save()
|
self.contact.save()
|
||||||
|
|
||||||
|
@ -713,3 +652,16 @@ class TestContact(TestCase):
|
||||||
# Updating the contact's email does not propagate
|
# Updating the contact's email does not propagate
|
||||||
self.assertEqual(self.contact.email, "joey.baloney@diaperville.com")
|
self.assertEqual(self.contact.email, "joey.baloney@diaperville.com")
|
||||||
self.assertEqual(self.user.email, "mayor@igorville.gov")
|
self.assertEqual(self.user.email, "mayor@igorville.gov")
|
||||||
|
|
||||||
|
def test_saving_contact_does_not_update_user_email_when_none(self):
|
||||||
|
"""When a contact's email is updated, and the first/last name is none,
|
||||||
|
the change is not propagated to the user."""
|
||||||
|
self.invalid_contact.email = "joey.baloney@diaperville.com"
|
||||||
|
self.invalid_contact.save()
|
||||||
|
|
||||||
|
# Refresh the user object to reflect the changes made in the database
|
||||||
|
self.invalid_user.refresh_from_db()
|
||||||
|
|
||||||
|
# Updating the contact's email does not propagate
|
||||||
|
self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com")
|
||||||
|
self.assertEqual(self.invalid_user.email, "intern@igorville.gov")
|
||||||
|
|
|
@ -763,7 +763,6 @@ class TestRegistrantContacts(MockEppLib):
|
||||||
self.assertEqual(expected_contact.email, actual_contact.email)
|
self.assertEqual(expected_contact.email, actual_contact.email)
|
||||||
|
|
||||||
def test_convert_public_contact_to_epp(self):
|
def test_convert_public_contact_to_epp(self):
|
||||||
self.maxDiff = None
|
|
||||||
domain, _ = Domain.objects.get_or_create(name="freeman.gov")
|
domain, _ = Domain.objects.get_or_create(name="freeman.gov")
|
||||||
dummy_contact = domain.get_default_security_contact()
|
dummy_contact = domain.get_default_security_contact()
|
||||||
test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__
|
test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__
|
||||||
|
|
|
@ -152,7 +152,6 @@ class CsvReportsTest(TestCase):
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_load_federal_report(self):
|
def test_load_federal_report(self):
|
||||||
"""Tests the get_current_federal api endpoint"""
|
"""Tests the get_current_federal api endpoint"""
|
||||||
self.maxDiff = None
|
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_instance = mock_client.return_value
|
mock_client_instance = mock_client.return_value
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from .common import MockEppLib, completed_application, create_user # type: ignore
|
from .common import MockEppLib, completed_application, create_user # type: ignore
|
||||||
|
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
|
||||||
|
@ -259,7 +258,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
ao_form["authorizing_official-last_name"] = "Tester ATO"
|
ao_form["authorizing_official-last_name"] = "Tester ATO"
|
||||||
ao_form["authorizing_official-title"] = "Chief Tester"
|
ao_form["authorizing_official-title"] = "Chief Tester"
|
||||||
ao_form["authorizing_official-email"] = "testy@town.com"
|
ao_form["authorizing_official-email"] = "testy@town.com"
|
||||||
ao_form["authorizing_official-phone"] = "(201) 555 5555"
|
|
||||||
|
|
||||||
# test next button
|
# test next button
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
@ -270,7 +268,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
self.assertEqual(application.authorizing_official.last_name, "Tester ATO")
|
self.assertEqual(application.authorizing_official.last_name, "Tester ATO")
|
||||||
self.assertEqual(application.authorizing_official.title, "Chief Tester")
|
self.assertEqual(application.authorizing_official.title, "Chief Tester")
|
||||||
self.assertEqual(application.authorizing_official.email, "testy@town.com")
|
self.assertEqual(application.authorizing_official.email, "testy@town.com")
|
||||||
self.assertEqual(application.authorizing_official.phone, "(201) 555 5555")
|
|
||||||
# the post request should return a redirect to the next form in
|
# the post request should return a redirect to the next form in
|
||||||
# the application
|
# the application
|
||||||
self.assertEqual(ao_result.status_code, 302)
|
self.assertEqual(ao_result.status_code, 302)
|
||||||
|
@ -460,7 +457,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
self.assertContains(review_page, "Tester ATO")
|
self.assertContains(review_page, "Tester ATO")
|
||||||
self.assertContains(review_page, "Chief Tester")
|
self.assertContains(review_page, "Chief Tester")
|
||||||
self.assertContains(review_page, "testy@town.com")
|
self.assertContains(review_page, "testy@town.com")
|
||||||
self.assertContains(review_page, "(201) 555-5555")
|
|
||||||
self.assertContains(review_page, "city.com")
|
self.assertContains(review_page, "city.com")
|
||||||
self.assertContains(review_page, "city.gov")
|
self.assertContains(review_page, "city.gov")
|
||||||
self.assertContains(review_page, "city1.gov")
|
self.assertContains(review_page, "city1.gov")
|
||||||
|
@ -888,7 +884,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
ao_form["authorizing_official-last_name"] = "Tester ATO"
|
ao_form["authorizing_official-last_name"] = "Tester ATO"
|
||||||
ao_form["authorizing_official-title"] = "Chief Tester"
|
ao_form["authorizing_official-title"] = "Chief Tester"
|
||||||
ao_form["authorizing_official-email"] = "testy@town.com"
|
ao_form["authorizing_official-email"] = "testy@town.com"
|
||||||
ao_form["authorizing_official-phone"] = "(201) 555 5555"
|
|
||||||
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
ao_result = ao_form.submit()
|
ao_result = ao_form.submit()
|
||||||
|
@ -1339,6 +1334,12 @@ class TestDomainDetail(TestDomainOverview):
|
||||||
|
|
||||||
|
|
||||||
class TestDomainManagers(TestDomainOverview):
|
class TestDomainManagers(TestDomainOverview):
|
||||||
|
def tearDown(self):
|
||||||
|
"""Ensure that the user has its original permissions"""
|
||||||
|
super().tearDown()
|
||||||
|
self.user.is_staff = False
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
def test_domain_managers(self):
|
def test_domain_managers(self):
|
||||||
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||||
self.assertContains(response, "Domain managers")
|
self.assertContains(response, "Domain managers")
|
||||||
|
@ -1442,6 +1443,7 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
add_page.form["email"] = email_address
|
add_page.form["email"] = email_address
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
add_page.form.submit()
|
add_page.form.submit()
|
||||||
|
|
||||||
# check the mock instance to see if `send_email` was called right
|
# check the mock instance to see if `send_email` was called right
|
||||||
mock_client_instance.send_email.assert_called_once_with(
|
mock_client_instance.send_email.assert_called_once_with(
|
||||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
@ -1449,6 +1451,189 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
Content=ANY,
|
Content=ANY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_domain_invitation_email_has_email_as_requester_non_existent(self):
|
||||||
|
"""Inviting a non existent user sends them an email, with email as the name."""
|
||||||
|
# make sure there is no user with this email
|
||||||
|
email_address = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=email_address).delete()
|
||||||
|
|
||||||
|
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client_instance = mock_client.return_value
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
add_page.form["email"] = email_address
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
add_page.form.submit()
|
||||||
|
|
||||||
|
# check the mock instance to see if `send_email` was called right
|
||||||
|
mock_client_instance.send_email.assert_called_once_with(
|
||||||
|
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
Destination={"ToAddresses": [email_address]},
|
||||||
|
Content=ANY,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the arguments passed to send_email method
|
||||||
|
_, kwargs = mock_client_instance.send_email.call_args
|
||||||
|
|
||||||
|
# Extract the email content, and check that the message is as we expect
|
||||||
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
self.assertIn("info@example.com", email_content)
|
||||||
|
|
||||||
|
# Check that the requesters first/last name do not exist
|
||||||
|
self.assertNotIn("First", email_content)
|
||||||
|
self.assertNotIn("Last", email_content)
|
||||||
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_domain_invitation_email_has_email_as_requester(self):
|
||||||
|
"""Inviting a user sends them an email, with email as the name."""
|
||||||
|
# Create a fake user object
|
||||||
|
email_address = "mayor@igorville.gov"
|
||||||
|
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
|
||||||
|
|
||||||
|
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client_instance = mock_client.return_value
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
add_page.form["email"] = email_address
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
add_page.form.submit()
|
||||||
|
|
||||||
|
# check the mock instance to see if `send_email` was called right
|
||||||
|
mock_client_instance.send_email.assert_called_once_with(
|
||||||
|
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
Destination={"ToAddresses": [email_address]},
|
||||||
|
Content=ANY,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the arguments passed to send_email method
|
||||||
|
_, kwargs = mock_client_instance.send_email.call_args
|
||||||
|
|
||||||
|
# Extract the email content, and check that the message is as we expect
|
||||||
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
self.assertIn("info@example.com", email_content)
|
||||||
|
|
||||||
|
# Check that the requesters first/last name do not exist
|
||||||
|
self.assertNotIn("First", email_content)
|
||||||
|
self.assertNotIn("Last", email_content)
|
||||||
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_domain_invitation_email_has_email_as_requester_staff(self):
|
||||||
|
"""Inviting a user sends them an email, with email as the name."""
|
||||||
|
# Create a fake user object
|
||||||
|
email_address = "mayor@igorville.gov"
|
||||||
|
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
|
||||||
|
|
||||||
|
# Make sure the user is staff
|
||||||
|
self.user.is_staff = True
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client_instance = mock_client.return_value
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
add_page.form["email"] = email_address
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
add_page.form.submit()
|
||||||
|
|
||||||
|
# check the mock instance to see if `send_email` was called right
|
||||||
|
mock_client_instance.send_email.assert_called_once_with(
|
||||||
|
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
Destination={"ToAddresses": [email_address]},
|
||||||
|
Content=ANY,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the arguments passed to send_email method
|
||||||
|
_, kwargs = mock_client_instance.send_email.call_args
|
||||||
|
|
||||||
|
# Extract the email content, and check that the message is as we expect
|
||||||
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
self.assertIn("help@get.gov", email_content)
|
||||||
|
|
||||||
|
# Check that the requesters first/last name do not exist
|
||||||
|
self.assertNotIn("First", email_content)
|
||||||
|
self.assertNotIn("Last", email_content)
|
||||||
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_domain_invitation_email_displays_error_non_existent(self):
|
||||||
|
"""Inviting a non existent user sends them an email, with email as the name."""
|
||||||
|
# make sure there is no user with this email
|
||||||
|
email_address = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=email_address).delete()
|
||||||
|
|
||||||
|
# Give the user who is sending the email an invalid email address
|
||||||
|
self.user.email = ""
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
|
||||||
|
mock_error_message = MagicMock()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
with patch("django.contrib.messages.error") as mock_error_message:
|
||||||
|
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
add_page.form["email"] = email_address
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
add_page.form.submit().follow()
|
||||||
|
|
||||||
|
expected_message_content = "Can't send invitation email. No email is associated with your account."
|
||||||
|
|
||||||
|
# Grab the message content
|
||||||
|
returned_error_message = mock_error_message.call_args[0][1]
|
||||||
|
|
||||||
|
# Check that the message content is what we expect
|
||||||
|
self.assertEqual(expected_message_content, returned_error_message)
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_domain_invitation_email_displays_error(self):
|
||||||
|
"""When the requesting user has no email, an error is displayed"""
|
||||||
|
# make sure there is no user with this email
|
||||||
|
# Create a fake user object
|
||||||
|
email_address = "mayor@igorville.gov"
|
||||||
|
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
|
||||||
|
|
||||||
|
# Give the user who is sending the email an invalid email address
|
||||||
|
self.user.email = ""
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
|
||||||
|
mock_error_message = MagicMock()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
with patch("django.contrib.messages.error") as mock_error_message:
|
||||||
|
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
add_page.form["email"] = email_address
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
add_page.form.submit().follow()
|
||||||
|
|
||||||
|
expected_message_content = "Can't send invitation email. No email is associated with your account."
|
||||||
|
|
||||||
|
# Grab the message content
|
||||||
|
returned_error_message = mock_error_message.call_args[0][1]
|
||||||
|
|
||||||
|
# Check that the message content is what we expect
|
||||||
|
self.assertEqual(expected_message_content, returned_error_message)
|
||||||
|
|
||||||
def test_domain_invitation_cancel(self):
|
def test_domain_invitation_cancel(self):
|
||||||
"""Posting to the delete view deletes an invitation."""
|
"""Posting to the delete view deletes an invitation."""
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
@ -2233,6 +2418,33 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
||||||
home_page = self.app.get("/")
|
home_page = self.app.get("/")
|
||||||
self.assertContains(home_page, "Withdrawn")
|
self.assertContains(home_page, "Withdrawn")
|
||||||
|
|
||||||
|
def test_application_withdraw_no_permissions(self):
|
||||||
|
"""Can't withdraw applications as a restricted user."""
|
||||||
|
self.user.status = User.RESTRICTED
|
||||||
|
self.user.save()
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
|
||||||
|
application.save()
|
||||||
|
|
||||||
|
home_page = self.app.get("/")
|
||||||
|
self.assertContains(home_page, "city.gov")
|
||||||
|
# click the "Manage" link
|
||||||
|
detail_page = home_page.click("Manage", index=0)
|
||||||
|
self.assertContains(detail_page, "city.gov")
|
||||||
|
self.assertContains(detail_page, "city1.gov")
|
||||||
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
|
self.assertContains(detail_page, "testy@town.com")
|
||||||
|
self.assertContains(detail_page, "Admin Tester")
|
||||||
|
self.assertContains(detail_page, "Status:")
|
||||||
|
# Restricted user trying to withdraw results in 403 error
|
||||||
|
with less_console_noise():
|
||||||
|
for url_name in [
|
||||||
|
"application-withdraw-confirmation",
|
||||||
|
"application-withdrawn",
|
||||||
|
]:
|
||||||
|
with self.subTest(url_name=url_name):
|
||||||
|
page = self.client.get(reverse(url_name, kwargs={"pk": application.pk}))
|
||||||
|
self.assertEqual(page.status_code, 403)
|
||||||
|
|
||||||
def test_application_status_no_permissions(self):
|
def test_application_status_no_permissions(self):
|
||||||
"""Can't access applications without being the creator."""
|
"""Can't access applications without being the creator."""
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
|
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
|
||||||
|
|
|
@ -13,7 +13,11 @@ from registrar.models import DomainApplication
|
||||||
from registrar.utility import StrEnum
|
from registrar.utility import StrEnum
|
||||||
from registrar.views.utility import StepsHelper
|
from registrar.views.utility import StepsHelper
|
||||||
|
|
||||||
from .utility import DomainApplicationPermissionView, ApplicationWizardPermissionView
|
from .utility import (
|
||||||
|
DomainApplicationPermissionView,
|
||||||
|
DomainApplicationPermissionWithdrawView,
|
||||||
|
ApplicationWizardPermissionView,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -544,7 +548,7 @@ class ApplicationStatus(DomainApplicationPermissionView):
|
||||||
template_name = "application_status.html"
|
template_name = "application_status.html"
|
||||||
|
|
||||||
|
|
||||||
class ApplicationWithdrawConfirmation(DomainApplicationPermissionView):
|
class ApplicationWithdrawConfirmation(DomainApplicationPermissionWithdrawView):
|
||||||
"""This page will ask user to confirm if they want to withdraw
|
"""This page will ask user to confirm if they want to withdraw
|
||||||
|
|
||||||
The DomainApplicationPermissionView restricts access so that only the
|
The DomainApplicationPermissionView restricts access so that only the
|
||||||
|
@ -554,7 +558,7 @@ class ApplicationWithdrawConfirmation(DomainApplicationPermissionView):
|
||||||
template_name = "application_withdraw_confirmation.html"
|
template_name = "application_withdraw_confirmation.html"
|
||||||
|
|
||||||
|
|
||||||
class ApplicationWithdrawn(DomainApplicationPermissionView):
|
class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView):
|
||||||
# this view renders no template
|
# this view renders no template
|
||||||
template_name = ""
|
template_name = ""
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ from django.views.generic.edit import FormMixin
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
DomainInformation,
|
|
||||||
DomainInvitation,
|
DomainInvitation,
|
||||||
User,
|
User,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
|
@ -644,21 +643,27 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
"""Get an absolute URL for this domain."""
|
"""Get an absolute URL for this domain."""
|
||||||
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
||||||
|
|
||||||
def _send_domain_invitation_email(self, email: str, add_success=True):
|
def _send_domain_invitation_email(self, email: str, requester: User, add_success=True):
|
||||||
"""Performs the sending of the domain invitation email,
|
"""Performs the sending of the domain invitation email,
|
||||||
does not make a domain information object
|
does not make a domain information object
|
||||||
email: string- email to send to
|
email: string- email to send to
|
||||||
add_success: bool- default True indicates:
|
add_success: bool- default True indicates:
|
||||||
adding a success message to the view if the email sending succeeds"""
|
adding a success message to the view if the email sending succeeds"""
|
||||||
# created a new invitation in the database, so send an email
|
|
||||||
domainInfoResults = DomainInformation.objects.filter(domain=self.object)
|
# Set a default email address to send to for staff
|
||||||
domainInfo = domainInfoResults.first()
|
requester_email = "help@get.gov"
|
||||||
first = ""
|
|
||||||
last = ""
|
# Check if the email requester has a valid email address
|
||||||
if domainInfo is not None:
|
if not requester.is_staff and requester.email is not None and requester.email.strip() != "":
|
||||||
first = domainInfo.creator.first_name
|
requester_email = requester.email
|
||||||
last = domainInfo.creator.last_name
|
elif not requester.is_staff:
|
||||||
full_name = f"{first} {last}"
|
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
||||||
|
logger.error(
|
||||||
|
f"Can't send email to '{email}' on domain '{self.object}'."
|
||||||
|
f"No email exists for the requester '{requester.username}'.",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
send_templated_email(
|
send_templated_email(
|
||||||
|
@ -668,7 +673,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
context={
|
context={
|
||||||
"domain_url": self._domain_abs_url(),
|
"domain_url": self._domain_abs_url(),
|
||||||
"domain": self.object,
|
"domain": self.object,
|
||||||
"full_name": full_name,
|
"requester_email": requester_email,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except EmailSendingError:
|
except EmailSendingError:
|
||||||
|
@ -683,7 +688,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
if add_success:
|
if add_success:
|
||||||
messages.success(self.request, f"Invited {email} to this domain.")
|
messages.success(self.request, f"Invited {email} to this domain.")
|
||||||
|
|
||||||
def _make_invitation(self, email_address: str):
|
def _make_invitation(self, email_address: str, requester: User):
|
||||||
"""Make a Domain invitation for this email and redirect with a message."""
|
"""Make a Domain invitation for this email and redirect with a message."""
|
||||||
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||||
if not created:
|
if not created:
|
||||||
|
@ -693,21 +698,22 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
f"{email_address} has already been invited to this domain.",
|
f"{email_address} has already been invited to this domain.",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._send_domain_invitation_email(email=email_address)
|
self._send_domain_invitation_email(email=email_address, requester=requester)
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Add the specified user on this domain."""
|
"""Add the specified user on this domain."""
|
||||||
requested_email = form.cleaned_data["email"]
|
requested_email = form.cleaned_data["email"]
|
||||||
|
requester = self.request.user
|
||||||
# look up a user with that email
|
# look up a user with that email
|
||||||
try:
|
try:
|
||||||
requested_user = User.objects.get(email=requested_email)
|
requested_user = User.objects.get(email=requested_email)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
# no matching user, go make an invitation
|
# no matching user, go make an invitation
|
||||||
return self._make_invitation(requested_email)
|
return self._make_invitation(requested_email, requester)
|
||||||
else:
|
else:
|
||||||
# if user already exists then just send an email
|
# if user already exists then just send an email
|
||||||
self._send_domain_invitation_email(requested_email, add_success=False)
|
self._send_domain_invitation_email(requested_email, requester, add_success=False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
UserDomainRole.objects.create(
|
UserDomainRole.objects.create(
|
||||||
|
|
|
@ -4,6 +4,7 @@ from .always_404 import always_404
|
||||||
from .permission_views import (
|
from .permission_views import (
|
||||||
DomainPermissionView,
|
DomainPermissionView,
|
||||||
DomainApplicationPermissionView,
|
DomainApplicationPermissionView,
|
||||||
|
DomainApplicationPermissionWithdrawView,
|
||||||
DomainInvitationPermissionDeleteView,
|
DomainInvitationPermissionDeleteView,
|
||||||
ApplicationWizardPermissionView,
|
ApplicationWizardPermissionView,
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,7 +26,8 @@ class PermissionsLoginMixin(PermissionRequiredMixin):
|
||||||
|
|
||||||
class DomainPermission(PermissionsLoginMixin):
|
class DomainPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
"""Does the logged-in user have access to this domain?"""
|
"""Permission mixin that redirects to domain if user has access,
|
||||||
|
otherwise 403"""
|
||||||
|
|
||||||
def has_permission(self):
|
def has_permission(self):
|
||||||
"""Check if this user has access to this domain.
|
"""Check if this user has access to this domain.
|
||||||
|
@ -134,7 +135,8 @@ class DomainPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
class DomainApplicationPermission(PermissionsLoginMixin):
|
class DomainApplicationPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
"""Does the logged-in user have access to this domain application?"""
|
"""Permission mixin that redirects to domain application if user
|
||||||
|
has access, otherwise 403"""
|
||||||
|
|
||||||
def has_permission(self):
|
def has_permission(self):
|
||||||
"""Check if this user has access to this domain application.
|
"""Check if this user has access to this domain application.
|
||||||
|
@ -154,9 +156,33 @@ class DomainApplicationPermission(PermissionsLoginMixin):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DomainApplicationPermissionWithdraw(PermissionsLoginMixin):
|
||||||
|
|
||||||
|
"""Permission mixin that redirects to withdraw action on domain application
|
||||||
|
if user has access, otherwise 403"""
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
"""Check if this user has access to withdraw this domain application."""
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# user needs to be the creator of the application
|
||||||
|
# this query is empty if there isn't a domain application with this
|
||||||
|
# id and this user as creator
|
||||||
|
if not DomainApplication.objects.filter(creator=self.request.user, id=self.kwargs["pk"]).exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Restricted users should not be able to withdraw domain requests
|
||||||
|
if self.request.user.is_restricted():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ApplicationWizardPermission(PermissionsLoginMixin):
|
class ApplicationWizardPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
"""Does the logged-in user have permission to start or edit an application?"""
|
"""Permission mixin that redirects to start or edit domain application if
|
||||||
|
user has access, otherwise 403"""
|
||||||
|
|
||||||
def has_permission(self):
|
def has_permission(self):
|
||||||
"""Check if this user has permission to start or edit an application.
|
"""Check if this user has permission to start or edit an application.
|
||||||
|
@ -173,7 +199,8 @@ class ApplicationWizardPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
class DomainInvitationPermission(PermissionsLoginMixin):
|
class DomainInvitationPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
"""Does the logged-in user have access to this domain invitation?
|
"""Permission mixin that redirects to domain invitation if user has
|
||||||
|
access, otherwise 403"
|
||||||
|
|
||||||
A user has access to a domain invitation if they have a role on the
|
A user has access to a domain invitation if they have a role on the
|
||||||
associated domain.
|
associated domain.
|
||||||
|
|
|
@ -8,6 +8,7 @@ from registrar.models import Domain, DomainApplication, DomainInvitation
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
DomainPermission,
|
DomainPermission,
|
||||||
DomainApplicationPermission,
|
DomainApplicationPermission,
|
||||||
|
DomainApplicationPermissionWithdraw,
|
||||||
DomainInvitationPermission,
|
DomainInvitationPermission,
|
||||||
ApplicationWizardPermission,
|
ApplicationWizardPermission,
|
||||||
)
|
)
|
||||||
|
@ -74,6 +75,26 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdraw, DetailView, abc.ABC):
|
||||||
|
|
||||||
|
"""Abstract base view for domain application withdraw function
|
||||||
|
|
||||||
|
This abstract view cannot be instantiated. Actual views must specify
|
||||||
|
`template_name`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# DetailView property for what model this is viewing
|
||||||
|
model = DomainApplication
|
||||||
|
# variable name in template context for the model object
|
||||||
|
context_object_name = "domainapplication"
|
||||||
|
|
||||||
|
# Abstract property enforces NotImplementedError on an attribute.
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def template_name(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC):
|
class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC):
|
||||||
|
|
||||||
"""Abstract base view for the application form that enforces permissions
|
"""Abstract base view for the application form that enforces permissions
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue