diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index ab62d96f26..91b81a42e0 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -650,6 +650,18 @@ class ModelAdmin(BaseModelAdmin): """ return self.list_display + def get_list_display_links(self, request, list_display): + """ + Return a sequence containing the fields to be displayed as links + on the changelist. The list_display parameter is the list of fields + returned by get_list_display(). + """ + if self.list_display_links or not list_display: + return self.list_display_links + else: + # Use only the first item in list_display as link + return list(list_display)[:1] + def construct_change_message(self, request, form, formsets): """ Construct a change message from a changed object. @@ -1087,22 +1099,20 @@ class ModelAdmin(BaseModelAdmin): @csrf_protect_m def changelist_view(self, request, extra_context=None): - "The 'change list' admin view for this model." + """ + The 'change list' admin view for this model. + """ from django.contrib.admin.views.main import ERROR_FLAG opts = self.model._meta app_label = opts.app_label if not self.has_change_permission(request, None): raise PermissionDenied + list_display = self.get_list_display(request) + list_display_links = self.get_list_display_links(request, list_display) + # Check actions to see if any are available on this changelist actions = self.get_actions(request) - - list_display = self.get_list_display(request) - - list_display_links = self.list_display_links - if not self.list_display_links and list_display: - list_display_links = list(list_display)[:1] - if actions: # Add the action checkboxes if there are any actions available. list_display = ['action_checkbox'] + list(list_display) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 1e283151b0..2590243144 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1044,6 +1044,16 @@ templates used by the :class:`ModelAdmin` views: displayed on the changelist view as described above in the :attr:`ModelAdmin.list_display` section. +.. method:: ModelAdmin.get_list_display_links(self, request, list_display) + + .. versionadded:: 1.4 + + The ``get_list_display_links`` method is given the ``HttpRequest`` and + the ``list`` or ``tuple`` returned by :meth:`ModelAdmin.get_list_display`. + It is expected to return a ``list`` or ``tuple`` of field names on the + changelist that will be linked to the change view, as described in the + :attr:`ModelAdmin.list_display_links` section. + .. method:: ModelAdmin.get_urls(self) The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index a11aa41ad5..6fb6f183cb 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -124,13 +124,20 @@ to work similarly to how desktop GUIs do it. The new hook :meth:`~django.contrib.admin.ModelAdmin.get_ordering` for specifying the ordering dynamically (e.g. depending on the request) has also been added. -``ModelAdmin.save_related()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +New ``ModelAdmin`` methods +~~~~~~~~~~~~~~~~~~~~~~~~~~ -A new :meth:`~django.contrib.admin.ModelAdmin.save_related` hook was added to +A new :meth:`~django.contrib.admin.ModelAdmin.save_related` method was added to :mod:`~django.contrib.admin.ModelAdmin` to ease the customization of how related objects are saved in the admin. +Two other new methods, +:meth:`~django.contrib.admin.ModelAdmin.get_list_display` and +:meth:`~django.contrib.admin.ModelAdmin.get_list_display_links` +were added to :class:`~django.contrib.admin.ModelAdmin` to enable the dynamic +customization of fields and links to display on the admin +change list. + Admin inlines respect user permissions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/admin_changelist/admin.py b/tests/regressiontests/admin_changelist/admin.py index 19c2b24028..d1d8e0fd1e 100644 --- a/tests/regressiontests/admin_changelist/admin.py +++ b/tests/regressiontests/admin_changelist/admin.py @@ -58,13 +58,20 @@ class ChordsBandAdmin(admin.ModelAdmin): class DynamicListDisplayChildAdmin(admin.ModelAdmin): - list_display = ('name', 'parent') + list_display = ('parent', 'name', 'age') def get_list_display(self, request): - my_list_display = list(self.list_display) + my_list_display = super(DynamicListDisplayChildAdmin, self).get_list_display(request) if request.user.username == 'noparents': + my_list_display = list(my_list_display) my_list_display.remove('parent') - return my_list_display +class DynamicListDisplayLinksChildAdmin(admin.ModelAdmin): + list_display = ('parent', 'name', 'age') + list_display_links = ['parent', 'name'] + + def get_list_display_links(self, request, list_display): + return ['age'] + site.register(Child, DynamicListDisplayChildAdmin) diff --git a/tests/regressiontests/admin_changelist/models.py b/tests/regressiontests/admin_changelist/models.py index 363b5dbed5..eed3f68508 100644 --- a/tests/regressiontests/admin_changelist/models.py +++ b/tests/regressiontests/admin_changelist/models.py @@ -7,6 +7,7 @@ class Parent(models.Model): class Child(models.Model): parent = models.ForeignKey(Parent, editable=False, null=True) name = models.CharField(max_length=30, blank=True) + age = models.IntegerField(null=True, blank=True) class Genre(models.Model): name = models.CharField(max_length=20) diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py index a081c980df..9292895552 100644 --- a/tests/regressiontests/admin_changelist/tests.py +++ b/tests/regressiontests/admin_changelist/tests.py @@ -9,7 +9,8 @@ from django.test import TestCase from django.test.client import RequestFactory from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, - GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, CustomPaginationAdmin, + GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, + DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, FilteredChildAdmin, CustomPaginator, site as custom_site) from .models import (Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation) @@ -41,7 +42,9 @@ class ChangeListTests(TestCase): new_child = Child.objects.create(name='name', parent=None) request = self.factory.get('/child/') m = ChildAdmin(Child, admin.site) - cl = ChangeList(request, Child, m.list_display, m.list_display_links, + list_display = m.get_list_display(request) + list_display_links = m.get_list_display_links(request, list_display) + cl = ChangeList(request, Child, list_display, list_display_links, 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) cl.formset = None @@ -61,7 +64,9 @@ class ChangeListTests(TestCase): new_child = Child.objects.create(name='name', parent=new_parent) request = self.factory.get('/child/') m = ChildAdmin(Child, admin.site) - cl = ChangeList(request, Child, m.list_display, m.list_display_links, + list_display = m.get_list_display(request) + list_display_links = m.get_list_display_links(request, list_display) + cl = ChangeList(request, Child, list_display, list_display_links, 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) cl.formset = None @@ -334,28 +339,31 @@ class ChangeListTests(TestCase): m = custom_site._registry[Child] 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') + list_display = m.get_list_display(request) + list_display_links = m.get_list_display_links(request, list_display) + self.assertEqual(list_display, ['name', 'age']) + self.assertEqual(list_display_links, ['name']) + # 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') custom_site.unregister(Child) + list_display = m.get_list_display(request) + list_display_links = m.get_list_display_links(request, list_display) + self.assertEqual(list_display, ('parent', 'name', 'age')) + self.assertEqual(list_display_links, ['parent']) + # Test default implementation custom_site.register(Child, ChildAdmin) m = custom_site._registry[Child] request = _mocked_authenticated_request(user_noparents) response = m.changelist_view(request) - # XXX - #15826 - response.render() self.assertContains(response, 'Parent object') def test_show_all(self): @@ -386,3 +394,30 @@ class ChangeListTests(TestCase): cl.get_results(request) self.assertEqual(len(cl.result_list), 10) + def test_dynamic_list_display_links(self): + """ + Regression tests for #16257: dynamic list_display_links support. + """ + parent = Parent.objects.create(name='parent') + for i in range(1, 10): + Child.objects.create(id=i, name='child %s' % i, parent=parent, age=i) + + superuser = User.objects.create( + username='superuser', + is_superuser=True) + + def _mocked_authenticated_request(user): + request = self.factory.get('/child/') + request.user = user + return request + + m = DynamicListDisplayLinksChildAdmin(Child, admin.site) + request = _mocked_authenticated_request(superuser) + response = m.changelist_view(request) + for i in range(1, 10): + self.assertContains(response, '%s' % (i, i)) + + list_display = m.get_list_display(request) + list_display_links = m.get_list_display_links(request, list_display) + self.assertEqual(list_display, ('parent', 'name', 'age')) + self.assertEqual(list_display_links, ['age']) \ No newline at end of file