diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index c603210e8be6d..e9064bb68cc4f 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -625,6 +625,13 @@ def get_action(self, action): description = capfirst(action.replace('_', ' ')) return func, action, description + def get_list_display(self, request): + """ + Return a sequence containing the fields to be displayed on the + changelist. + """ + return self.list_display + def construct_change_message(self, request, form, formsets): """ Construct a change message from a changed object. @@ -1053,7 +1060,7 @@ def changelist_view(self, request, extra_context=None): actions = self.get_actions(request) # Remove action checkboxes if there aren't any actions available. - list_display = list(self.list_display) + list_display = list(self.get_list_display(request)) if not actions: try: list_display.remove('action_checkbox') diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 3ba3b1b0eb0ce..e0ddd4ae854d9 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -967,6 +967,15 @@ templates used by the :class:`ModelAdmin` views: a ``dictionary``, as described above in the :attr:`ModelAdmin.prepopulated_fields` section. +.. method:: ModelAdmin.get_list_display(self, request) + + .. versionadded:: 1.4 + + The ``get_list_display`` method is given the ``HttpRequest`` and is + expected to return a ``list`` or ``tuple`` of field names that will be + displayed on the changelist view as described above in the + :attr:`ModelAdmin.list_display` section. + .. method:: ModelAdmin.get_urls(self) The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py index 5186508dd9c7a..bc7fce2025a1e 100644 --- a/tests/regressiontests/admin_changelist/tests.py +++ b/tests/regressiontests/admin_changelist/tests.py @@ -4,19 +4,25 @@ from django.core.paginator import Paginator from django.template import Context, Template from django.test import TransactionTestCase +from django.test.client import RequestFactory +from django.contrib.auth.models import User from models import (Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation) class ChangeListTests(TransactionTestCase): + def setUp(self): + self.factory = RequestFactory() + def test_select_related_preserved(self): """ Regression test for #10348: ChangeList.get_query_set() shouldn't overwrite a custom select_related provided by ModelAdmin.queryset(). """ m = ChildAdmin(Child, admin.site) - cl = ChangeList(MockRequest(), Child, m.list_display, m.list_display_links, + request = self.factory.get('/child/') + cl = ChangeList(request, Child, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, m.search_fields, m.list_select_related, m.list_per_page, m.list_editable, m) self.assertEqual(cl.query_set.query.select_related, {'parent': {'name': {}}}) @@ -27,7 +33,7 @@ def test_result_list_empty_changelist_value(self): for relationship fields """ new_child = Child.objects.create(name='name', parent=None) - request = MockRequest() + request = self.factory.get('/child/') m = ChildAdmin(Child, admin.site) cl = ChangeList(request, Child, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, m.search_fields, @@ -40,7 +46,6 @@ def test_result_list_empty_changelist_value(self): self.assertFalse(table_output.find(row_html) == -1, 'Failed to find expected row element: %s' % table_output) - def test_result_list_html(self): """ Verifies that inclusion tag result_list generates a table when with @@ -48,7 +53,7 @@ def test_result_list_html(self): """ new_parent = Parent.objects.create(name='parent') new_child = Child.objects.create(name='name', parent=new_parent) - request = MockRequest() + request = self.factory.get('/child/') m = ChildAdmin(Child, admin.site) cl = ChangeList(request, Child, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, m.search_fields, @@ -72,7 +77,7 @@ def test_result_list_editable_html(self): """ new_parent = Parent.objects.create(name='parent') new_child = Child.objects.create(name='name', parent=new_parent) - request = MockRequest() + request = self.factory.get('/child/') m = ChildAdmin(Child, admin.site) # Test with list_editable fields @@ -104,8 +109,7 @@ def test_result_list_editable(self): new_parent = Parent.objects.create(name='parent') for i in range(200): new_child = Child.objects.create(name='name %s' % i, parent=new_parent) - request = MockRequest() - request.GET['p'] = -1 # Anything outside range + request = self.factory.get('/child/', data={'p': -1}) # Anything outside range m = ChildAdmin(Child, admin.site) # Test with list_editable fields @@ -122,7 +126,7 @@ def test_custom_paginator(self): for i in range(200): new_child = Child.objects.create(name='name %s' % i, parent=new_parent) - request = MockRequest() + request = self.factory.get('/child/') m = ChildAdmin(Child, admin.site) m.list_display = ['id', 'name', 'parent'] m.list_display_links = ['id'] @@ -148,7 +152,7 @@ def test_distinct_for_m2m_in_list_filter(self): band.genres.add(blues) m = BandAdmin(Band, admin.site) - request = MockFilterRequest('genres', blues.pk) + request = self.factory.get('/band/', data={'genres': blues.pk}) cl = ChangeList(request, Band, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, @@ -171,7 +175,7 @@ def test_distinct_for_through_m2m_in_list_filter(self): Membership.objects.create(group=band, music=lead, role='bass player') m = GroupAdmin(Group, admin.site) - request = MockFilterRequest('members', lead.pk) + request = self.factory.get('/group/', data={'members': lead.pk}) cl = ChangeList(request, Group, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, @@ -195,7 +199,7 @@ def test_distinct_for_inherited_m2m_in_list_filter(self): Membership.objects.create(group=four, music=lead, role='guitar player') m = QuartetAdmin(Quartet, admin.site) - request = MockFilterRequest('members', lead.pk) + request = self.factory.get('/quartet/', data={'members': lead.pk}) cl = ChangeList(request, Quartet, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, @@ -219,7 +223,7 @@ def test_distinct_for_m2m_to_inherited_in_list_filter(self): Invitation.objects.create(band=three, player=lead, instrument='bass') m = ChordsBandAdmin(ChordsBand, admin.site) - request = MockFilterRequest('members', lead.pk) + request = self.factory.get('/chordsband/', data={'members': lead.pk}) cl = ChangeList(request, ChordsBand, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, @@ -242,7 +246,7 @@ def test_distinct_for_non_unique_related_object_in_list_filter(self): Child.objects.create(parent=parent, name='Daniel') m = ParentAdmin(Parent, admin.site) - request = MockFilterRequest('child__name', 'Daniel') + request = self.factory.get('/parent/', data={'child__name': 'Daniel'}) cl = ChangeList(request, Parent, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, m.search_fields, @@ -262,7 +266,7 @@ def test_distinct_for_non_unique_related_object_in_search_fields(self): Child.objects.create(parent=parent, name='Daniel') m = ParentAdmin(Parent, admin.site) - request = MockSearchRequest('daniel') + request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel'}) cl = ChangeList(request, Parent, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, m.search_fields, @@ -282,7 +286,7 @@ def test_pagination(self): Child.objects.create(name='name %s' % i, parent=parent) Child.objects.create(name='filtered %s' % i, parent=parent) - request = MockRequest() + request = self.factory.get('/child/') # Test default queryset m = ChildAdmin(Child, admin.site) @@ -302,6 +306,51 @@ def test_pagination(self): self.assertEqual(cl.paginator.count, 30) self.assertEqual(cl.paginator.page_range, [1, 2, 3]) + def test_dynamic_list_display(self): + """ + Regression tests for #14206: dynamic list_display support. + """ + parent = Parent.objects.create(name='parent') + for i in range(10): + Child.objects.create(name='child %s' % i, parent=parent) + + user_noparents = User.objects.create( + username='noparents', + is_superuser=True) + user_parents = User.objects.create( + username='parents', + is_superuser=True) + + def _mocked_authenticated_request(user): + request = self.factory.get('/child/') + request.user = user + return request + + # Test with user 'noparents' + m = DynamicListDisplayChildAdmin(Child, admin.site) + request = _mocked_authenticated_request(user_noparents) + response = m.changelist_view(request) + # XXX - Calling render here to avoid ContentNotRenderedError to be + # raised. Ticket #15826 should fix this but it's not yet integrated. + response.render() + self.assertNotContains(response, 'Parent object') + + # Test with user 'parents' + m = DynamicListDisplayChildAdmin(Child, admin.site) + request = _mocked_authenticated_request(user_parents) + response = m.changelist_view(request) + # XXX - #15826 + response.render() + self.assertContains(response, 'Parent object') + + # Test default implementation + m = ChildAdmin(Child, admin.site) + request = _mocked_authenticated_request(user_noparents) + response = m.changelist_view(request) + # XXX - #15826 + response.render() + self.assertContains(response, 'Parent object') + class ParentAdmin(admin.ModelAdmin): list_filter = ['child__name'] @@ -311,18 +360,19 @@ class ParentAdmin(admin.ModelAdmin): class ChildAdmin(admin.ModelAdmin): list_display = ['name', 'parent'] list_per_page = 10 + def queryset(self, request): return super(ChildAdmin, self).queryset(request).select_related("parent__name") + class FilteredChildAdmin(admin.ModelAdmin): list_display = ['name', 'parent'] list_per_page = 10 + def queryset(self, request): return super(FilteredChildAdmin, self).queryset(request).filter( name__contains='filtered') -class MockRequest(object): - GET = {} class CustomPaginator(Paginator): def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True): @@ -333,19 +383,25 @@ def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True): class BandAdmin(admin.ModelAdmin): list_filter = ['genres'] + class GroupAdmin(admin.ModelAdmin): list_filter = ['members'] + class QuartetAdmin(admin.ModelAdmin): list_filter = ['members'] + class ChordsBandAdmin(admin.ModelAdmin): list_filter = ['members'] -class MockFilterRequest(object): - def __init__(self, filter, q): - self.GET = {filter: q} -class MockSearchRequest(object): - def __init__(self, q): - self.GET = {SEARCH_VAR: q} +class DynamicListDisplayChildAdmin(admin.ModelAdmin): + list_display = ('name', 'parent') + + def get_list_display(self, request): + my_list_display = list(self.list_display) + if request.user.username == 'noparents': + my_list_display.remove('parent') + + return my_list_display