Features: 1) Add AjaxAutocompleteListFilter to enable autocomplete functionality for admin list filters; 2) Introduce AjaxAutocompleteSelectWidget for enhanced UI integration; 3) Update ProductAdmin to use new autocomplete list filter; 4) Add template and styles for AutocompleteListFilter.

Fixes: None;

Extra: Add supporting JS and CSS for autocomplete list filter functionality.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-07-01 16:28:07 +03:00
parent 455c3d71b3
commit c5fe0cb6c6
4 changed files with 131 additions and 2 deletions

View file

@ -3,9 +3,12 @@ from contextlib import suppress
from constance.admin import Config
from constance.admin import ConstanceAdmin as BaseConstanceAdmin
from django.apps import apps
from django.contrib.admin import ModelAdmin, TabularInline, action, register, site
from django.contrib.admin import ModelAdmin, RelatedFieldListFilter, TabularInline, action, register, site
from django.contrib.admin.widgets import AutocompleteSelect
from django.contrib.gis.admin import GISModelAdmin
from django.db.models import Model
from django.db.models.fields.related_descriptors import ManyToManyDescriptor, ReverseManyToOneDescriptor
from django.forms import CharField, Form, HiddenInput, ModelChoiceField
from django.utils.translation import gettext_lazy as _
from modeltranslation.translator import NotRegistered, translator
from modeltranslation.utils import get_translation_fields
@ -112,6 +115,88 @@ class ActivationActionsMixin:
return actions
class AjaxAutocompleteSelectWidget(AutocompleteSelect):
def __init__(self, *args, **kwargs):
self.qs_target_value = kwargs.pop('qs_target_value')
self.model_admin = kwargs.pop('model_admin')
self.model = kwargs.pop('model')
self.field_name = kwargs.pop('field_name')
kwargs.update(admin_site=self.model_admin.admin_site)
kwargs.update(field=getattr(self.model, self.field_name).field)
super().__init__(*args, **kwargs)
def render(self, name, value, attrs=None, renderer=None):
rendered = super().render(name, value, attrs, renderer)
return (f'<div class="ajax-autocomplete-select-widget-wrapper" data-qs-target-value="{self.qs_target_value}">'
f'{rendered}</div>')
class AjaxAutocompleteListFilter(RelatedFieldListFilter):
title = _('list filter')
parameter_name = '%s__%s__exact'
template = 'djaa_list_filter/admin/filter/autocomplete_list_filter.html'
def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
qs_target_value = self.parameter_name % (field.name, model._meta.pk.name)
queryset = self.get_queryset_for_field(model, field.name)
widget = AjaxAutocompleteSelectWidget(
model_admin=model_admin, model=model, field_name=field.name, qs_target_value=qs_target_value
)
class AutocompleteForm(Form):
autocomplete_field = ModelChoiceField(queryset=queryset, widget=widget, required=False)
querystring_value = CharField(widget=HiddenInput())
autocomplete_field_initial_value = request.GET.get(qs_target_value, None)
initial_values = {"querystring_value": request.GET.urlencode()}
if autocomplete_field_initial_value:
initial_values.update(autocomplete_field=autocomplete_field_initial_value)
self.autocomplete_form = AutocompleteForm(initial=initial_values, prefix=field.name)
def get_queryset_for_field(self, model, name):
field_desc = getattr(model, name)
if isinstance(field_desc, ManyToManyDescriptor):
related_model = field_desc.rel.related_model if field_desc.reverse else field_desc.rel.model
elif isinstance(field_desc, ReverseManyToOneDescriptor):
related_model = field_desc.rel.related_model
else:
return field_desc.get_queryset()
return related_model.objects.get_queryset()
class AjaxAutocompleteListFilterModelAdmin(ModelAdmin):
def get_list_filter(self, request):
list_filter = list(super().get_list_filter(request))
autocomplete_list_filter = self.get_autocomplete_list_filter()
if autocomplete_list_filter:
for field in autocomplete_list_filter:
list_filter.insert(0, (field, AjaxAutocompleteListFilter))
return list_filter
def get_autocomplete_list_filter(self):
return list(getattr(self, 'autocomplete_list_filter', []))
class Media:
js = [
'admin/js/vendor/jquery/jquery.js',
'admin/js/vendor/select2/select2.full.js',
'admin/js/vendor/select2/i18n/tr.js',
'admin/js/jquery.init.js',
'admin/js/autocomplete.js',
'djaa_list_filter/admin/js/autocomplete_list_filter.js',
]
css = {
'screen': [
'admin/css/vendor/select2/select2.css',
'admin/css/autocomplete.css',
'djaa_list_filter/admin/css/autocomplete_list_filter.css',
]
}
class AttributeValueInline(TabularInline):
model = AttributeValue
extra = 0
@ -230,7 +315,7 @@ class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
@register(Product)
class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, AjaxAutocompleteListFilterModelAdmin):
model = Product # type: ignore
list_display = (
"name",

View file

@ -0,0 +1,3 @@
.ajax-autocomplete-select-widget-wrapper .select2-container--admin-autocomplete {
width: 100% !important;
}

View file

@ -0,0 +1,32 @@
function handle_querystring_and_redirect(querystring_value, qs_target_value, selection) {
let required_queryset = [];
if (querystring_value.length > 0) {
for(const field_eq_value of querystring_value.split("&")){
let [field, value] = field_eq_value.split("=");
if (field !== qs_target_value){
required_queryset.push(field_eq_value)
}
}
}
if (selection.length > 0) {
required_queryset.push(qs_target_value + "=" + selection);
}
window.location.href = "?" + required_queryset.join("&");
}
django.jQuery(document).ready(function(){
django.jQuery(".ajax-autocomplete-select-widget-wrapper select").on('select2:unselect', function(e){
let qs_target_value = django.jQuery(this).parent().data("qs-target-value");
let querystring_value = django.jQuery(this).closest("form").find('input[name$="querystring_value"]').val();
handle_querystring_and_redirect(querystring_value, qs_target_value, "");
});
django.jQuery(".ajax-autocomplete-select-widget-wrapper select").on('change', function(e, choice){
let selection = django.jQuery(e.target).val() || "";
let qs_target_value = django.jQuery(this).parent().data("qs-target-value");
let querystring_value = django.jQuery(this).closest("form").find('input[name$="querystring_value"]').val();
if(selection.length > 0){
handle_querystring_and_redirect(querystring_value, qs_target_value, selection);
}
});
});

View file

@ -0,0 +1,9 @@
{% load i18n static %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<form method="get">
<ul>
<li>{{ spec.autocomplete_form.autocomplete_field }}</li>
</ul>
{{ spec.autocomplete_form.querystring_value }}
</form>