Fixed #19080 -- Fine-grained control over select_related in admin
This commit is contained in:
parent
4f4e9243e4
commit
0fd9f7c95f
|
@ -310,8 +310,14 @@ class ModelAdminValidator(BaseValidator):
|
||||||
% (cls.__name__, idx, field))
|
% (cls.__name__, idx, field))
|
||||||
|
|
||||||
def validate_list_select_related(self, cls, model):
|
def validate_list_select_related(self, cls, model):
|
||||||
" Validate that list_select_related is a boolean. "
|
" Validate that list_select_related is a boolean, a list or a tuple. "
|
||||||
check_type(cls, 'list_select_related', bool)
|
list_select_related = getattr(cls, 'list_select_related', None)
|
||||||
|
if list_select_related:
|
||||||
|
types = (bool, tuple, list)
|
||||||
|
if not isinstance(list_select_related, types):
|
||||||
|
raise ImproperlyConfigured("'%s.list_select_related' should be "
|
||||||
|
"either a bool, a tuple or a list" %
|
||||||
|
cls.__name__)
|
||||||
|
|
||||||
def validate_list_per_page(self, cls, model):
|
def validate_list_per_page(self, cls, model):
|
||||||
" Validate that list_per_page is an integer. "
|
" Validate that list_per_page is an integer. "
|
||||||
|
|
|
@ -356,36 +356,46 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
|
||||||
# ValueError, ValidationError, or ?.
|
# ValueError, ValidationError, or ?.
|
||||||
raise IncorrectLookupParameters(e)
|
raise IncorrectLookupParameters(e)
|
||||||
|
|
||||||
# Use select_related() if one of the list_display options is a field
|
|
||||||
# with a relationship and the provided queryset doesn't already have
|
|
||||||
# select_related defined.
|
|
||||||
if not qs.query.select_related:
|
if not qs.query.select_related:
|
||||||
if self.list_select_related:
|
qs = self.apply_select_related(qs)
|
||||||
qs = qs.select_related()
|
|
||||||
else:
|
|
||||||
for field_name in self.list_display:
|
|
||||||
try:
|
|
||||||
field = self.lookup_opts.get_field(field_name)
|
|
||||||
except models.FieldDoesNotExist:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if isinstance(field.rel, models.ManyToOneRel):
|
|
||||||
qs = qs.select_related()
|
|
||||||
break
|
|
||||||
|
|
||||||
# Set ordering.
|
# Set ordering.
|
||||||
ordering = self.get_ordering(request, qs)
|
ordering = self.get_ordering(request, qs)
|
||||||
qs = qs.order_by(*ordering)
|
qs = qs.order_by(*ordering)
|
||||||
|
|
||||||
# Apply search results
|
# Apply search results
|
||||||
qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query)
|
qs, search_use_distinct = self.model_admin.get_search_results(
|
||||||
|
request, qs, self.query)
|
||||||
|
|
||||||
# Remove duplicates from results, if neccesary
|
# Remove duplicates from results, if necessary
|
||||||
if filters_use_distinct | search_use_distinct:
|
if filters_use_distinct | search_use_distinct:
|
||||||
return qs.distinct()
|
return qs.distinct()
|
||||||
else:
|
else:
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def apply_select_related(self, qs):
|
||||||
|
if self.list_select_related is True:
|
||||||
|
return qs.select_related()
|
||||||
|
|
||||||
|
if self.list_select_related is False:
|
||||||
|
if self.has_related_field_in_list_display():
|
||||||
|
return qs.select_related()
|
||||||
|
|
||||||
|
if self.list_select_related:
|
||||||
|
return qs.select_related(*self.list_select_related)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def has_related_field_in_list_display(self):
|
||||||
|
for field_name in self.list_display:
|
||||||
|
try:
|
||||||
|
field = self.lookup_opts.get_field(field_name)
|
||||||
|
except models.FieldDoesNotExist:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(field.rel, models.ManyToOneRel):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def url_for_result(self, result):
|
def url_for_result(self, result):
|
||||||
pk = getattr(result, self.pk_attname)
|
pk = getattr(result, self.pk_attname)
|
||||||
return reverse('admin:%s_%s_change' % (self.opts.app_label,
|
return reverse('admin:%s_%s_change' % (self.opts.app_label,
|
||||||
|
|
|
@ -812,12 +812,24 @@ subclass::
|
||||||
the list of objects on the admin change list page. This can save you a
|
the list of objects on the admin change list page. This can save you a
|
||||||
bunch of database queries.
|
bunch of database queries.
|
||||||
|
|
||||||
The value should be either ``True`` or ``False``. Default is ``False``.
|
.. versionchanged:: dev
|
||||||
|
|
||||||
Note that Django will use
|
The value should be either a boolean, a list or a tuple. Default is
|
||||||
:meth:`~django.db.models.query.QuerySet.select_related`,
|
``False``.
|
||||||
regardless of this setting if one of the ``list_display`` fields is a
|
|
||||||
``ForeignKey``.
|
When value is ``True``, ``select_related()`` will always be called. When
|
||||||
|
value is set to ``False``, Django will look at ``list_display`` and call
|
||||||
|
``select_related()`` if any ``ForeignKey`` is present.
|
||||||
|
|
||||||
|
If you need more fine-grained control, use a tuple (or list) as value for
|
||||||
|
``list_select_related``. Empty tuple will prevent Django from calling
|
||||||
|
``select_related`` at all. Any other tuple will be passed directly to
|
||||||
|
``select_related`` as parameters. For example::
|
||||||
|
|
||||||
|
class ArticleAdmin(admin.ModelAdmin):
|
||||||
|
list_select_related = ('author', 'category')
|
||||||
|
|
||||||
|
will call ``select_related('author', 'category')``.
|
||||||
|
|
||||||
.. attribute:: ModelAdmin.ordering
|
.. attribute:: ModelAdmin.ordering
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,11 @@ class ChordsBandAdmin(admin.ModelAdmin):
|
||||||
list_filter = ['members']
|
list_filter = ['members']
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('band', 'player')
|
||||||
|
list_select_related = ('player',)
|
||||||
|
|
||||||
|
|
||||||
class DynamicListDisplayChildAdmin(admin.ModelAdmin):
|
class DynamicListDisplayChildAdmin(admin.ModelAdmin):
|
||||||
list_display = ('parent', 'name', 'age')
|
list_display = ('parent', 'name', 'age')
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin,
|
||||||
GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin,
|
GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin,
|
||||||
DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin,
|
DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin,
|
||||||
FilteredChildAdmin, CustomPaginator, site as custom_site,
|
FilteredChildAdmin, CustomPaginator, site as custom_site,
|
||||||
SwallowAdmin, DynamicListFilterChildAdmin)
|
SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin)
|
||||||
from .models import (Event, Child, Parent, Genre, Band, Musician, Group,
|
from .models import (Event, Child, Parent, Genre, Band, Musician, Group,
|
||||||
Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow,
|
Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow,
|
||||||
UnorderedObject, OrderedObject, CustomIdUser)
|
UnorderedObject, OrderedObject, CustomIdUser)
|
||||||
|
@ -46,9 +46,32 @@ class ChangeListTests(TestCase):
|
||||||
m = ChildAdmin(Child, admin.site)
|
m = ChildAdmin(Child, admin.site)
|
||||||
request = self.factory.get('/child/')
|
request = self.factory.get('/child/')
|
||||||
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
||||||
m.list_filter, m.date_hierarchy, m.search_fields,
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
||||||
m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m)
|
m.list_select_related, m.list_per_page,
|
||||||
self.assertEqual(cl.queryset.query.select_related, {'parent': {'name': {}}})
|
m.list_max_show_all, m.list_editable, m)
|
||||||
|
self.assertEqual(cl.queryset.query.select_related, {
|
||||||
|
'parent': {'name': {}}
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_select_related_as_tuple(self):
|
||||||
|
ia = InvitationAdmin(Invitation, admin.site)
|
||||||
|
request = self.factory.get('/invitation/')
|
||||||
|
cl = ChangeList(request, Child, ia.list_display, ia.list_display_links,
|
||||||
|
ia.list_filter, ia.date_hierarchy, ia.search_fields,
|
||||||
|
ia.list_select_related, ia.list_per_page,
|
||||||
|
ia.list_max_show_all, ia.list_editable, ia)
|
||||||
|
self.assertEqual(cl.queryset.query.select_related, {'player': {}})
|
||||||
|
|
||||||
|
def test_select_related_as_empty_tuple(self):
|
||||||
|
ia = InvitationAdmin(Invitation, admin.site)
|
||||||
|
ia.list_select_related = ()
|
||||||
|
request = self.factory.get('/invitation/')
|
||||||
|
cl = ChangeList(request, Child, ia.list_display, ia.list_display_links,
|
||||||
|
ia.list_filter, ia.date_hierarchy, ia.search_fields,
|
||||||
|
ia.list_select_related, ia.list_per_page,
|
||||||
|
ia.list_max_show_all, ia.list_editable, ia)
|
||||||
|
self.assertEqual(cl.queryset.query.select_related, False)
|
||||||
|
|
||||||
|
|
||||||
def test_result_list_empty_changelist_value(self):
|
def test_result_list_empty_changelist_value(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1161,9 +1161,11 @@ class ValidationTests(unittest.TestCase):
|
||||||
class ValidationTestModelAdmin(ModelAdmin):
|
class ValidationTestModelAdmin(ModelAdmin):
|
||||||
list_select_related = 1
|
list_select_related = 1
|
||||||
|
|
||||||
six.assertRaisesRegex(self,
|
six.assertRaisesRegex(
|
||||||
|
self,
|
||||||
ImproperlyConfigured,
|
ImproperlyConfigured,
|
||||||
"'ValidationTestModelAdmin.list_select_related' should be a bool.",
|
"'ValidationTestModelAdmin.list_select_related' should be either a "
|
||||||
|
"bool, a tuple or a list",
|
||||||
ValidationTestModelAdmin.validate,
|
ValidationTestModelAdmin.validate,
|
||||||
ValidationTestModel,
|
ValidationTestModel,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue