__init__.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Autocomplete feature for admin panel
  4. #
  5. import six
  6. import operator
  7. from functools import update_wrapper
  8. from six.moves import reduce
  9. from typing import Tuple, Dict, Callable # NOQA
  10. from django.apps import apps
  11. from django.http import HttpResponse, HttpResponseNotFound
  12. from django.conf import settings
  13. from django.db import models
  14. from django.db.models.query import QuerySet
  15. from django.utils.encoding import smart_str
  16. from django.utils.translation import gettext as _
  17. from django.utils.text import get_text_list
  18. from django.contrib import admin
  19. from django_extensions.admin.widgets import ForeignKeySearchInput
  20. class ForeignKeyAutocompleteAdminMixin:
  21. """
  22. Admin class for models using the autocomplete feature.
  23. There are two additional fields:
  24. - related_search_fields: defines fields of managed model that
  25. have to be represented by autocomplete input, together with
  26. a list of target model fields that are searched for
  27. input string, e.g.:
  28. related_search_fields = {
  29. 'author': ('first_name', 'email'),
  30. }
  31. - related_string_functions: contains optional functions which
  32. take target model instance as only argument and return string
  33. representation. By default __unicode__() method of target
  34. object is used.
  35. And also an optional additional field to set the limit on the
  36. results returned by the autocomplete query. You can set this integer
  37. value in your settings file using FOREIGNKEY_AUTOCOMPLETE_LIMIT or
  38. you can set this per ForeignKeyAutocompleteAdmin basis. If any value
  39. is set the results will not be limited.
  40. """
  41. related_search_fields = {} # type: Dict[str, Tuple[str]]
  42. related_string_functions = {} # type: Dict[str, Callable]
  43. autocomplete_limit = getattr(settings, 'FOREIGNKEY_AUTOCOMPLETE_LIMIT', None)
  44. def get_urls(self):
  45. from django.urls import path
  46. def wrap(view):
  47. def wrapper(*args, **kwargs):
  48. return self.admin_site.admin_view(view)(*args, **kwargs)
  49. return update_wrapper(wrapper, view)
  50. return [
  51. path('foreignkey_autocomplete/', wrap(self.foreignkey_autocomplete),
  52. name='%s_%s_autocomplete' % (self.model._meta.app_label, self.model._meta.model_name))
  53. ] + super().get_urls()
  54. def foreignkey_autocomplete(self, request):
  55. """
  56. Search in the fields of the given related model and returns the
  57. result as a simple string to be used by the jQuery Autocomplete plugin
  58. """
  59. query = request.GET.get('q', None)
  60. app_label = request.GET.get('app_label', None)
  61. model_name = request.GET.get('model_name', None)
  62. search_fields = request.GET.get('search_fields', None)
  63. object_pk = request.GET.get('object_pk', None)
  64. try:
  65. to_string_function = self.related_string_functions[model_name]
  66. except KeyError:
  67. to_string_function = lambda x: x.__str__()
  68. if search_fields and app_label and model_name and (query or object_pk):
  69. def construct_search(field_name):
  70. # use different lookup methods depending on the notation
  71. if field_name.startswith('^'):
  72. return "%s__istartswith" % field_name[1:]
  73. elif field_name.startswith('='):
  74. return "%s__iexact" % field_name[1:]
  75. elif field_name.startswith('@'):
  76. return "%s__search" % field_name[1:]
  77. else:
  78. return "%s__icontains" % field_name
  79. model = apps.get_model(app_label, model_name)
  80. queryset = model._default_manager.all()
  81. data = ''
  82. if query:
  83. for bit in query.split():
  84. or_queries = [models.Q(**{construct_search(smart_str(field_name)): smart_str(bit)}) for field_name in search_fields.split(',')]
  85. other_qs = QuerySet(model)
  86. other_qs.query.select_related = queryset.query.select_related
  87. other_qs = other_qs.filter(reduce(operator.or_, or_queries))
  88. queryset = queryset & other_qs
  89. additional_filter = self.get_related_filter(model, request)
  90. if additional_filter:
  91. queryset = queryset.filter(additional_filter)
  92. if self.autocomplete_limit:
  93. queryset = queryset[:self.autocomplete_limit]
  94. data = ''.join([six.u('%s|%s\n') % (to_string_function(f), f.pk) for f in queryset])
  95. elif object_pk:
  96. try:
  97. obj = queryset.get(pk=object_pk)
  98. except Exception: # FIXME: use stricter exception checking
  99. pass
  100. else:
  101. data = to_string_function(obj)
  102. return HttpResponse(data, content_type='text/plain')
  103. return HttpResponseNotFound()
  104. def get_related_filter(self, model, request):
  105. """
  106. Given a model class and current request return an optional Q object
  107. that should be applied as an additional filter for autocomplete query.
  108. If no additional filtering is needed, this method should return
  109. None.
  110. """
  111. return None
  112. def get_help_text(self, field_name, model_name):
  113. searchable_fields = self.related_search_fields.get(field_name, None)
  114. if searchable_fields:
  115. help_kwargs = {
  116. 'model_name': model_name,
  117. 'field_list': get_text_list(searchable_fields, _('and')),
  118. }
  119. return _('Use the left field to do %(model_name)s lookups in the fields %(field_list)s.') % help_kwargs
  120. return ''
  121. def formfield_for_dbfield(self, db_field, **kwargs):
  122. """
  123. Override the default widget for Foreignkey fields if they are
  124. specified in the related_search_fields class attribute.
  125. """
  126. if isinstance(db_field, models.ForeignKey) and db_field.name in self.related_search_fields:
  127. help_text = self.get_help_text(db_field.name, db_field.remote_field.model._meta.object_name)
  128. if kwargs.get('help_text'):
  129. help_text = six.u('%s %s' % (kwargs['help_text'], help_text))
  130. kwargs['widget'] = ForeignKeySearchInput(db_field.remote_field, self.related_search_fields[db_field.name])
  131. kwargs['help_text'] = help_text
  132. return super().formfield_for_dbfield(db_field, **kwargs)
  133. class ForeignKeyAutocompleteAdmin(ForeignKeyAutocompleteAdminMixin, admin.ModelAdmin):
  134. pass
  135. class ForeignKeyAutocompleteTabularInline(ForeignKeyAutocompleteAdminMixin, admin.TabularInline):
  136. pass
  137. class ForeignKeyAutocompleteStackedInline(ForeignKeyAutocompleteAdminMixin, admin.StackedInline):
  138. pass