mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-02 02:28:32 +02:00
merge and lint
This commit is contained in:
commit
ec311b89cf
12 changed files with 749 additions and 132 deletions
|
@ -1,18 +1,20 @@
|
||||||
|
from datetime import date
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.http import HttpResponse
|
from django.db.models import Value, CharField
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
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
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http.response import HttpResponseRedirect
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from dateutil.relativedelta import relativedelta # type: ignore
|
||||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models.domain import Domain
|
from registrar.models import Contact, Domain, DomainApplication, DraftDomain, User, Website
|
||||||
from registrar.models.user import User
|
|
||||||
from registrar.utility import csv_export
|
from registrar.utility import csv_export
|
||||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||||
from django.contrib.admin.views.main import ORDER_VAR
|
from django.contrib.admin.views.main import ORDER_VAR
|
||||||
|
@ -23,6 +25,7 @@ from django_fsm import TransitionNotAllowed # type: ignore
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,41 +121,52 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
||||||
|
|
||||||
|
|
||||||
class AdminSortFields:
|
class AdminSortFields:
|
||||||
def get_queryset(db_field):
|
_name_sort = ["first_name", "last_name", "email"]
|
||||||
|
|
||||||
|
# Define a mapping of field names to model querysets and sort expressions.
|
||||||
|
# A dictionary is used for specificity, but the downside is some degree of repetition.
|
||||||
|
# To eliminate this, this list can be generated dynamically but the readability of that
|
||||||
|
# is impacted.
|
||||||
|
sort_mapping = {
|
||||||
|
# == Contact == #
|
||||||
|
"other_contacts": (Contact, _name_sort),
|
||||||
|
"authorizing_official": (Contact, _name_sort),
|
||||||
|
"submitter": (Contact, _name_sort),
|
||||||
|
# == User == #
|
||||||
|
"creator": (User, _name_sort),
|
||||||
|
"user": (User, _name_sort),
|
||||||
|
"investigator": (User, _name_sort),
|
||||||
|
# == Website == #
|
||||||
|
"current_websites": (Website, "website"),
|
||||||
|
"alternative_domains": (Website, "website"),
|
||||||
|
# == DraftDomain == #
|
||||||
|
"requested_domain": (DraftDomain, "name"),
|
||||||
|
# == DomainApplication == #
|
||||||
|
"domain_application": (DomainApplication, "requested_domain__name"),
|
||||||
|
# == Domain == #
|
||||||
|
"domain": (Domain, "name"),
|
||||||
|
"approved_domain": (Domain, "name"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, db_field):
|
||||||
"""This is a helper function for formfield_for_manytomany and formfield_for_foreignkey"""
|
"""This is a helper function for formfield_for_manytomany and formfield_for_foreignkey"""
|
||||||
# customize sorting
|
queryset_info = cls.sort_mapping.get(db_field.name, None)
|
||||||
if db_field.name in (
|
if queryset_info is None:
|
||||||
"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
|
return None
|
||||||
|
|
||||||
|
# Grab the model we want to order, and grab how we want to order it
|
||||||
|
model, order_by = queryset_info
|
||||||
|
match db_field.name:
|
||||||
|
case "investigator":
|
||||||
|
# We should only return users who are staff.
|
||||||
|
return model.objects.filter(is_staff=True).order_by(*order_by)
|
||||||
|
case _:
|
||||||
|
if isinstance(order_by, list) or isinstance(order_by, tuple):
|
||||||
|
return model.objects.order_by(*order_by)
|
||||||
|
else:
|
||||||
|
return model.objects.order_by(order_by)
|
||||||
|
|
||||||
|
|
||||||
class AuditedAdmin(admin.ModelAdmin):
|
class AuditedAdmin(admin.ModelAdmin):
|
||||||
"""Custom admin to make auditing easier."""
|
"""Custom admin to make auditing easier."""
|
||||||
|
@ -170,9 +184,14 @@ class AuditedAdmin(admin.ModelAdmin):
|
||||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||||
"""customize the behavior of formfields with manytomany relationships. the customized
|
"""customize the behavior of formfields with manytomany relationships. the customized
|
||||||
behavior includes sorting of objects in lists as well as customizing helper text"""
|
behavior includes sorting of objects in lists as well as customizing helper text"""
|
||||||
|
|
||||||
|
# Define a queryset. Note that in the super of this,
|
||||||
|
# a new queryset will only be generated if one does not exist.
|
||||||
|
# Thus, the order in which we define queryset matters.
|
||||||
queryset = AdminSortFields.get_queryset(db_field)
|
queryset = AdminSortFields.get_queryset(db_field)
|
||||||
if queryset:
|
if queryset:
|
||||||
kwargs["queryset"] = queryset
|
kwargs["queryset"] = queryset
|
||||||
|
|
||||||
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
|
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
# customize the help text for all formfields for manytomany
|
# customize the help text for all formfields for manytomany
|
||||||
formfield.help_text = (
|
formfield.help_text = (
|
||||||
|
@ -182,11 +201,16 @@ class AuditedAdmin(admin.ModelAdmin):
|
||||||
return formfield
|
return formfield
|
||||||
|
|
||||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
"""customize the behavior of formfields with foreign key relationships. this will customize
|
"""Customize the behavior of formfields with foreign key relationships. This will customize
|
||||||
the behavior of selects. customized behavior includes sorting of objects in list"""
|
the behavior of selects. Customized behavior includes sorting of objects in list."""
|
||||||
|
|
||||||
|
# Define a queryset. Note that in the super of this,
|
||||||
|
# a new queryset will only be generated if one does not exist.
|
||||||
|
# Thus, the order in which we define queryset matters.
|
||||||
queryset = AdminSortFields.get_queryset(db_field)
|
queryset = AdminSortFields.get_queryset(db_field)
|
||||||
if queryset:
|
if queryset:
|
||||||
kwargs["queryset"] = queryset
|
kwargs["queryset"] = queryset
|
||||||
|
|
||||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -221,7 +245,6 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
|
||||||
parameter_value: string}
|
parameter_value: string}
|
||||||
TODO: convert investigator id to investigator username
|
TODO: convert investigator id to investigator username
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filters = []
|
filters = []
|
||||||
# Retrieve the filter parameters
|
# Retrieve the filter parameters
|
||||||
for param in request.GET.keys():
|
for param in request.GET.keys():
|
||||||
|
@ -265,6 +288,14 @@ class UserContactInline(admin.StackedInline):
|
||||||
class MyUserAdmin(BaseUserAdmin):
|
class MyUserAdmin(BaseUserAdmin):
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
model = models.User
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
_meta = Meta()
|
||||||
|
|
||||||
inlines = [UserContactInline]
|
inlines = [UserContactInline]
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
|
@ -353,6 +384,42 @@ class MyUserAdmin(BaseUserAdmin):
|
||||||
# in autocomplete_fields for user
|
# in autocomplete_fields for user
|
||||||
ordering = ["first_name", "last_name", "email"]
|
ordering = ["first_name", "last_name", "email"]
|
||||||
|
|
||||||
|
def get_search_results(self, request, queryset, search_term):
|
||||||
|
"""
|
||||||
|
Override for get_search_results. This affects any upstream model using autocomplete_fields,
|
||||||
|
such as DomainApplication. This is because autocomplete_fields uses an API call to fetch data,
|
||||||
|
and this fetch comes from this method.
|
||||||
|
"""
|
||||||
|
# Custom filtering logic
|
||||||
|
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
|
||||||
|
|
||||||
|
# If we aren't given a request to modify, we shouldn't try to
|
||||||
|
if request is None or not hasattr(request, "GET"):
|
||||||
|
return queryset, use_distinct
|
||||||
|
|
||||||
|
# Otherwise, lets modify it!
|
||||||
|
request_get = request.GET
|
||||||
|
|
||||||
|
# The request defines model name and field name.
|
||||||
|
# For instance, model_name could be "DomainApplication"
|
||||||
|
# and field_name could be "investigator".
|
||||||
|
model_name = request_get.get("model_name", None)
|
||||||
|
field_name = request_get.get("field_name", None)
|
||||||
|
|
||||||
|
# Make sure we're only modifying requests from these models.
|
||||||
|
models_to_target = {"domainapplication"}
|
||||||
|
if model_name in models_to_target:
|
||||||
|
# Define rules per field
|
||||||
|
match field_name:
|
||||||
|
case "investigator":
|
||||||
|
# We should not display investigators who don't have a staff role
|
||||||
|
queryset = queryset.filter(is_staff=True)
|
||||||
|
case _:
|
||||||
|
# In the default case, do nothing
|
||||||
|
pass
|
||||||
|
|
||||||
|
return queryset, use_distinct
|
||||||
|
|
||||||
# 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):
|
||||||
|
@ -417,6 +484,9 @@ class ContactAdmin(ListHeaderAdmin):
|
||||||
"contact",
|
"contact",
|
||||||
"email",
|
"email",
|
||||||
]
|
]
|
||||||
|
# this ordering effects the ordering of results
|
||||||
|
# in autocomplete_fields for user
|
||||||
|
ordering = ["first_name", "last_name", "email"]
|
||||||
|
|
||||||
# We name the custom prop 'contact' because linter
|
# We name the custom prop 'contact' because linter
|
||||||
# is not allowing a short_description attr on it
|
# is not allowing a short_description attr on it
|
||||||
|
@ -752,8 +822,23 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
"""Lookup reimplementation, gets users of is_staff.
|
"""Lookup reimplementation, gets users of is_staff.
|
||||||
Returns a list of tuples consisting of (user.id, user)
|
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")
|
# Select all investigators that are staff, then order by name and email
|
||||||
return [(user.id, user) for user in privileged_users]
|
privileged_users = (
|
||||||
|
DomainApplication.objects.select_related("investigator")
|
||||||
|
.filter(investigator__is_staff=True)
|
||||||
|
.order_by("investigator__first_name", "investigator__last_name", "investigator__email")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Annotate the full name and return a values list that lookups can use
|
||||||
|
privileged_users_annotated = privileged_users.annotate(
|
||||||
|
full_name=Coalesce(
|
||||||
|
Concat("investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()),
|
||||||
|
"investigator__email",
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).values_list("investigator__id", "full_name")
|
||||||
|
|
||||||
|
return privileged_users_annotated
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
"""Custom queryset implementation, filters by investigator"""
|
"""Custom queryset implementation, filters by investigator"""
|
||||||
|
@ -853,26 +938,19 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
]
|
]
|
||||||
|
autocomplete_fields = [
|
||||||
|
"approved_domain",
|
||||||
|
"requested_domain",
|
||||||
|
"submitter",
|
||||||
|
"creator",
|
||||||
|
"authorizing_official",
|
||||||
|
"investigator",
|
||||||
|
]
|
||||||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
||||||
|
|
||||||
# Table ordering
|
# Table ordering
|
||||||
ordering = ["requested_domain__name"]
|
ordering = ["requested_domain__name"]
|
||||||
|
|
||||||
# lists in filter_horizontal are not sorted properly, sort them
|
|
||||||
# by website
|
|
||||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
|
||||||
if db_field.name in ("current_websites", "alternative_domains"):
|
|
||||||
kwargs["queryset"] = models.Website.objects.all().order_by("website") # Sort websites
|
|
||||||
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:
|
||||||
|
@ -940,7 +1018,6 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
admin user permissions and the application creator's status, so
|
admin user permissions and the application creator's status, so
|
||||||
we'll use the baseline readonly_fields and extend it as needed.
|
we'll use the baseline readonly_fields and extend it as needed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
readonly_fields = list(self.readonly_fields)
|
readonly_fields = list(self.readonly_fields)
|
||||||
|
|
||||||
# Check if the creator is restricted
|
# Check if the creator is restricted
|
||||||
|
@ -1018,8 +1095,8 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
return formfield
|
return formfield
|
||||||
|
|
||||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
"""customize the behavior of formfields with foreign key relationships. this will customize
|
"""Customize the behavior of formfields with foreign key relationships. This will customize
|
||||||
the behavior of selects. customized behavior includes sorting of objects in list"""
|
the behavior of selects. Customized behavior includes sorting of objects in list."""
|
||||||
queryset = AdminSortFields.get_queryset(db_field)
|
queryset = AdminSortFields.get_queryset(db_field)
|
||||||
if queryset:
|
if queryset:
|
||||||
kwargs["queryset"] = queryset
|
kwargs["queryset"] = queryset
|
||||||
|
@ -1073,6 +1150,26 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
# Table ordering
|
# Table ordering
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
||||||
|
"""Custom changeform implementation to pass in context information"""
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
|
||||||
|
# Pass in what the an extended expiration date would be for the expiration date modal
|
||||||
|
if object_id is not None:
|
||||||
|
domain = Domain.objects.get(pk=object_id)
|
||||||
|
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
|
||||||
|
curr_exp_date = domain.registry_expiration_date
|
||||||
|
if curr_exp_date < date.today():
|
||||||
|
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
|
||||||
|
else:
|
||||||
|
new_date = domain.registry_expiration_date + relativedelta(years=years_to_extend_by)
|
||||||
|
extra_context["extended_expiration_date"] = new_date
|
||||||
|
else:
|
||||||
|
extra_context["extended_expiration_date"] = None
|
||||||
|
|
||||||
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
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")
|
||||||
|
@ -1131,6 +1228,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
"_edit_domain": self.do_edit_domain,
|
"_edit_domain": self.do_edit_domain,
|
||||||
"_delete_domain": self.do_delete_domain,
|
"_delete_domain": self.do_delete_domain,
|
||||||
"_get_status": self.do_get_status,
|
"_get_status": self.do_get_status,
|
||||||
|
"_extend_expiration_date": self.do_extend_expiration_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check which action button was pressed and call the corresponding function
|
# Check which action button was pressed and call the corresponding function
|
||||||
|
@ -1141,6 +1239,81 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
# If no matching action button is found, return the super method
|
# If no matching action button is found, return the super method
|
||||||
return super().response_change(request, obj)
|
return super().response_change(request, obj)
|
||||||
|
|
||||||
|
def do_extend_expiration_date(self, request, obj):
|
||||||
|
"""Extends a domains expiration date by one year from the current date"""
|
||||||
|
|
||||||
|
# Make sure we're dealing with a Domain
|
||||||
|
if not isinstance(obj, Domain):
|
||||||
|
self.message_user(request, "Object is not of type Domain.", messages.ERROR)
|
||||||
|
return None
|
||||||
|
|
||||||
|
years = self._get_calculated_years_for_exp_date(obj)
|
||||||
|
|
||||||
|
# Renew the domain.
|
||||||
|
try:
|
||||||
|
obj.renew_domain(length=years)
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Successfully extended the expiration date.",
|
||||||
|
)
|
||||||
|
except RegistryError as err:
|
||||||
|
if err.is_connection_error():
|
||||||
|
error_message = "Error connecting to the registry."
|
||||||
|
else:
|
||||||
|
error_message = f"Error extending this domain: {err}."
|
||||||
|
self.message_user(request, error_message, messages.ERROR)
|
||||||
|
except KeyError:
|
||||||
|
# In normal code flow, a keyerror can only occur when
|
||||||
|
# fresh data can't be pulled from the registry, and thus there is no cache.
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Error connecting to the registry. No expiration date was found.",
|
||||||
|
messages.ERROR,
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(err, stack_info=True)
|
||||||
|
self.message_user(request, "Could not delete: An unspecified error occured", messages.ERROR)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
|
def _get_calculated_years_for_exp_date(self, obj, extension_period: int = 1):
|
||||||
|
"""Given the current date, an extension period, and a registry_expiration_date
|
||||||
|
on the domain object, calculate the number of years needed to extend the
|
||||||
|
current expiration date by the extension period.
|
||||||
|
"""
|
||||||
|
# Get the date we want to update to
|
||||||
|
desired_date = self._get_current_date() + relativedelta(years=extension_period)
|
||||||
|
|
||||||
|
# Grab the current expiration date
|
||||||
|
try:
|
||||||
|
exp_date = obj.registry_expiration_date
|
||||||
|
except KeyError:
|
||||||
|
# if no expiration date from registry, set it to today
|
||||||
|
logger.warning("current expiration date not set; setting to today")
|
||||||
|
exp_date = self._get_current_date()
|
||||||
|
|
||||||
|
# If the expiration date is super old (2020, for example), we need to
|
||||||
|
# "catch up" to the current year, so we add the difference.
|
||||||
|
# If both years match, then lets just proceed as normal.
|
||||||
|
calculated_exp_date = exp_date + relativedelta(years=extension_period)
|
||||||
|
|
||||||
|
year_difference = desired_date.year - exp_date.year
|
||||||
|
|
||||||
|
years = extension_period
|
||||||
|
if desired_date > calculated_exp_date:
|
||||||
|
# Max probably isn't needed here (no code flow), but it guards against negative and 0.
|
||||||
|
# In both of those cases, we just want to extend by the extension_period.
|
||||||
|
years = max(extension_period, year_difference)
|
||||||
|
|
||||||
|
return years
|
||||||
|
|
||||||
|
# Workaround for unit tests, as we cannot mock date directly.
|
||||||
|
# it is immutable. Rather than dealing with a convoluted workaround,
|
||||||
|
# lets wrap this in a function.
|
||||||
|
def _get_current_date(self):
|
||||||
|
"""Gets the current date"""
|
||||||
|
return date.today()
|
||||||
|
|
||||||
def do_delete_domain(self, request, obj):
|
def do_delete_domain(self, request, obj):
|
||||||
if not isinstance(obj, Domain):
|
if not isinstance(obj, Domain):
|
||||||
# Could be problematic if the type is similar,
|
# Could be problematic if the type is similar,
|
||||||
|
@ -1299,6 +1472,10 @@ class DraftDomainAdmin(ListHeaderAdmin):
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
search_help_text = "Search by draft domain name."
|
search_help_text = "Search by draft domain name."
|
||||||
|
|
||||||
|
# this ordering effects the ordering of results
|
||||||
|
# in autocomplete_fields for user
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
class VerifiedByStaffAdmin(ListHeaderAdmin):
|
class VerifiedByStaffAdmin(ListHeaderAdmin):
|
||||||
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||||
|
|
|
@ -23,6 +23,33 @@ function openInNewTab(el, removeAttribute = false){
|
||||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
// Initialization code.
|
// Initialization code.
|
||||||
|
|
||||||
|
/** An IIFE for pages in DjangoAdmin that use modals.
|
||||||
|
* Dja strips out form elements, and modals generate their content outside
|
||||||
|
* of the current form scope, so we need to "inject" these inputs.
|
||||||
|
*/
|
||||||
|
(function (){
|
||||||
|
function createPhantomModalFormButtons(){
|
||||||
|
let submitButtons = document.querySelectorAll('.usa-modal button[type="submit"]');
|
||||||
|
form = document.querySelector("form")
|
||||||
|
submitButtons.forEach((button) => {
|
||||||
|
|
||||||
|
let input = document.createElement("input");
|
||||||
|
input.type = "submit";
|
||||||
|
input.name = button.name;
|
||||||
|
input.value = button.value;
|
||||||
|
input.style.display = "none"
|
||||||
|
|
||||||
|
// Add the hidden input to the form
|
||||||
|
form.appendChild(input);
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
console.log("clicking")
|
||||||
|
input.click();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createPhantomModalFormButtons();
|
||||||
|
})();
|
||||||
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
||||||
* Currently only appends target="_blank" to the domain_form object,
|
* Currently only appends target="_blank" to the domain_form object,
|
||||||
* but this can be expanded.
|
* but this can be expanded.
|
||||||
|
@ -41,8 +68,8 @@ function openInNewTab(el, removeAttribute = false){
|
||||||
let domainFormElement = document.getElementById("domain_form");
|
let domainFormElement = document.getElementById("domain_form");
|
||||||
let domainSubmitButton = document.getElementById("manageDomainSubmitButton");
|
let domainSubmitButton = document.getElementById("manageDomainSubmitButton");
|
||||||
if(domainSubmitButton && domainFormElement){
|
if(domainSubmitButton && domainFormElement){
|
||||||
domainSubmitButton.addEventListener("mouseover", () => openInNewTab(domainFormElement, true));
|
domainSubmitButton.addEventListener("mouseover", () => openInNewTab(domainFormElement, true));
|
||||||
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
|
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -183,6 +183,10 @@ h1, h2, h3,
|
||||||
.submit-row div.spacer {
|
.submit-row div.spacer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
.submit-row .mini-spacer{
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
.submit-row span {
|
.submit-row span {
|
||||||
margin-top: units(1);
|
margin-top: units(1);
|
||||||
}
|
}
|
||||||
|
@ -270,3 +274,31 @@ h1, h2, h3,
|
||||||
margin: 0!important;
|
margin: 0!important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hides the "clear" button on autocomplete, as we already have one to use
|
||||||
|
.select2-selection__clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixes a display issue where the list was entirely white, or had too much whitespace
|
||||||
|
.select2-dropdown {
|
||||||
|
display: inline-grid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.admin-confirm-button {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button groups in /admin incorrectly have bullets.
|
||||||
|
// Remove that!
|
||||||
|
.usa-modal__footer .usa-button-group__item {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// USWDS media checks are overzealous in this situation,
|
||||||
|
// we should manually define this behaviour.
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.button-list-mobile {
|
||||||
|
display: contents !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -265,15 +265,17 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
Default length and unit of time are 1 year.
|
Default length and unit of time are 1 year.
|
||||||
"""
|
"""
|
||||||
# if no expiration date from registry, set to today
|
|
||||||
|
# If no date is specified, grab the registry_expiration_date
|
||||||
try:
|
try:
|
||||||
cur_exp_date = self.registry_expiration_date
|
exp_date = self.registry_expiration_date
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
# if no expiration date from registry, set it to today
|
||||||
logger.warning("current expiration date not set; setting to today")
|
logger.warning("current expiration date not set; setting to today")
|
||||||
cur_exp_date = date.today()
|
exp_date = date.today()
|
||||||
|
|
||||||
# create RenewDomain request
|
# create RenewDomain request
|
||||||
request = commands.RenewDomain(name=self.name, cur_exp_date=cur_exp_date, period=epp.Period(length, unit))
|
request = commands.RenewDomain(name=self.name, cur_exp_date=exp_date, period=epp.Period(length, unit))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# update expiration date in registry, and set the updated
|
# update expiration date in registry, and set the updated
|
||||||
|
|
|
@ -589,7 +589,9 @@ class DomainApplication(TimeStampedModel):
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
logger.error(f"Can't query an approved domain while attempting {called_from}")
|
logger.error(f"Can't query an approved domain while attempting {called_from}")
|
||||||
|
|
||||||
def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True, bcc_address=''):
|
def _send_status_update_email(
|
||||||
|
self, new_status, email_template, email_template_subject, send_email=True, bcc_address=""
|
||||||
|
):
|
||||||
"""Send a status update email to the submitter.
|
"""Send a status update email to the submitter.
|
||||||
|
|
||||||
The email goes to the email address that the submitter gave as their
|
The email goes to the email address that the submitter gave as their
|
||||||
|
@ -656,7 +658,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
# Limit email notifications to transitions from Started and Withdrawn
|
# Limit email notifications to transitions from Started and Withdrawn
|
||||||
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
|
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
|
||||||
|
|
||||||
bcc_address = ''
|
bcc_address = ""
|
||||||
if settings.IS_PRODUCTION:
|
if settings.IS_PRODUCTION:
|
||||||
bcc_address = settings.DEFAULT_FROM_EMAIL
|
bcc_address = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
|
|
37
src/registrar/models/utility/generic_helper.py
Normal file
37
src/registrar/models/utility/generic_helper.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""This file contains general purpose helpers that don't belong in any specific location"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Timer:
|
||||||
|
"""
|
||||||
|
This class is used to measure execution time for performance profiling.
|
||||||
|
__enter__ and __exit__ is used such that you can wrap any code you want
|
||||||
|
around a with statement. After this exits, logger.info will print
|
||||||
|
the execution time in seconds.
|
||||||
|
|
||||||
|
Note that this class does not account for general randomness as more
|
||||||
|
robust libraries do, so there is some tiny amount of latency involved
|
||||||
|
in using this, but it is minimal enough that for most applications it is not
|
||||||
|
noticable.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
with Timer():
|
||||||
|
...some code
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Starts the timer"""
|
||||||
|
self.start = time.time()
|
||||||
|
# This allows usage of the instance within the with block
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
"""Ends the timer and logs what happened"""
|
||||||
|
self.end = time.time()
|
||||||
|
self.duration = self.end - self.start
|
||||||
|
logger.info(f"Execution time: {self.duration} seconds")
|
|
@ -18,11 +18,12 @@
|
||||||
<link rel="apple-touch-icon" size="180x180"
|
<link rel="apple-touch-icon" size="180x180"
|
||||||
href="{% static 'img/registrar/favicons/favicon-180.png' %}"
|
href="{% static 'img/registrar/favicons/favicon-180.png' %}"
|
||||||
>
|
>
|
||||||
|
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
|
||||||
|
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||||
|
|
||||||
{% block extrastyle %}{{ block.super }}
|
{% block extrastyle %}{{ block.super }}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
|
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,21 +2,117 @@
|
||||||
{% load i18n static %}
|
{% load i18n static %}
|
||||||
|
|
||||||
{% block field_sets %}
|
{% block field_sets %}
|
||||||
<div class="submit-row">
|
<div class="display-flex flex-row flex-justify submit-row">
|
||||||
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
<div class="flex-align-self-start button-list-mobile">
|
||||||
<input type="submit" value="Get registry status" name="_get_status">
|
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
||||||
<div class="spacer"></div>
|
{# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #}
|
||||||
{% if original.state == original.State.READY %}
|
<span class="mini-spacer"></span>
|
||||||
<input type="submit" value="Place hold" name="_place_client_hold" class="custom-link-button">
|
<input type="submit" value="Get registry status" name="_get_status">
|
||||||
{% elif original.state == original.State.ON_HOLD %}
|
</div>
|
||||||
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
<div class="desktop:flex-align-self-end">
|
||||||
{% endif %}
|
{% if original.state != original.State.DELETED %}
|
||||||
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
|
<a
|
||||||
<span> | </span>
|
class="text-middle"
|
||||||
{% endif %}
|
href="#toggle-extend-expiration-alert"
|
||||||
{% if original.state != original.State.DELETED %}
|
aria-controls="toggle-extend-expiration-alert"
|
||||||
<input type="submit" value="Remove from registry" name="_delete_domain" class="custom-link-button">
|
data-open-modal
|
||||||
|
>
|
||||||
|
Extend expiration date
|
||||||
|
</a>
|
||||||
|
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||||
|
{% endif %}
|
||||||
|
{% if original.state == original.State.READY %}
|
||||||
|
<input type="submit" value="Place hold" name="_place_client_hold" class="custom-link-button">
|
||||||
|
{% elif original.state == original.State.ON_HOLD %}
|
||||||
|
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
||||||
|
{% endif %}
|
||||||
|
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
|
||||||
|
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||||
|
{% endif %}
|
||||||
|
{% if original.state != original.State.DELETED %}
|
||||||
|
<input type="submit" value="Remove from registry" name="_delete_domain" class="custom-link-button">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block submit_buttons_bottom %}
|
||||||
|
{% comment %}
|
||||||
|
Modals behave very weirdly in django admin.
|
||||||
|
They tend to "strip out" any injected form elements, leaving only the main form.
|
||||||
|
In addition, USWDS handles modals by first destroying the element, then repopulating it toward the end of the page.
|
||||||
|
In effect, this means that the modal is not, and cannot, be surrounded by any form element at compile time.
|
||||||
|
|
||||||
|
The current workaround for this is to use javascript to inject a hidden input, and bind submit of that
|
||||||
|
element to the click of the confirmation button within this modal.
|
||||||
|
|
||||||
|
This is controlled by the class `dja-form-placeholder` on the button.
|
||||||
|
|
||||||
|
In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions
|
||||||
|
of the application, so this means that it will briefly "populate", causing unintended visual effects.
|
||||||
|
{% endcomment %}
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="toggle-extend-expiration-alert"
|
||||||
|
aria-labelledby="Are you sure you want to extend the expiration date?"
|
||||||
|
aria-describedby="This expiration date will be extended."
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
|
Are you sure you want to extend the expiration date?
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p>
|
||||||
|
This will extend the expiration date by one year.
|
||||||
|
{# Acts as a <br> #}
|
||||||
|
<div class="display-inline"></div>
|
||||||
|
This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Domain: <b>{{ original.name }}</b>
|
||||||
|
{# Acts as a <br> #}
|
||||||
|
<div class="display-inline"></div>
|
||||||
|
New expiration date: <b>{{ extended_expiration_date }}</b>
|
||||||
|
{{test}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button dja-form-placeholder"
|
||||||
|
name="_extend_expiration_date"
|
||||||
|
>
|
||||||
|
Yes, extend date
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
|
@ -231,6 +231,7 @@ class AuditedAdminMockData:
|
||||||
first_name="{} first_name:{}".format(item_name, short_hand),
|
first_name="{} first_name:{}".format(item_name, short_hand),
|
||||||
last_name="{} last_name:{}".format(item_name, short_hand),
|
last_name="{} last_name:{}".format(item_name, short_hand),
|
||||||
username="{} username:{}".format(item_name + str(uuid.uuid4())[:8], short_hand),
|
username="{} username:{}".format(item_name + str(uuid.uuid4())[:8], short_hand),
|
||||||
|
is_staff=True,
|
||||||
)[0]
|
)[0]
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
@ -921,6 +922,11 @@ class MockEppLib(TestCase):
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=datetime.date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mockButtonRenewedDomainExpDate = fakedEppObject(
|
||||||
|
"fake.gov",
|
||||||
|
ex_date=datetime.date(2025, 5, 25),
|
||||||
|
)
|
||||||
|
|
||||||
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
|
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
|
||||||
"fakeneeded.gov",
|
"fakeneeded.gov",
|
||||||
ex_date=datetime.date(2023, 2, 15),
|
ex_date=datetime.date(2023, 2, 15),
|
||||||
|
@ -1049,11 +1055,19 @@ class MockEppLib(TestCase):
|
||||||
res_data=[self.mockMaximumRenewedDomainExpDate],
|
res_data=[self.mockMaximumRenewedDomainExpDate],
|
||||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
)
|
)
|
||||||
else:
|
elif getattr(_request, "name", None) == "fake.gov":
|
||||||
return MagicMock(
|
period = getattr(_request, "period", None)
|
||||||
res_data=[self.mockRenewedDomainExpDate],
|
extension_period = getattr(period, "length", None)
|
||||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
if extension_period == 2:
|
||||||
)
|
return MagicMock(
|
||||||
|
res_data=[self.mockButtonRenewedDomainExpDate],
|
||||||
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[self.mockRenewedDomainExpDate],
|
||||||
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
|
)
|
||||||
|
|
||||||
def mockInfoDomainCommands(self, _request, cleaned):
|
def mockInfoDomainCommands(self, _request, cleaned):
|
||||||
request_name = getattr(_request, "name", None)
|
request_name = getattr(_request, "name", None)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
from datetime import date
|
||||||
from django.test import TestCase, RequestFactory, Client, override_settings
|
from django.test import TestCase, RequestFactory, Client, override_settings
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
|
from django_webtest import WebTest # type: ignore
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from registrar.admin import (
|
from registrar.admin import (
|
||||||
|
@ -35,7 +37,7 @@ from .common import (
|
||||||
)
|
)
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from unittest.mock import patch
|
from unittest.mock import ANY, call, patch
|
||||||
from unittest import skip
|
from unittest import skip
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -45,7 +47,11 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainAdmin(MockEppLib):
|
class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
|
# csrf checks do not work with WebTest.
|
||||||
|
# We disable them here. TODO for another ticket.
|
||||||
|
csrf_checks = False
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
||||||
|
@ -53,8 +59,177 @@ class TestDomainAdmin(MockEppLib):
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
self.staffuser = create_user()
|
self.staffuser = create_user()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
self.app.set_user(self.superuser.username)
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
@skip("TODO for another ticket. This test case is grabbing old db data.")
|
||||||
|
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
|
||||||
|
def test_extend_expiration_date_button(self, mock_date_today):
|
||||||
|
"""
|
||||||
|
Tests if extend_expiration_date button extends correctly
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a ready domain with a preset expiration date
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||||
|
|
||||||
|
# Make sure the ex date is what we expect it to be
|
||||||
|
domain_ex_date = Domain.objects.get(id=domain.id).expiration_date
|
||||||
|
self.assertEqual(domain_ex_date, date(2023, 5, 25))
|
||||||
|
|
||||||
|
# Make sure that the page is loading as expected
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Grab the form to submit
|
||||||
|
form = response.forms["domain_form"]
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||||
|
# Submit the form
|
||||||
|
response = form.submit("_extend_expiration_date")
|
||||||
|
|
||||||
|
# Follow the response
|
||||||
|
response = response.follow()
|
||||||
|
|
||||||
|
# refresh_from_db() does not work for objects with protected=True.
|
||||||
|
# https://github.com/viewflow/django-fsm/issues/89
|
||||||
|
new_domain = Domain.objects.get(id=domain.id)
|
||||||
|
|
||||||
|
# Check that the current expiration date is what we expect
|
||||||
|
self.assertEqual(new_domain.expiration_date, date(2025, 5, 25))
|
||||||
|
|
||||||
|
# Assert that everything on the page looks correct
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Ensure the message we recieve is in line with what we expect
|
||||||
|
expected_message = "Successfully extended the expiration date."
|
||||||
|
expected_call = call(
|
||||||
|
# The WGSI request doesn't need to be tested
|
||||||
|
ANY,
|
||||||
|
messages.INFO,
|
||||||
|
expected_message,
|
||||||
|
extra_tags="",
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
mock_add_message.assert_has_calls([expected_call], 1)
|
||||||
|
|
||||||
|
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
|
||||||
|
def test_extend_expiration_date_button_epp(self, mock_date_today):
|
||||||
|
"""
|
||||||
|
Tests if extend_expiration_date button sends the right epp command
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a ready domain with a preset expiration date
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||||
|
|
||||||
|
# Make sure that the page is loading as expected
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Grab the form to submit
|
||||||
|
form = response.forms["domain_form"]
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||||
|
with patch("registrar.models.Domain.renew_domain") as renew_mock:
|
||||||
|
# Submit the form
|
||||||
|
response = form.submit("_extend_expiration_date")
|
||||||
|
|
||||||
|
# Follow the response
|
||||||
|
response = response.follow()
|
||||||
|
|
||||||
|
# This value is based off of the current year - the expiration date.
|
||||||
|
# We "freeze" time to 2024, so 2024 - 2023 will always result in an
|
||||||
|
# "extension" of 2, as that will be one year of extension from that date.
|
||||||
|
extension_length = 2
|
||||||
|
|
||||||
|
# Assert that it is calling the function with the right extension length.
|
||||||
|
# We only need to test the value that EPP sends, as we can assume the other
|
||||||
|
# test cases cover the "renew" function.
|
||||||
|
renew_mock.assert_has_calls([call(length=extension_length)], any_order=False)
|
||||||
|
|
||||||
|
# We should not make duplicate calls
|
||||||
|
self.assertEqual(renew_mock.call_count, 1)
|
||||||
|
|
||||||
|
# Assert that everything on the page looks correct
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Ensure the message we recieve is in line with what we expect
|
||||||
|
expected_message = "Successfully extended the expiration date."
|
||||||
|
expected_call = call(
|
||||||
|
# The WGSI request doesn't need to be tested
|
||||||
|
ANY,
|
||||||
|
messages.INFO,
|
||||||
|
expected_message,
|
||||||
|
extra_tags="",
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
mock_add_message.assert_has_calls([expected_call], 1)
|
||||||
|
|
||||||
|
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2023, 1, 1))
|
||||||
|
def test_extend_expiration_date_button_date_matches_epp(self, mock_date_today):
|
||||||
|
"""
|
||||||
|
Tests if extend_expiration_date button sends the right epp command
|
||||||
|
when the current year matches the expiration date
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a ready domain with a preset expiration date
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||||
|
|
||||||
|
# Make sure that the page is loading as expected
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Grab the form to submit
|
||||||
|
form = response.forms["domain_form"]
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||||
|
with patch("registrar.models.Domain.renew_domain") as renew_mock:
|
||||||
|
# Submit the form
|
||||||
|
response = form.submit("_extend_expiration_date")
|
||||||
|
|
||||||
|
# Follow the response
|
||||||
|
response = response.follow()
|
||||||
|
|
||||||
|
extension_length = 1
|
||||||
|
|
||||||
|
# Assert that it is calling the function with the right extension length.
|
||||||
|
# We only need to test the value that EPP sends, as we can assume the other
|
||||||
|
# test cases cover the "renew" function.
|
||||||
|
renew_mock.assert_has_calls([call(length=extension_length)], any_order=False)
|
||||||
|
|
||||||
|
# We should not make duplicate calls
|
||||||
|
self.assertEqual(renew_mock.call_count, 1)
|
||||||
|
|
||||||
|
# Assert that everything on the page looks correct
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Ensure the message we recieve is in line with what we expect
|
||||||
|
expected_message = "Successfully extended the expiration date."
|
||||||
|
expected_call = call(
|
||||||
|
# The WGSI request doesn't need to be tested
|
||||||
|
ANY,
|
||||||
|
messages.INFO,
|
||||||
|
expected_message,
|
||||||
|
extra_tags="",
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
mock_add_message.assert_has_calls([expected_call], 1)
|
||||||
|
|
||||||
def test_short_org_name_in_domains_list(self):
|
def test_short_org_name_in_domains_list(self):
|
||||||
"""
|
"""
|
||||||
Make sure the short name is displaying in admin on the list page
|
Make sure the short name is displaying in admin on the list page
|
||||||
|
@ -426,7 +601,9 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
self.admin.save_model(request, application, form=None, change=True)
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
def assert_email_is_accurate(self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address=''):
|
def assert_email_is_accurate(
|
||||||
|
self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address=""
|
||||||
|
):
|
||||||
"""Helper method for the email test cases.
|
"""Helper method for the email test cases.
|
||||||
email_index is the index of the email in mock_client."""
|
email_index is the index of the email in mock_client."""
|
||||||
|
|
||||||
|
@ -446,11 +623,11 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertIn(expected_string, email_body)
|
self.assertIn(expected_string, email_body)
|
||||||
|
|
||||||
if test_that_no_bcc:
|
if test_that_no_bcc:
|
||||||
_ = ''
|
_ = ""
|
||||||
with self.assertRaises(KeyError):
|
with self.assertRaises(KeyError):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
_ = kwargs["Destination"]["BccAddresses"][0]
|
_ = kwargs["Destination"]["BccAddresses"][0]
|
||||||
self.assertEqual(_, '')
|
self.assertEqual(_, "")
|
||||||
|
|
||||||
if bcc_email_address:
|
if bcc_email_address:
|
||||||
bcc_email = kwargs["Destination"]["BccAddresses"][0]
|
bcc_email = kwargs["Destination"]["BccAddresses"][0]
|
||||||
|
@ -462,7 +639,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
When transitioning to submitted from dns needed or in review on a domain request,
|
When transitioning to submitted from dns needed or in review on a domain request,
|
||||||
no email is sent out.
|
no email is sent out.
|
||||||
|
|
||||||
Also test that the default email set in settings is NOT BCCd on non-prod whenever
|
Also test that the default email set in settings is NOT BCCd on non-prod whenever
|
||||||
an email does go out."""
|
an email does go out."""
|
||||||
|
|
||||||
|
@ -516,7 +693,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
When transitioning to submitted from dns needed or in review on a domain request,
|
When transitioning to submitted from dns needed or in review on a domain request,
|
||||||
no email is sent out.
|
no email is sent out.
|
||||||
|
|
||||||
Also test that the default email set in settings IS BCCd on prod whenever
|
Also test that the default email set in settings IS BCCd on prod whenever
|
||||||
an email does go out."""
|
an email does go out."""
|
||||||
|
|
||||||
|
@ -1012,8 +1189,17 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
investigator_field = DomainApplication._meta.get_field("investigator")
|
investigator_field = DomainApplication._meta.get_field("investigator")
|
||||||
|
|
||||||
# We should only be displaying staff users, in alphabetical order
|
# We should only be displaying staff users, in alphabetical order
|
||||||
expected_dropdown = list(User.objects.filter(is_staff=True))
|
sorted_fields = ["first_name", "last_name", "email"]
|
||||||
current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset)
|
expected_dropdown = list(User.objects.filter(is_staff=True).order_by(*sorted_fields))
|
||||||
|
|
||||||
|
# Grab the current dropdown. We do an API call to autocomplete to get this info.
|
||||||
|
application_queryset = self.admin.formfield_for_foreignkey(investigator_field, request).queryset
|
||||||
|
user_request = self.factory.post(
|
||||||
|
"/admin/autocomplete/?app_label=registrar&model_name=domainapplication&field_name=investigator"
|
||||||
|
)
|
||||||
|
user_admin = MyUserAdmin(User, self.site)
|
||||||
|
user_queryset = user_admin.get_search_results(user_request, application_queryset, None)[0]
|
||||||
|
current_dropdown = list(user_queryset)
|
||||||
|
|
||||||
self.assertEqual(expected_dropdown, current_dropdown)
|
self.assertEqual(expected_dropdown, current_dropdown)
|
||||||
|
|
||||||
|
@ -1047,7 +1233,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.client.login(username="staffuser", password=p)
|
self.client.login(username="staffuser", password=p)
|
||||||
request = RequestFactory().get("/")
|
request = RequestFactory().get("/")
|
||||||
|
|
||||||
expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email"))
|
# These names have metadata embedded in them. :investigator implicitly tests if
|
||||||
|
# these are actually from the attribute "investigator".
|
||||||
|
expected_list = [
|
||||||
|
"AGuy AGuy last_name:investigator",
|
||||||
|
"FinalGuy FinalGuy last_name:investigator",
|
||||||
|
"SomeGuy first_name:investigator SomeGuy last_name:investigator",
|
||||||
|
]
|
||||||
|
|
||||||
# Get the actual sorted list of investigators from the lookups method
|
# 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)]
|
actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)]
|
||||||
|
@ -1467,11 +1659,46 @@ class AuditedAdminTest(TestCase):
|
||||||
|
|
||||||
return ordered_list
|
return ordered_list
|
||||||
|
|
||||||
|
def test_alphabetically_sorted_domain_application_investigator(self):
|
||||||
|
"""Tests if the investigator field is alphabetically sorted by mimicking
|
||||||
|
the call event flow"""
|
||||||
|
# Creates multiple domain applications - review status does not matter
|
||||||
|
applications = multiple_unalphabetical_domain_objects("application")
|
||||||
|
|
||||||
|
# Create a mock request
|
||||||
|
application_request = self.factory.post(
|
||||||
|
"/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the formfield data from the application page
|
||||||
|
application_admin = AuditedAdmin(DomainApplication, self.site)
|
||||||
|
field = DomainApplication.investigator.field
|
||||||
|
application_queryset = application_admin.formfield_for_foreignkey(field, application_request).queryset
|
||||||
|
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/autocomplete/?app_label=registrar&model_name=domainapplication&field_name=investigator"
|
||||||
|
)
|
||||||
|
|
||||||
|
sorted_fields = ["first_name", "last_name", "email"]
|
||||||
|
desired_sort_order = list(User.objects.filter(is_staff=True).order_by(*sorted_fields))
|
||||||
|
|
||||||
|
# Grab the data returned from get search results
|
||||||
|
admin = MyUserAdmin(User, self.site)
|
||||||
|
search_queryset = admin.get_search_results(request, application_queryset, None)[0]
|
||||||
|
current_sort_order = list(search_queryset)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
desired_sort_order,
|
||||||
|
current_sort_order,
|
||||||
|
"Investigator is not ordered alphabetically",
|
||||||
|
)
|
||||||
|
|
||||||
|
# This test case should be refactored in general, as it is too overly specific and engineered
|
||||||
def test_alphabetically_sorted_fk_fields_domain_application(self):
|
def test_alphabetically_sorted_fk_fields_domain_application(self):
|
||||||
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,
|
||||||
]
|
]
|
||||||
|
@ -1489,39 +1716,40 @@ class AuditedAdminTest(TestCase):
|
||||||
# but both fields are of a fixed length.
|
# but both fields are of a fixed length.
|
||||||
# For test case purposes, this should be performant.
|
# For test case purposes, this should be performant.
|
||||||
for field in tested_fields:
|
for field in tested_fields:
|
||||||
isNamefield: bool = field == DomainApplication.requested_domain.field
|
with self.subTest(field=field):
|
||||||
if isNamefield:
|
isNamefield: bool = field == DomainApplication.requested_domain.field
|
||||||
sorted_fields = ["name"]
|
if isNamefield:
|
||||||
else:
|
sorted_fields = ["name"]
|
||||||
sorted_fields = ["first_name", "last_name"]
|
|
||||||
# We want both of these to be lists, as it is richer test wise.
|
|
||||||
|
|
||||||
desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields)
|
|
||||||
current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset)
|
|
||||||
|
|
||||||
# Conforms to the same object structure as desired_order
|
|
||||||
current_sort_order_coerced_type = []
|
|
||||||
|
|
||||||
# This is necessary as .queryset and get_queryset
|
|
||||||
# return lists of different types/structures.
|
|
||||||
# We need to parse this data and coerce them into the same type.
|
|
||||||
for contact in current_sort_order:
|
|
||||||
if not isNamefield:
|
|
||||||
first = contact.first_name
|
|
||||||
last = contact.last_name
|
|
||||||
else:
|
else:
|
||||||
first = contact.name
|
sorted_fields = ["first_name", "last_name"]
|
||||||
last = None
|
# We want both of these to be lists, as it is richer test wise.
|
||||||
|
|
||||||
name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":")
|
desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields)
|
||||||
if name_tuple is not None:
|
current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset)
|
||||||
current_sort_order_coerced_type.append(name_tuple)
|
|
||||||
|
|
||||||
self.assertEqual(
|
# Conforms to the same object structure as desired_order
|
||||||
desired_order,
|
current_sort_order_coerced_type = []
|
||||||
current_sort_order_coerced_type,
|
|
||||||
"{} is not ordered alphabetically".format(field.name),
|
# This is necessary as .queryset and get_queryset
|
||||||
)
|
# return lists of different types/structures.
|
||||||
|
# We need to parse this data and coerce them into the same type.
|
||||||
|
for contact in current_sort_order:
|
||||||
|
if not isNamefield:
|
||||||
|
first = contact.first_name
|
||||||
|
last = contact.last_name
|
||||||
|
else:
|
||||||
|
first = contact.name
|
||||||
|
last = None
|
||||||
|
|
||||||
|
name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":")
|
||||||
|
if name_tuple is not None:
|
||||||
|
current_sort_order_coerced_type.append(name_tuple)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
desired_order,
|
||||||
|
current_sort_order_coerced_type,
|
||||||
|
"{} is not ordered alphabetically".format(field.name),
|
||||||
|
)
|
||||||
|
|
||||||
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
||||||
tested_fields = [
|
tested_fields = [
|
||||||
|
|
|
@ -51,6 +51,7 @@ class TestWithUser(MockEppLib):
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
self.user.delete()
|
self.user.delete()
|
||||||
|
|
||||||
|
|
||||||
class TestEnvironmentVariablesEffects(TestCase):
|
class TestEnvironmentVariablesEffects(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
|
@ -77,4 +78,4 @@ class TestEnvironmentVariablesEffects(TestCase):
|
||||||
def test_non_production_environment(self):
|
def test_non_production_environment(self):
|
||||||
"""Banner on non-prod."""
|
"""Banner on non-prod."""
|
||||||
home_page = self.client.get("/")
|
home_page = self.client.get("/")
|
||||||
self.assertContains(home_page, "You are on a test site.")
|
self.assertContains(home_page, "You are on a test site.")
|
||||||
|
|
|
@ -15,7 +15,7 @@ class EmailSendingError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address='', context={}):
|
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}):
|
||||||
"""Send an email built from a template to one email address.
|
"""Send an email built from a template to one email address.
|
||||||
|
|
||||||
template_name and subject_template_name are relative to the same template
|
template_name and subject_template_name are relative to the same template
|
||||||
|
@ -39,7 +39,7 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise EmailSendingError("Could not access the SES client.") from exc
|
raise EmailSendingError("Could not access the SES client.") from exc
|
||||||
|
|
||||||
destination = {"ToAddresses": [to_address]}
|
destination = {"ToAddresses": [to_address]}
|
||||||
if bcc_address:
|
if bcc_address:
|
||||||
destination["BccAddresses"] = [bcc_address]
|
destination["BccAddresses"] = [bcc_address]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue