Test autocomplete dropdown

This commit is contained in:
zandercymatics 2024-02-14 13:22:10 -07:00
parent 63438f0193
commit 6a0587d5fe
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
2 changed files with 183 additions and 197 deletions

View file

@ -1,5 +1,6 @@
import logging import logging
import time import time
from django.db import transaction
from django import forms from django import forms
from django.db.models.functions import Concat, Coalesce from django.db.models.functions import Concat, Coalesce
from django.db.models import Value, CharField from django.db.models import Value, CharField
@ -59,46 +60,44 @@ class MultiFieldSortableChangeList(admin.views.main.ChangeList):
Mostly identical to the base implementation, except that now it can return Mostly identical to the base implementation, except that now it can return
a list of order_field objects rather than just one. a list of order_field objects rather than just one.
""" """
logger.info("timing get_ordering") params = self.params
with Timer() as t: ordering = list(self.model_admin.get_ordering(request) or self._get_default_ordering())
params = self.params
ordering = list(self.model_admin.get_ordering(request) or self._get_default_ordering())
if ORDER_VAR in params: if ORDER_VAR in params:
# Clear ordering and used params # Clear ordering and used params
ordering = [] ordering = []
order_params = params[ORDER_VAR].split(".") order_params = params[ORDER_VAR].split(".")
for p in order_params: for p in order_params:
try: try:
none, pfx, idx = p.rpartition("-") none, pfx, idx = p.rpartition("-")
field_name = self.list_display[int(idx)] field_name = self.list_display[int(idx)]
order_fields = self.get_ordering_field(field_name) order_fields = self.get_ordering_field(field_name)
if isinstance(order_fields, list): if isinstance(order_fields, list):
for order_field in order_fields: for order_field in order_fields:
if order_field: if order_field:
ordering.append(pfx + order_field) ordering.append(pfx + order_field)
else: else:
ordering.append(pfx + order_fields) ordering.append(pfx + order_fields)
except (IndexError, ValueError): except (IndexError, ValueError):
continue # Invalid ordering specified, skip it. continue # Invalid ordering specified, skip it.
# Add the given query's ordering fields, if any. # Add the given query's ordering fields, if any.
ordering.extend(queryset.query.order_by) ordering.extend(queryset.query.order_by)
# Ensure that the primary key is systematically present in the list of # Ensure that the primary key is systematically present in the list of
# ordering fields so we can guarantee a deterministic order across all # ordering fields so we can guarantee a deterministic order across all
# database backends. # database backends.
pk_name = self.lookup_opts.pk.name pk_name = self.lookup_opts.pk.name
if not (set(ordering) & set(["pk", "-pk", pk_name, "-" + pk_name])): if not (set(ordering) & set(["pk", "-pk", pk_name, "-" + pk_name])):
# The two sets do not intersect, meaning the pk isn't present. So # The two sets do not intersect, meaning the pk isn't present. So
# we add it. # we add it.
ordering.append("-pk") ordering.append("-pk")
return ordering return ordering
class CustomLogEntryAdmin(LogEntryAdmin): class CustomLogEntryAdmin(LogEntryAdmin):
@ -129,18 +128,24 @@ class AdminSortFields:
_name_sort = Concat("first_name", "last_name", "email") _name_sort = Concat("first_name", "last_name", "email")
# Define a mapping of field names to model querysets and sort expressions # Define a mapping of field names to model querysets and sort expressions
sort_mapping = { sort_mapping = {
# == Contact == #
"other_contacts": (Contact, _name_sort), "other_contacts": (Contact, _name_sort),
"authorizing_official": (Contact, _name_sort), "authorizing_official": (Contact, _name_sort),
"submitter": (Contact, _name_sort), "submitter": (Contact, _name_sort),
"current_websites": (Website, "website"), # == User == #
"alternative_domains": (Website, "website"),
"creator": (User, _name_sort), "creator": (User, _name_sort),
"user": (User, _name_sort), "user": (User, _name_sort),
"investigator": (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"), "domain": (Domain, "name"),
"approved_domain": (Domain, "name"), "approved_domain": (Domain, "name"),
"requested_domain": (DraftDomain, "name"),
"domain_application": (DomainApplication, "requested_domain__name"),
} }
@classmethod @classmethod
@ -155,6 +160,9 @@ class AdminSortFields:
case "investigator": case "investigator":
# We should only return users who are staff # We should only return users who are staff
return model.objects.filter(is_staff=True).order_by(order_by) return model.objects.filter(is_staff=True).order_by(order_by)
case "submitter":
db_field_model = db_field.model
db_field_model.objects.select_related("submitter")
case _: case _:
# If no case is defined, return the default # If no case is defined, return the default
return model.objects.order_by(order_by) return model.objects.order_by(order_by)
@ -208,21 +216,17 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
Reference: https://code.djangoproject.com/ticket/31975 Reference: https://code.djangoproject.com/ticket/31975
""" """
logger.info("timing get_changelist") return MultiFieldSortableChangeList
with Timer() as t:
return MultiFieldSortableChangeList
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
logger.info("timing changelist_view") if extra_context is None:
with Timer() as t: extra_context = {}
if extra_context is None: # Get the filtered values
extra_context = {} filters = self.get_filters(request)
# Get the filtered values # Pass the filtered values to the template context
filters = self.get_filters(request) extra_context["filters"] = filters
# Pass the filtered values to the template context extra_context["search_query"] = request.GET.get("q", "") # Assuming the search query parameter is 'q'
extra_context["filters"] = filters return super().changelist_view(request, extra_context=extra_context)
extra_context["search_query"] = request.GET.get("q", "") # Assuming the search query parameter is 'q'
return super().changelist_view(request, extra_context=extra_context)
def get_filters(self, request): def get_filters(self, request):
"""Retrieve the current set of parameters being used to filter the table """Retrieve the current set of parameters being used to filter the table
@ -231,40 +235,38 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
parameter_value: string} parameter_value: string}
TODO: convert investigator id to investigator username TODO: convert investigator id to investigator username
""" """
logger.info("timing get_filters") filters = []
with Timer() as t: # Retrieve the filter parameters
filters = [] for param in request.GET.keys():
# Retrieve the filter parameters # Exclude the default search parameter 'q'
for param in request.GET.keys(): if param != "q" and param != "o":
# Exclude the default search parameter 'q' parameter_name = param.replace("__exact", "").replace("_type", "").replace("__id", " id")
if param != "q" and param != "o":
parameter_name = param.replace("__exact", "").replace("_type", "").replace("__id", " id")
if parameter_name == "investigator id": if parameter_name == "investigator id":
# Retrieves the corresponding contact from Users # Retrieves the corresponding contact from Users
id_value = request.GET.get(param) id_value = request.GET.get(param)
try: try:
contact = models.User.objects.get(id=id_value) contact = models.User.objects.get(id=id_value)
investigator_name = contact.first_name + " " + contact.last_name investigator_name = contact.first_name + " " + contact.last_name
filters.append(
{
"parameter_name": "investigator",
"parameter_value": investigator_name,
}
)
except models.User.DoesNotExist:
pass
else:
# For other parameter names, append a dictionary with the original
# parameter_name and the corresponding parameter_value
filters.append( filters.append(
{ {
"parameter_name": parameter_name, "parameter_name": "investigator",
"parameter_value": request.GET.get(param), "parameter_value": investigator_name,
} }
) )
return filters except models.User.DoesNotExist:
pass
else:
# For other parameter names, append a dictionary with the original
# parameter_name and the corresponding parameter_value
filters.append(
{
"parameter_name": parameter_name,
"parameter_value": request.GET.get(param),
}
)
return filters
class UserContactInline(admin.StackedInline): class UserContactInline(admin.StackedInline):
@ -775,46 +777,36 @@ 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)
""" """
logger.info("timing lookups") # Select all investigators that are staff, then order by name and email
with Timer() as t: privileged_users = (
DomainApplication.objects.select_related("investigator")
# Select all investigators that are staff, then order by name and email .filter(investigator__is_staff=True)
privileged_users = ( .order_by(
DomainApplication.objects.select_related("investigator") "investigator__first_name",
.filter(investigator__is_staff=True) "investigator__last_name",
.order_by( "investigator__email"
"investigator__first_name",
"investigator__last_name",
"investigator__email"
)
) )
)
# Annotate the full name and return a values list that lookups can use # Annotate the full name and return a values list that lookups can use
privileged_users_annotated = privileged_users.annotate( privileged_users_annotated = privileged_users.annotate(
full_name=Coalesce( full_name=Coalesce(
Concat( Concat(
"investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField() "investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()
), ),
"investigator__email", "investigator__email",
output_field=CharField() output_field=CharField()
) )
).values_list("investigator__id", "full_name") ).values_list("investigator__id", "full_name")
return privileged_users_annotated 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"""
logger.info("timing queryset") if self.value() is None:
with Timer() as t: return queryset
if self.value() is None: else:
return queryset return queryset.filter(investigator__id__exact=self.value())
else:
return queryset.filter(investigator__id__exact=self.value())
def __new__(self, *args, **kwargs):
logger.info("timing __new__")
with Timer() as t:
return super().__new__(self, *args, **kwargs)
# Columns # Columns
list_display = [ list_display = [
@ -907,7 +899,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
"anything_else", "anything_else",
"is_policy_acknowledged", "is_policy_acknowledged",
] ]
autocomplete_fields = ["submitter"]
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts") filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
# Table ordering # Table ordering
@ -918,77 +910,73 @@ class DomainApplicationAdmin(ListHeaderAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs): def formfield_for_manytomany(self, db_field, request, **kwargs):
logger.info(f"timing formfield_for_manytomany -> {db_field.name}") logger.info(f"timing formfield_for_manytomany -> {db_field.name}")
with Timer() as t: with Timer() as t:
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) return super().formfield_for_manytomany(db_field, request, **kwargs)
def formfield_for_foreignkey(self, db_field, request, **kwargs): def formfield_for_foreignkey(self, db_field, request, **kwargs):
logger.info(f"timing formfield_for_foreignkey -> {db_field.name}")
with Timer() as t: with Timer() as t:
print(f"This is the db_field: {db_field}")
return super().formfield_for_foreignkey(db_field, request, **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):
logger.info("timing save_model") if obj and obj.creator.status != models.User.RESTRICTED:
with Timer() as t: if change: # Check if the application is being edited
if obj and obj.creator.status != models.User.RESTRICTED: # Get the original application from the database
if change: # Check if the application is being edited original_obj = models.DomainApplication.objects.get(pk=obj.pk)
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if ( if (
obj obj
and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
and obj.status != models.DomainApplication.ApplicationStatus.APPROVED and obj.status != models.DomainApplication.ApplicationStatus.APPROVED
and not obj.domain_is_not_active() and not obj.domain_is_not_active()
): ):
# If an admin tried to set an approved application to # If an admin tried to set an approved application to
# another status and the related domain is already # another status and the related domain is already
# active, shortcut the action and throw a friendly # active, shortcut the action and throw a friendly
# error message. This action would still not go through # error message. This action would still not go through
# shortcut or not as the rules are duplicated on the model, # shortcut or not as the rules are duplicated on the model,
# but the error would be an ugly Django error screen. # but the error would be an ugly Django error screen.
# Clear the success message # Clear the success message
messages.set_level(request, messages.ERROR) messages.set_level(request, messages.ERROR)
messages.error( messages.error(
request, request,
"This action is not permitted. The domain is already active.", "This action is not permitted. The domain is already active.",
) )
else: else:
if obj.status != original_obj.status: if obj.status != original_obj.status:
status_method_mapping = { status_method_mapping = {
models.DomainApplication.ApplicationStatus.STARTED: None, models.DomainApplication.ApplicationStatus.STARTED: None,
models.DomainApplication.ApplicationStatus.SUBMITTED: obj.submit, models.DomainApplication.ApplicationStatus.SUBMITTED: obj.submit,
models.DomainApplication.ApplicationStatus.IN_REVIEW: obj.in_review, models.DomainApplication.ApplicationStatus.IN_REVIEW: obj.in_review,
models.DomainApplication.ApplicationStatus.ACTION_NEEDED: obj.action_needed, models.DomainApplication.ApplicationStatus.ACTION_NEEDED: obj.action_needed,
models.DomainApplication.ApplicationStatus.APPROVED: obj.approve, models.DomainApplication.ApplicationStatus.APPROVED: obj.approve,
models.DomainApplication.ApplicationStatus.WITHDRAWN: obj.withdraw, models.DomainApplication.ApplicationStatus.WITHDRAWN: obj.withdraw,
models.DomainApplication.ApplicationStatus.REJECTED: obj.reject, models.DomainApplication.ApplicationStatus.REJECTED: obj.reject,
models.DomainApplication.ApplicationStatus.INELIGIBLE: (obj.reject_with_prejudice), models.DomainApplication.ApplicationStatus.INELIGIBLE: (obj.reject_with_prejudice),
} }
selected_method = status_method_mapping.get(obj.status) selected_method = status_method_mapping.get(obj.status)
if selected_method is None: if selected_method is None:
logger.warning("Unknown status selected in django admin") logger.warning("Unknown status selected in django admin")
else: else:
# This is an fsm in model which will throw an error if the # This is an fsm in model which will throw an error if the
# transition condition is violated, so we roll back the # transition condition is violated, so we roll back the
# status to what it was before the admin user changed it and # status to what it was before the admin user changed it and
# let the fsm method set it. # let the fsm method set it.
obj.status = original_obj.status obj.status = original_obj.status
selected_method() selected_method()
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
else: else:
# Clear the success message # Clear the success message
messages.set_level(request, messages.ERROR) messages.set_level(request, messages.ERROR)
messages.error( messages.error(
request, request,
"This action is not permitted for applications with a restricted creator.", "This action is not permitted for applications with a restricted creator.",
) )
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.
@ -996,41 +984,35 @@ 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.
""" """
logger.info("timing get_readonly_fields") readonly_fields = list(self.readonly_fields)
with Timer() as t:
readonly_fields = list(self.readonly_fields)
# Check if the creator is restricted # Check if the creator is restricted
if obj and obj.creator.status == models.User.RESTRICTED: if obj and obj.creator.status == models.User.RESTRICTED:
# For fields like CharField, IntegerField, etc., the widget used is # For fields like CharField, IntegerField, etc., the widget used is
# straightforward and the readonly_fields list can control their behavior # straightforward and the readonly_fields list can control their behavior
readonly_fields.extend([field.name for field in self.model._meta.fields]) readonly_fields.extend([field.name for field in self.model._meta.fields])
# Add the multi-select fields to readonly_fields: # Add the multi-select fields to readonly_fields:
# Complex fields like ManyToManyField require special handling # Complex fields like ManyToManyField require special handling
readonly_fields.extend(["current_websites", "other_contacts", "alternative_domains"]) readonly_fields.extend(["current_websites", "other_contacts", "alternative_domains"])
if request.user.has_perm("registrar.full_access_permission"): if request.user.has_perm("registrar.full_access_permission"):
return readonly_fields
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields return readonly_fields
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
def display_restricted_warning(self, request, obj): def display_restricted_warning(self, request, obj):
logger.info("timing display_restricted_warning") if obj and obj.creator.status == models.User.RESTRICTED:
with Timer() as t: messages.warning(
if obj and obj.creator.status == models.User.RESTRICTED: request,
messages.warning( "Cannot edit an application with a restricted creator.",
request, )
"Cannot edit an application with a restricted creator.",
)
def change_view(self, request, object_id, form_url="", extra_context=None): def change_view(self, request, object_id, form_url="", extra_context=None):
logger.info("timing change_view") obj = self.get_object(request, object_id)
with Timer() as t: self.display_restricted_warning(request, obj)
obj = self.get_object(request, object_id) return super().change_view(request, object_id, form_url, extra_context)
self.display_restricted_warning(request, obj)
return super().change_view(request, object_id, form_url, extra_context)
class TransitionDomainAdmin(ListHeaderAdmin): class TransitionDomainAdmin(ListHeaderAdmin):

View file

@ -270,3 +270,7 @@ h1, h2, h3,
margin: 0!important; margin: 0!important;
} }
} }
.select2-dropdown {
display: inline-grid !important;
}