Updates
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# -*- 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, request, **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, request, **kwargs)
|
||||
|
||||
|
||||
class ForeignKeyAutocompleteAdmin(ForeignKeyAutocompleteAdminMixin, admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class ForeignKeyAutocompleteTabularInline(
|
||||
ForeignKeyAutocompleteAdminMixin, admin.TabularInline
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class ForeignKeyAutocompleteStackedInline(
|
||||
ForeignKeyAutocompleteAdminMixin, admin.StackedInline
|
||||
):
|
||||
pass
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,57 @@
|
||||
# -*- 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")),
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- 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
|
||||
|
||||
@property
|
||||
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,
|
||||
)
|
||||
|
||||
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 = "?" + "&".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))
|
||||
Reference in New Issue
Block a user