This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
#
# Autocomplete feature for admin panel
#
import operator
from functools import update_wrapper, reduce
from typing import Tuple, Dict, Callable # NOQA
from django.apps import apps
from django.http import HttpResponse, HttpResponseNotFound
from django.conf import settings
from django.db import models
from django.db.models.query import QuerySet
from django.utils.encoding import smart_str
from django.utils.translation import gettext as _
from django.utils.text import get_text_list
from django.contrib import admin
from django_extensions.admin.widgets import ForeignKeySearchInput
class ForeignKeyAutocompleteAdminMixin:
"""
Admin class for models using the autocomplete feature.
There are two additional fields:
- related_search_fields: defines fields of managed model that
have to be represented by autocomplete input, together with
a list of target model fields that are searched for
input string, e.g.:
related_search_fields = {
'author': ('first_name', 'email'),
}
- related_string_functions: contains optional functions which
take target model instance as only argument and return string
representation. By default __unicode__() method of target
object is used.
And also an optional additional field to set the limit on the
results returned by the autocomplete query. You can set this integer
value in your settings file using FOREIGNKEY_AUTOCOMPLETE_LIMIT or
you can set this per ForeignKeyAutocompleteAdmin basis. If any value
is set the results will not be limited.
"""
related_search_fields = {} # type: Dict[str, Tuple[str]]
related_string_functions = {} # type: Dict[str, Callable]
autocomplete_limit = getattr(settings, 'FOREIGNKEY_AUTOCOMPLETE_LIMIT', None)
def get_urls(self):
from django.urls import path
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
return update_wrapper(wrapper, view)
return [
path('foreignkey_autocomplete/', wrap(self.foreignkey_autocomplete),
name='%s_%s_autocomplete' % (self.model._meta.app_label, self.model._meta.model_name))
] + super().get_urls()
def foreignkey_autocomplete(self, request):
"""
Search in the fields of the given related model and returns the
result as a simple string to be used by the jQuery Autocomplete plugin
"""
query = request.GET.get('q', None)
app_label = request.GET.get('app_label', None)
model_name = request.GET.get('model_name', None)
search_fields = request.GET.get('search_fields', None)
object_pk = request.GET.get('object_pk', None)
try:
to_string_function = self.related_string_functions[model_name]
except KeyError:
to_string_function = lambda x: x.__str__()
if search_fields and app_label and model_name and (query or object_pk):
def construct_search(field_name):
# use different lookup methods depending on the notation
if field_name.startswith('^'):
return "%s__istartswith" % field_name[1:]
elif field_name.startswith('='):
return "%s__iexact" % field_name[1:]
elif field_name.startswith('@'):
return "%s__search" % field_name[1:]
else:
return "%s__icontains" % field_name
model = apps.get_model(app_label, model_name)
queryset = model._default_manager.all()
data = ''
if query:
for bit in query.split():
or_queries = [models.Q(**{construct_search(smart_str(field_name)): smart_str(bit)}) for field_name in search_fields.split(',')]
other_qs = QuerySet(model)
other_qs.query.select_related = queryset.query.select_related
other_qs = other_qs.filter(reduce(operator.or_, or_queries))
queryset = queryset & other_qs
additional_filter = self.get_related_filter(model, request)
if additional_filter:
queryset = queryset.filter(additional_filter)
if self.autocomplete_limit:
queryset = queryset[:self.autocomplete_limit]
data = ''.join([str('%s|%s\n') % (to_string_function(f), f.pk) for f in queryset])
elif object_pk:
try:
obj = queryset.get(pk=object_pk)
except Exception: # FIXME: use stricter exception checking
pass
else:
data = to_string_function(obj)
return HttpResponse(data, content_type='text/plain')
return HttpResponseNotFound()
def get_related_filter(self, model, request):
"""
Given a model class and current request return an optional Q object
that should be applied as an additional filter for autocomplete query.
If no additional filtering is needed, this method should return
None.
"""
return None
def get_help_text(self, field_name, model_name):
searchable_fields = self.related_search_fields.get(field_name, None)
if searchable_fields:
help_kwargs = {
'model_name': model_name,
'field_list': get_text_list(searchable_fields, _('and')),
}
return _('Use the left field to do %(model_name)s lookups in the fields %(field_list)s.') % help_kwargs
return ''
def formfield_for_dbfield(self, db_field, **kwargs):
"""
Override the default widget for Foreignkey fields if they are
specified in the related_search_fields class attribute.
"""
if isinstance(db_field, models.ForeignKey) and db_field.name in self.related_search_fields:
help_text = self.get_help_text(db_field.name, db_field.remote_field.model._meta.object_name)
if kwargs.get('help_text'):
help_text = str('%s %s') % (kwargs['help_text'], help_text)
kwargs['widget'] = ForeignKeySearchInput(db_field.remote_field, self.related_search_fields[db_field.name])
kwargs['help_text'] = help_text
return super().formfield_for_dbfield(db_field, **kwargs)
class ForeignKeyAutocompleteAdmin(ForeignKeyAutocompleteAdminMixin, admin.ModelAdmin):
pass
class ForeignKeyAutocompleteTabularInline(ForeignKeyAutocompleteAdminMixin, admin.TabularInline):
pass
class ForeignKeyAutocompleteStackedInline(ForeignKeyAutocompleteAdminMixin, admin.StackedInline):
pass

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from django.contrib.admin import FieldListFilter
from django.contrib.admin.utils import prepare_lookup_value
from django.utils.translation import gettext_lazy as _
class NullFieldListFilter(FieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
self.lookup_kwarg = '{0}__isnull'.format(field_path)
super().__init__(field, request, params, model, model_admin, field_path)
lookup_choices = self.lookups(request, model_admin)
self.lookup_choices = () if lookup_choices is None else list(lookup_choices)
def expected_parameters(self):
return [self.lookup_kwarg]
def value(self):
return self.used_parameters.get(self.lookup_kwarg, None)
def lookups(self, request, model_admin):
return (
('1', _('Yes')),
('0', _('No')),
)
def choices(self, cl):
yield {
'selected': self.value() is None,
'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
'display': _('All'),
}
for lookup, title in self.lookup_choices:
yield {
'selected': self.value() == prepare_lookup_value(self.lookup_kwarg, lookup),
'query_string': cl.get_query_string({
self.lookup_kwarg: lookup,
}, []),
'display': title,
}
def queryset(self, request, queryset):
if self.value() is not None:
kwargs = {self.lookup_kwarg: self.value()}
return queryset.filter(**kwargs)
return queryset
class NotNullFieldListFilter(NullFieldListFilter):
def lookups(self, request, model_admin):
return (
('0', _('Yes')),
('1', _('No')),
)

View File

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
import urllib
from django import forms
from django.contrib.admin.sites import site
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.template.loader import render_to_string
from django.templatetags.static import static
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.text import Truncator
class ForeignKeySearchInput(ForeignKeyRawIdWidget):
"""
Widget for displaying ForeignKeys in an autocomplete search input
instead in a <select> box.
"""
# Set in subclass to render the widget with a different template
widget_template = None
# Set this to the patch of the search view
search_path = None
def _media(self):
js_files = [
static('django_extensions/js/jquery.bgiframe.js'),
static('django_extensions/js/jquery.ajaxQueue.js'),
static('django_extensions/js/jquery.autocomplete.js'),
]
return forms.Media(
css={'all': (static('django_extensions/css/jquery.autocomplete.css'), )},
js=js_files,
)
media = property(_media)
def label_for_value(self, value):
key = self.rel.get_related_field().name
obj = self.rel.model._default_manager.get(**{key: value})
return Truncator(obj).words(14, truncate='...')
def __init__(self, rel, search_fields, attrs=None):
self.search_fields = search_fields
super().__init__(rel, site, attrs)
def render(self, name, value, attrs=None, renderer=None):
if attrs is None:
attrs = {}
opts = self.rel.model._meta
app_label = opts.app_label
model_name = opts.object_name.lower()
related_url = reverse('admin:%s_%s_changelist' % (app_label, model_name))
if not self.search_path:
self.search_path = urllib.parse.urljoin(related_url, 'foreignkey_autocomplete/')
params = self.url_parameters()
if params:
url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
else:
url = ''
if 'class' not in attrs:
attrs['class'] = 'vForeignKeyRawIdAdminField'
# Call the TextInput render method directly to have more control
output = [forms.TextInput.render(self, name, value, attrs)]
if value:
label = self.label_for_value(value)
else:
label = ''
context = {
'url': url,
'related_url': related_url,
'search_path': self.search_path,
'search_fields': ','.join(self.search_fields),
'app_label': app_label,
'model_name': model_name,
'label': label,
'name': name,
}
output.append(render_to_string(self.widget_template or (
'django_extensions/widgets/%s/%s/foreignkey_searchinput.html' % (app_label, model_name),
'django_extensions/widgets/%s/foreignkey_searchinput.html' % app_label,
'django_extensions/widgets/foreignkey_searchinput.html',
), context))
output.reverse()
return mark_safe(''.join(output))