Permalink
Browse files

Merge 32f587e into 56970c5

  • Loading branch information...
2 parents 56970c5 + 32f587e commit 05587e98652ec3a58455630da62ddbfd454b3657 @codingjoe codingjoe committed on GitHub Apr 25, 2017
Showing with 8,176 additions and 20 deletions.
  1. +58 −0 django/contrib/admin/checks.py
  2. +39 −4 django/contrib/admin/options.py
  3. +261 −0 django/contrib/admin/static/admin/css/autocomplete.css
  4. +21 −0 django/contrib/admin/static/admin/css/vendor/select2/LICENSE.md
  5. +484 −0 django/contrib/admin/static/admin/css/vendor/select2/select2.css
  6. +1 −0 django/contrib/admin/static/admin/css/vendor/select2/select2.min.css
  7. +4 −0 django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
  8. +39 −0 django/contrib/admin/static/admin/js/autocomplete.js
  9. +21 −0 django/contrib/admin/static/admin/js/vendor/select2/LICENSE.md
  10. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/ar.js
  11. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/az.js
  12. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/bg.js
  13. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/ca.js
  14. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/cs.js
  15. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/da.js
  16. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/de.js
  17. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/el.js
  18. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/en.js
  19. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/es.js
  20. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/et.js
  21. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/eu.js
  22. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/fa.js
  23. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/fi.js
  24. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/fr.js
  25. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/gl.js
  26. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/he.js
  27. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/hi.js
  28. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/hr.js
  29. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/hu.js
  30. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/id.js
  31. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/is.js
  32. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/it.js
  33. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/ja.js
  34. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/km.js
  35. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/ko.js
  36. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/lt.js
  37. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/lv.js
  38. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/mk.js
  39. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/ms.js
  40. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/nb.js
  41. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/nl.js
  42. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/pl.js
  43. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/pt-BR.js
  44. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/pt.js
  45. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/ro.js
  46. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/ru.js
  47. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/sk.js
  48. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/sr-Cyrl.js
  49. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/sr.js
  50. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/sv.js
  51. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/th.js
  52. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/tr.js
  53. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/uk.js
  54. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/vi.js
  55. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/zh-CN.js
  56. +3 −0 django/contrib/admin/static/admin/js/vendor/select2/i18n/zh-TW.js
  57. +6,440 −0 django/contrib/admin/static/admin/js/vendor/select2/select2.full.js
  58. +5 −0 django/contrib/admin/static/admin/js/vendor/select2/select2.full.min.js
  59. +92 −0 django/contrib/admin/views/autocomplete.py
  60. +117 −0 django/contrib/admin/widgets.py
  61. +9 −0 docs/ref/checks.txt
  62. +59 −4 docs/ref/contrib/admin/index.txt
  63. +3 −2 docs/ref/models/fields.txt
  64. +2 −1 docs/releases/2.0.txt
  65. +1 −0 docs/spelling_wordlist
  66. +66 −0 tests/admin_checks/tests.py
  67. +1 −1 tests/admin_scripts/tests.py
  68. +33 −7 tests/admin_views/admin.py
  69. +31 −1 tests/admin_views/models.py
  70. +161 −0 tests/admin_views/test_autocomplete_view.py
  71. +87 −0 tests/admin_widgets/test_autocomplete_widget.py
@@ -67,6 +67,7 @@ class BaseModelAdminChecks:
def check(self, admin_obj, **kwargs):
errors = []
errors.extend(self._check_raw_id_fields(admin_obj))
+ errors.extend(self._check_autocomplete_fields(admin_obj))
errors.extend(self._check_fields(admin_obj))
errors.extend(self._check_fieldsets(admin_obj))
errors.extend(self._check_exclude(admin_obj))
@@ -109,6 +110,63 @@ def _check_raw_id_fields_item(self, obj, model, field_name, label):
else:
return []
+ def _check_autocomplete_fields(self, obj):
+ """
+ Check that `autocomplete_fields` only contains field names that are
+ listed on the model.
+ """
+ if not isinstance(obj.autocomplete_fields, (list, tuple)):
+ return must_be('a list or tuple', option='autocomplete_fields', obj=obj, id='admin.E036')
+ else:
+ return list(chain(*[
+ self._check_autocomplete_fields_item(obj, obj.model, field_name, 'autocomplete_fields[%d]' % index)
+ for index, field_name in enumerate(obj.autocomplete_fields)
+ ]))
+
+ def _check_autocomplete_fields_item(self, obj, model, field_name, label):
+ """
+ Check an item of `autocomplete_fields`, i.e. check that field named
+ `field_name` exists in model `model` and is a ForeignKey or a
+ ManyToManyField.
+ """
+ try:
+ field = model._meta.get_field(field_name)
+ except FieldDoesNotExist:
+ return refer_to_missing_field(field=field_name, option=label, model=model, obj=obj, id='admin.E037')
+ else:
+ if not (field.many_to_many or isinstance(field, models.ForeignKey)):
+ return must_be(
+ 'a foreign key or a many-to-many field',
+ option=label, obj=obj, id='admin.E038'
+ )
+ related_admin = obj.admin_site._registry.get(field.remote_field.model)
+ if related_admin is None:
+ return [
+ checks.Error(
+ 'An admin for model "%s" has to be registered'
+ ' to be referenced by %s.autocomplete_fields.' % (
+ field.remote_field.model.__name__,
+ type(obj).__name__,
+ ),
+ obj=obj.__class__,
+ id='admin.E039',
+ )
+ ]
+ elif not related_admin.search_fields:
+ return [
+ checks.Error(
+ '%s, the admin for %s, needs to implement "search_fields",'
+ ' because it\'s referenced by %s.autocomplete_fields.' % (
+ related_admin,
+ field.remote_field.model.__name__,
+ type(obj).__name__,
+ ),
+ obj=obj.__class__,
+ id='admin.E040',
+ )
+ ]
+ return []
+
def _check_fields(self, obj):
""" Check that `fields` only refer to existing fields, doesn't contain
duplicates. Check if at most one of `fields` and `fieldsets` is defined.
@@ -19,6 +19,10 @@
get_deleted_objects, lookup_needs_distinct, model_format_dict,
model_ngettext, quote, unquote,
)
+from django.contrib.admin.views.autocomplete import AutocompleteJsonView
+from django.contrib.admin.widgets import (
+ AutocompleteSelect, AutocompleteSelectMultiple,
+)
from django.contrib.auth import get_permission_codename
from django.core.exceptions import (
FieldDoesNotExist, FieldError, PermissionDenied, ValidationError,
@@ -95,6 +99,7 @@ class IncorrectLookupParameters(Exception):
class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
"""Functionality common to both ModelAdmin and InlineAdmin."""
+ autocomplete_fields = ()
raw_id_fields = ()
fields = None
exclude = None
@@ -214,7 +219,10 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
Get a form Field for a ForeignKey.
"""
db = kwargs.get('using')
- if db_field.name in self.raw_id_fields:
+
+ if db_field.name in self.get_autocomplete_fields(request):
+ kwargs['widget'] = AutocompleteSelect(model=self.model, using=db)
+ elif db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.remote_field, self.admin_site, using=db)
elif db_field.name in self.radio_fields:
kwargs['widget'] = widgets.AdminRadioSelect(attrs={
@@ -239,7 +247,10 @@ def formfield_for_manytomany(self, db_field, request, **kwargs):
return None
db = kwargs.get('using')
- if db_field.name in self.raw_id_fields:
+ autocomplete_fields = self.get_autocomplete_fields(request)
+ if db_field.name in autocomplete_fields:
+ kwargs['widget'] = AutocompleteSelectMultiple(model=self.model, using=db)
+ elif db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.remote_field, self.admin_site, using=db)
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
kwargs['widget'] = widgets.FilteredSelectMultiple(
@@ -253,12 +264,17 @@ def formfield_for_manytomany(self, db_field, request, **kwargs):
kwargs['queryset'] = queryset
form_field = db_field.formfield(**kwargs)
- if isinstance(form_field.widget, SelectMultiple) and not isinstance(form_field.widget, CheckboxSelectMultiple):
+ if isinstance(form_field.widget, SelectMultiple) and \
+ not isinstance(form_field.widget, CheckboxSelectMultiple) and \
+ not isinstance(form_field.widget, AutocompleteSelectMultiple):
msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
help_text = form_field.help_text
form_field.help_text = format_lazy('{} {}', help_text, msg) if help_text else msg
return form_field
+ def get_autocomplete_fields(self, request):
+ return self.autocomplete_fields
+
def get_view_on_site_url(self, obj=None):
if obj is None or not self.view_on_site:
return None
@@ -558,14 +574,33 @@ def wrapper(*args, **kwargs):
urlpatterns = [
url(r'^$', wrap(self.changelist_view), name='%s_%s_changelist' % info),
url(r'^add/$', wrap(self.add_view), name='%s_%s_add' % info),
+ url(r'^autocomplete/(?P<field_name>.+)/$',
+ wrap(AutocompleteJsonView.as_view(
+ admin_site=self.admin_site,
+ source_model=self.model,
+ source_model_admin=self,
+ )), name='%s_%s_autocomplete' % info),
url(r'^(.+)/history/$', wrap(self.history_view), name='%s_%s_history' % info),
url(r'^(.+)/delete/$', wrap(self.delete_view), name='%s_%s_delete' % info),
url(r'^(.+)/change/$', wrap(self.change_view), name='%s_%s_change' % info),
+ ]
+ for inline in self.inlines:
+ inline_info = inline.model._meta.app_label, inline.model._meta.model_name
+ if inline.autocomplete_fields:
+ urlpatterns.append(
+ url(r'^autocomplete_inline/(?P<field_name>.+)/$',
+ wrap(AutocompleteJsonView.as_view(
+ admin_site=self.admin_site,
+ source_model=inline.model,
+ source_model_admin=inline(inline.model, self.admin_site),
+ )), name='%s_%s_autocomplete' % inline_info)
+ )
+ urlpatterns.append(
# For backwards compatibility (was the change url before 1.9)
url(r'^(.+)/$', wrap(RedirectView.as_view(
pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info)
))),
- ]
+ )
return urlpatterns
@property
Oops, something went wrong.

0 comments on commit 05587e9

Please sign in to comment.