File: __init__.py

package info (click to toggle)
python-django-extensions 4.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,812 kB
  • sloc: python: 18,601; javascript: 7,354; makefile: 108; xml: 17
file content (195 lines) | stat: -rw-r--r-- 7,316 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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