Skip to content

Commit

Permalink
Edits
Browse files Browse the repository at this point in the history
  • Loading branch information
timgraham committed Sep 13, 2017
1 parent a0fd014 commit a674a36
Show file tree
Hide file tree
Showing 16 changed files with 193 additions and 249 deletions.
71 changes: 35 additions & 36 deletions django/contrib/admin/checks.py
Expand Up @@ -66,8 +66,8 @@ 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_raw_id_fields(admin_obj))
errors.extend(self._check_fields(admin_obj))
errors.extend(self._check_fieldsets(admin_obj))
errors.extend(self._check_exclude(admin_obj))
Expand All @@ -81,39 +81,9 @@ def check(self, admin_obj, **kwargs):
errors.extend(self._check_readonly_fields(admin_obj))
return errors

def _check_raw_id_fields(self, obj):
""" Check that `raw_id_fields` only contains field names that are listed
on the model. """

if not isinstance(obj.raw_id_fields, (list, tuple)):
return must_be('a list or tuple', option='raw_id_fields', obj=obj, id='admin.E001')
else:
return list(chain.from_iterable(
self._check_raw_id_fields_item(obj, obj.model, field_name, 'raw_id_fields[%d]' % index)
for index, field_name in enumerate(obj.raw_id_fields)
))

def _check_raw_id_fields_item(self, obj, model, field_name, label):
""" Check an item of `raw_id_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.E002')
else:
if not field.many_to_many and not isinstance(field, models.ForeignKey):
return must_be('a foreign key or a many-to-many field',
option=label, obj=obj, id='admin.E003')
else:
return []

def _check_autocomplete_fields(self, obj):
"""
Check that `autocomplete_fields` only contains field names that are
listed on the model.
Check that `autocomplete_fields` is a list or tuple of model fields.
"""
if not isinstance(obj.autocomplete_fields, (list, tuple)):
return must_be('a list or tuple', option='autocomplete_fields', obj=obj, id='admin.E036')
Expand All @@ -125,9 +95,9 @@ def _check_autocomplete_fields(self, obj):

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.
Check that an item in `autocomplete_fields` is a ForeignKey or a
ManyToManyField and that the item has a related ModelAdmin with
search_fields defined.
"""
try:
field = model._meta.get_field(field_name)
Expand Down Expand Up @@ -155,7 +125,7 @@ def _check_autocomplete_fields_item(self, obj, model, field_name, label):
elif not related_admin.search_fields:
return [
checks.Error(
'%s must implement "search_fields", because it\'s '
'%s must define "search_fields", because it\'s '
'referenced by %s.autocomplete_fields.' % (
related_admin.__class__.__name__,
type(obj).__name__,
Expand All @@ -166,6 +136,35 @@ def _check_autocomplete_fields_item(self, obj, model, field_name, label):
]
return []

def _check_raw_id_fields(self, obj):
""" Check that `raw_id_fields` only contains field names that are listed
on the model. """

if not isinstance(obj.raw_id_fields, (list, tuple)):
return must_be('a list or tuple', option='raw_id_fields', obj=obj, id='admin.E001')
else:
return list(chain.from_iterable(
self._check_raw_id_fields_item(obj, obj.model, field_name, 'raw_id_fields[%d]' % index)
for index, field_name in enumerate(obj.raw_id_fields)
))

def _check_raw_id_fields_item(self, obj, model, field_name, label):
""" Check an item of `raw_id_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.E002')
else:
if not field.many_to_many and not isinstance(field, models.ForeignKey):
return must_be('a foreign key or a many-to-many field',
option=label, obj=obj, id='admin.E003')
else:
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.
Expand Down
5 changes: 4 additions & 1 deletion django/contrib/admin/options.py
Expand Up @@ -271,7 +271,10 @@ def formfield_for_manytomany(self, db_field, request, **kwargs):
return form_field

def get_autocomplete_fields(self, request):
"""Return list of ``ForeignKey`` or ``ManyToMany`` fields."""
"""
Return a list of ForeignKey and/or ManyToMany fields which should use
an autocomplete widget.
"""
return self.autocomplete_fields

def get_view_on_site_url(self, obj=None):
Expand Down
3 changes: 1 addition & 2 deletions django/contrib/admin/static/admin/js/autocomplete.js
Expand Up @@ -30,8 +30,7 @@

$(document).on('formset:added', (function() {
return function(event, $newFormset) {
var $widget;
$widget = $newFormset.find('.admin-autocomplete');
var $widget = $newFormset.find('.admin-autocomplete');
return init($widget);
};
})(this));
Expand Down
54 changes: 17 additions & 37 deletions django/contrib/admin/views/autocomplete.py
Expand Up @@ -3,38 +3,17 @@


class AutocompleteJsonView(BaseListView):
"""
View that handles requests from `.AutocompleteWidget`.
The view only supports HTTP's GET method.
"""
"""Handle AutocompleteWidget's AJAX requests for its data."""
paginate_by = 20
model_admin = None

def has_perm(self, request, obj=None):
"""Check if user has permission to access the related model."""
return self.model_admin.has_change_permission(request, obj=obj)

def get_paginator(self, *args, **kwargs):
return self.model_admin.get_paginator(self.request, *args, **kwargs)

def get(self, request, *args, **kwargs):
"""
Return JSON search result based on a field identifier and a search term.
Returns:
(django.http.JsonResponse)::
{
results: [
{
id: "123"
text: "foo",
}
],
pagination: {
more: true
}
}
Return a JsonResponse with search results of the form:
{
results: [{id: "123" text: "foo"}],
pagination: {more: true}
}
"""
if not self.model_admin.get_search_fields(request):
raise Http404(
Expand All @@ -52,23 +31,24 @@ def get(self, request, *args, **kwargs):
context = self.get_context_data()
return JsonResponse({
'results': [
{
'id': str(obj.pk),
'text': str(obj),
}
{'id': str(obj.pk), 'text': str(obj)}
for obj in context['object_list']
],
'pagination': {
'more': context['page_obj'].has_next()
},
'pagination': {'more': context['page_obj'].has_next()},
})

def get_paginator(self, *args, **kwargs):
"""Use the ModelAdmin's paginator."""
return self.model_admin.get_paginator(self.request, *args, **kwargs)

def get_queryset(self):
"""
Return queryset based on the `.ModelAdmin.get_search_results`.
"""
"""Return queryset based on ModelAdmin.get_search_results()."""
qs = self.model_admin.get_queryset(self.request)
qs, search_use_distinct = self.model_admin.get_search_results(self.request, qs, self.term)
if search_use_distinct:
qs = qs.distinct()
return qs

def has_perm(self, request, obj=None):
"""Check if user has permission to access the related model."""
return self.model_admin.has_change_permission(request, obj=obj)
21 changes: 9 additions & 12 deletions django/contrib/admin/widgets.py
Expand Up @@ -397,12 +397,10 @@ class AdminBigIntegerFieldWidget(AdminIntegerFieldWidget):

class AutocompleteMixin:
"""
Select mixin that loads options via AJAX to avoid rendering large
Querysets.
Select widget mixin that loads options from AutocompleteJsonView via AJAX.
This class is responsible for rendering the necessary
data attributes for select2 as well as adding the static
form media.
Renders the necessary data attributes for select2 and adds the static form
media.
"""
url_name = 'admin:%s_%s_autocomplete'

Expand All @@ -424,7 +422,7 @@ def build_attrs(self, base_attrs, extra_attrs=None):
Set select2's AJAX attributes.
Attributes can be set using the html5 data attribute.
Nested attributes require a double dash, please see:
Nested attributes require a double dash as per
https://select2.org/configuration/data-attributes#nested-subkey-options
"""
attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
Expand All @@ -441,17 +439,16 @@ def build_attrs(self, base_attrs, extra_attrs=None):
return attrs

def optgroups(self, name, value, attr=None):
"""Return only selected options and set QuerySet from `ModelChoiceIterator`."""
"""Return selected options and set QuerySet from ModelChoiceIterator."""
default = (None, [], 0)
groups = [default]
has_selected = False
selected_choices = {str(v) for v in value}
if not self.is_required and not self.allow_multiple_selected:
default[1].append(self.create_option(name, '', '', False, 0))
selected_choices = {
c for c in selected_choices
if c not in self.choices.field.empty_values
str(v) for v in value
if str(v) not in self.choices.field.empty_values
}
if not self.is_required and not self.allow_multiple_selected:
default[1].append(self.create_option(name, '', '', False, 0))
choices = (
(obj.pk, self.choices.field.label_from_instance(obj))
for obj in self.choices.queryset.using(self.db).filter(pk__in=selected_choices)
Expand Down
2 changes: 1 addition & 1 deletion docs/ref/checks.txt
Expand Up @@ -534,7 +534,7 @@ with the admin site:
key or a many-to-many field.
* **admin.E039**: An admin for model ``<model>`` has to be registered to be
referenced by ``<modeladmin>.autocomplete_fields``.
* **admin.E040**: ``<modeladmin>`` must implement ``search_fields``, because
* **admin.E040**: ``<modeladmin>`` must define ``search_fields``, because
it's referenced by ``<other_modeladmin>.autocomplete_fields``.

``ModelAdmin``
Expand Down
50 changes: 23 additions & 27 deletions docs/ref/contrib/admin/index.txt
Expand Up @@ -1079,50 +1079,46 @@ subclass::

By default, the admin uses a select-box interface (``<select>``) for fields
that are ``ForeignKey`` or ``ManyToManyField``. Sometimes you don't want to
incur the overhead of having to select all the related instances to display
in the drop-down.
incur the overhead of selecting all the related instances to display in the
dropdown.

``autocomplete_fields`` is a list of fields that you would like to change
to `Select2 <contrib-admin-select2>`_ autocomplete inputs.

The Select2 input looks similar to the default input, but comes with a
search feature and loads the options asynchronously. This is faster and
more user friendly if the related model has many instances.
The Select2 input looks similar to the default input but comes with a
search feature that loads the options asynchronously. This is faster and
more user-friendly if the related model has many instances.

The autocomplete search is performed based on the
:attr:`ModelAdmin.search_fields` defined on the related object.
Therefore, you must define :attr:`ModelAdmin.search_fields` on the related
``ModelAdmin``.
You must define :attr:`~ModelAdmin.search_fields` on the related object's
``ModelAdmin`` because the autocomplete search uses it.

The ordering is defined by the related ``ModelAdmin``'s
:func:`ModelAdmin.get_ordering` implementation. Pagination is handled by
the related ``ModelAdmin``'s :func:`ModelAdmin.get_paginator`
implementation.
Ordering and pagination of the results are controlled by the related
``ModelAdmin``'s :meth:`~ModelAdmin.get_ordering` and
:meth:`~ModelAdmin.get_paginator` methods.

Example::
In the following example, ``ChoiceAdmin`` has an autocomplete field for the
``ForeignKey`` to the ``Question``. The results are filtered by the
``question_text`` field and ordered by the ``date_created`` field::

class QuestionAdmin(admin.ModelAdmin):
search_fields = ('question_text',)
ordering = ('date_created',)
ordering = ['date_created']
search_fields = ['question_text']

class ChoiceAdmin(admin.ModelAdmin):
autocomplete_fields = ('question',)

In the example, the ``ChoiceAdmin`` will have an autocomplete field for the
``ForeignKey`` to the ``Question``. The results will be filtered by the
``question_text`` and sorted by the ``date_created`` field.
autocomplete_fields = ['question']

.. admonition:: Performance
.. admonition:: Performance considerations for large datasets

Ordering using :attr:`ModelAdmin.ordering` may cause performance
problems, as sorting on a large queryset will be slow.
problems as sorting on a large queryset will be slow.

Since the default search mechanism is not index based, you might
encounter performance issues on extremely large tables.
Also, if your search fields include fields that aren't indexed by the
database, you might encounter poor performance on extremely large
tables.

For those cases, it's advisable to write your own
:func:`ModelAdmin.get_search_results` implementation based on a
full text index search.
:func:`ModelAdmin.get_search_results` implementation using a
full-text indexed search.

It's also advisable to change the ```Paginator`` on very large tables
as the default paginator always performs a ``count()`` query.
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/2.0.txt
Expand Up @@ -52,7 +52,8 @@ Minor features
:mod:`django.contrib.admin`
~~~~~~~~~~~~~~~~~~~~~~~~~~~

* The new :attr:`.ModelAdmin.autocomplete_fields` attribute allows using an
* The new :attr:`.ModelAdmin.autocomplete_fields` attribute and
:meth:`.ModelAdmin.get_autocomplete_fields` method allow using an
AJAX search widget for ``ForeignKey`` and ``ManyToManyField``.

:mod:`django.contrib.admindocs`
Expand Down

0 comments on commit a674a36

Please sign in to comment.