Fixed #16257 -- Added new `ModelAdmin.get_list_display_links()` method to allow for the dynamic display of links on the admin changelist. Thanks to graveyboat for the suggestion and initial patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17037 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Julien Phalip 2011-10-26 12:19:18 +00:00
parent a05c70fae1
commit 9796f69533
6 changed files with 94 additions and 24 deletions

View File

@ -650,6 +650,18 @@ class ModelAdmin(BaseModelAdmin):
""" """
return self.list_display 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): def construct_change_message(self, request, form, formsets):
""" """
Construct a change message from a changed object. Construct a change message from a changed object.
@ -1087,22 +1099,20 @@ class ModelAdmin(BaseModelAdmin):
@csrf_protect_m @csrf_protect_m
def changelist_view(self, request, extra_context=None): 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 from django.contrib.admin.views.main import ERROR_FLAG
opts = self.model._meta opts = self.model._meta
app_label = opts.app_label app_label = opts.app_label
if not self.has_change_permission(request, None): if not self.has_change_permission(request, None):
raise PermissionDenied 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 # Check actions to see if any are available on this changelist
actions = self.get_actions(request) 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: if actions:
# Add the action checkboxes if there are any actions available. # Add the action checkboxes if there are any actions available.
list_display = ['action_checkbox'] + list(list_display) list_display = ['action_checkbox'] + list(list_display)

View File

@ -1044,6 +1044,16 @@ templates used by the :class:`ModelAdmin` views:
displayed on the changelist view as described above in the displayed on the changelist view as described above in the
:attr:`ModelAdmin.list_display` section. :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) .. method:: ModelAdmin.get_urls(self)
The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for

View File

@ -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 :meth:`~django.contrib.admin.ModelAdmin.get_ordering` for specifying the
ordering dynamically (e.g. depending on the request) has also been added. 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 :mod:`~django.contrib.admin.ModelAdmin` to ease the customization of how
related objects are saved in the admin. 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 Admin inlines respect user permissions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -58,13 +58,20 @@ class ChordsBandAdmin(admin.ModelAdmin):
class DynamicListDisplayChildAdmin(admin.ModelAdmin): class DynamicListDisplayChildAdmin(admin.ModelAdmin):
list_display = ('name', 'parent') list_display = ('parent', 'name', 'age')
def get_list_display(self, request): 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': if request.user.username == 'noparents':
my_list_display = list(my_list_display)
my_list_display.remove('parent') my_list_display.remove('parent')
return my_list_display 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) site.register(Child, DynamicListDisplayChildAdmin)

View File

@ -7,6 +7,7 @@ class Parent(models.Model):
class Child(models.Model): class Child(models.Model):
parent = models.ForeignKey(Parent, editable=False, null=True) parent = models.ForeignKey(Parent, editable=False, null=True)
name = models.CharField(max_length=30, blank=True) name = models.CharField(max_length=30, blank=True)
age = models.IntegerField(null=True, blank=True)
class Genre(models.Model): class Genre(models.Model):
name = models.CharField(max_length=20) name = models.CharField(max_length=20)

View File

@ -9,7 +9,8 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin,
GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, CustomPaginationAdmin, GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin,
DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin,
FilteredChildAdmin, CustomPaginator, site as custom_site) FilteredChildAdmin, CustomPaginator, site as custom_site)
from .models import (Child, Parent, Genre, Band, Musician, Group, Quartet, from .models import (Child, Parent, Genre, Band, Musician, Group, Quartet,
Membership, ChordsMusician, ChordsBand, Invitation) Membership, ChordsMusician, ChordsBand, Invitation)
@ -41,7 +42,9 @@ class ChangeListTests(TestCase):
new_child = Child.objects.create(name='name', parent=None) new_child = Child.objects.create(name='name', parent=None)
request = self.factory.get('/child/') request = self.factory.get('/child/')
m = ChildAdmin(Child, admin.site) 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_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, m.list_max_show_all, m.list_editable, m)
cl.formset = None cl.formset = None
@ -61,7 +64,9 @@ class ChangeListTests(TestCase):
new_child = Child.objects.create(name='name', parent=new_parent) new_child = Child.objects.create(name='name', parent=new_parent)
request = self.factory.get('/child/') request = self.factory.get('/child/')
m = ChildAdmin(Child, admin.site) 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_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, m.list_max_show_all, m.list_editable, m)
cl.formset = None cl.formset = None
@ -334,28 +339,31 @@ class ChangeListTests(TestCase):
m = custom_site._registry[Child] m = custom_site._registry[Child]
request = _mocked_authenticated_request(user_noparents) request = _mocked_authenticated_request(user_noparents)
response = m.changelist_view(request) 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') 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' # Test with user 'parents'
m = DynamicListDisplayChildAdmin(Child, admin.site) m = DynamicListDisplayChildAdmin(Child, admin.site)
request = _mocked_authenticated_request(user_parents) request = _mocked_authenticated_request(user_parents)
response = m.changelist_view(request) response = m.changelist_view(request)
# XXX - #15826
response.render()
self.assertContains(response, 'Parent object') self.assertContains(response, 'Parent object')
custom_site.unregister(Child) 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 # Test default implementation
custom_site.register(Child, ChildAdmin) custom_site.register(Child, ChildAdmin)
m = custom_site._registry[Child] m = custom_site._registry[Child]
request = _mocked_authenticated_request(user_noparents) request = _mocked_authenticated_request(user_noparents)
response = m.changelist_view(request) response = m.changelist_view(request)
# XXX - #15826
response.render()
self.assertContains(response, 'Parent object') self.assertContains(response, 'Parent object')
def test_show_all(self): def test_show_all(self):
@ -386,3 +394,30 @@ class ChangeListTests(TestCase):
cl.get_results(request) cl.get_results(request)
self.assertEqual(len(cl.result_list), 10) 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, '<a href="%s/">%s</a>' % (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'])