Fixed #15185 -- Allowed ModelAdmin.list_display_links=None to disable change list links.

Thanks rm_ for the suggestion.
This commit is contained in:
Ramiro Morales 2013-09-06 15:25:13 -03:00 committed by Tim Graham
parent bf757a2f4d
commit 1d0fc61b1c
8 changed files with 83 additions and 26 deletions

View File

@ -794,7 +794,7 @@ class ModelAdmin(BaseModelAdmin):
on the changelist. The list_display parameter is the list of fields on the changelist. The list_display parameter is the list of fields
returned by get_list_display(). returned by get_list_display().
""" """
if self.list_display_links or not list_display: if self.list_display_links or self.list_display_links is None or not list_display:
return self.list_display_links return self.list_display_links
else: else:
# Use only the first item in list_display as link # Use only the first item in list_display as link

View File

@ -178,6 +178,14 @@ def items_for_result(cl, result, form):
""" """
Generates the actual list of data. Generates the actual list of data.
""" """
def link_in_col(is_first, field_name, cl):
if cl.list_display_links is None:
return False
if is_first and not cl.list_display_links:
return True
return field_name in cl.list_display_links
first = True first = True
pk = cl.lookup_opts.pk.attname pk = cl.lookup_opts.pk.attname
for field_name in cl.list_display: for field_name in cl.list_display:
@ -216,7 +224,7 @@ def items_for_result(cl, result, form):
result_repr = mark_safe(' ') result_repr = mark_safe(' ')
row_class = mark_safe(' class="%s"' % ' '.join(row_classes)) row_class = mark_safe(' class="%s"' % ' '.join(row_classes))
# If list_display_links not defined, add the link tag to the first field # If list_display_links not defined, add the link tag to the first field
if (first and not cl.list_display_links) or field_name in cl.list_display_links: if link_in_col(first, field_name, cl):
table_tag = 'th' if first else 'td' table_tag = 'th' if first else 'td'
first = False first = False

View File

@ -257,8 +257,10 @@ class ModelAdminValidator(BaseValidator):
% (cls.__name__, idx, field)) % (cls.__name__, idx, field))
def validate_list_display_links(self, cls, model): def validate_list_display_links(self, cls, model):
" Validate that list_display_links is a unique subset of list_display. " " Validate that list_display_links either is None or a unique subset of list_display."
if hasattr(cls, 'list_display_links'): if hasattr(cls, 'list_display_links'):
if cls.list_display_links is None:
return
check_isseq(cls, 'list_display_links', cls.list_display_links) check_isseq(cls, 'list_display_links', cls.list_display_links)
for idx, field in enumerate(cls.list_display_links): for idx, field in enumerate(cls.list_display_links):
if field not in cls.list_display: if field not in cls.list_display:
@ -344,15 +346,16 @@ class ModelAdminValidator(BaseValidator):
raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " raise ImproperlyConfigured("'%s.list_editable[%d]' refers to "
"'%s' which is not defined in 'list_display'." "'%s' which is not defined in 'list_display'."
% (cls.__name__, idx, field_name)) % (cls.__name__, idx, field_name))
if field_name in cls.list_display_links: if cls.list_display_links is not None:
raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" if field_name in cls.list_display_links:
" and '%s.list_display_links'" raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'"
% (field_name, cls.__name__, cls.__name__)) " and '%s.list_display_links'"
if not cls.list_display_links and cls.list_display[0] in cls.list_editable: % (field_name, cls.__name__, cls.__name__))
raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" if not cls.list_display_links and cls.list_display[0] in cls.list_editable:
" the first field in list_display, '%s', which can't be" raise ImproperlyConfigured("'%s.list_editable[%d]' refers to"
" used unless list_display_links is set." " the first field in list_display, '%s', which can't be"
% (cls.__name__, idx, cls.list_display[0])) " used unless list_display_links is set."
% (cls.__name__, idx, cls.list_display[0]))
if not field.editable: if not field.editable:
raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a "
"field, '%s', which isn't editable through the admin." "field, '%s', which isn't editable through the admin."

View File

@ -648,19 +648,21 @@ subclass::
.. attribute:: ModelAdmin.list_display_links .. attribute:: ModelAdmin.list_display_links
Set ``list_display_links`` to control which fields in ``list_display`` Use ``list_display_links`` to control if and which fields in
should be linked to the "change" page for an object. :attr:`list_display` should be linked to the "change" page for an object.
By default, the change list page will link the first column -- the first By default, the change list page will link the first column -- the first
field specified in ``list_display`` -- to the change page for each item. field specified in ``list_display`` -- to the change page for each item.
But ``list_display_links`` lets you change which columns are linked. Set But ``list_display_links`` lets you change this:
``list_display_links`` to a list or tuple of fields (in the same
format as ``list_display``) to link.
``list_display_links`` can specify one or many fields. As long as the * Set it to ``None`` to get no links at all.
fields appear in ``list_display``, Django doesn't care how many (or * Set it to a list or tuple of fields (in the same format as
how few) fields are linked. The only requirement is: If you want to use ``list_display``) whose columns you want converted to links.
``list_display_links``, you must define ``list_display``.
You can specify one or many fields. As long as the fields appear in
``list_display``, Django doesn't care how many (or how few) fields are
linked. The only requirement is that if you want to use
``list_display_links`` in this fashion, you must define ``list_display``.
In this example, the ``first_name`` and ``last_name`` fields will be In this example, the ``first_name`` and ``last_name`` fields will be
linked on the change list page:: linked on the change list page::
@ -669,7 +671,17 @@ subclass::
list_display = ('first_name', 'last_name', 'birthday') list_display = ('first_name', 'last_name', 'birthday')
list_display_links = ('first_name', 'last_name') list_display_links = ('first_name', 'last_name')
.. _admin-list-editable: In this example, the change list page grid will have no links::
class AuditEntryAdmin(admin.ModelAdmin):
list_display = ('timestamp', 'message')
list_display_links = None
.. versionchanged:: 1.7
``None`` was added as a valid ``list_display_links`` value.
.. _admin-list-editable:
.. attribute:: ModelAdmin.list_editable .. attribute:: ModelAdmin.list_editable
@ -1242,9 +1254,13 @@ templates used by the :class:`ModelAdmin` views:
The ``get_list_display_links`` method is given the ``HttpRequest`` and The ``get_list_display_links`` method is given the ``HttpRequest`` and
the ``list`` or ``tuple`` returned by :meth:`ModelAdmin.get_list_display`. 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 It is expected to return either ``None`` or a ``list`` or ``tuple`` of field
changelist that will be linked to the change view, as described in the names on the changelist that will be linked to the change view, as described
:attr:`ModelAdmin.list_display_links` section. in the :attr:`ModelAdmin.list_display_links` section.
.. versionchanged:: 1.7
``None`` was added as a valid ``get_list_display_links()`` return value.
.. method:: ModelAdmin.get_fields(self, request, obj=None) .. method:: ModelAdmin.get_fields(self, request, obj=None)

View File

@ -164,6 +164,10 @@ Minor features
new :func:`~django.contrib.admin.register` decorator to register a new :func:`~django.contrib.admin.register` decorator to register a
:class:`~django.contrib.admin.ModelAdmin`. :class:`~django.contrib.admin.ModelAdmin`.
* You may specify :meth:`ModelAdmin.list_display_links
<django.contrib.admin.ModelAdmin.list_display_links>` ``= None`` to disable
links on the change list page grid.
:mod:`django.contrib.auth` :mod:`django.contrib.auth`
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -7,6 +7,7 @@ from .models import (Event, Child, Parent, Genre, Band, Musician, Group,
site = admin.AdminSite(name="admin") site = admin.AdminSite(name="admin")
class CustomPaginator(Paginator): class CustomPaginator(Paginator):
def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True): def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True):
super(CustomPaginator, self).__init__(queryset, 5, orphans=2, super(CustomPaginator, self).__init__(queryset, 5, orphans=2,
@ -80,6 +81,7 @@ class DynamicListDisplayChildAdmin(admin.ModelAdmin):
my_list_display.remove('parent') my_list_display.remove('parent')
return my_list_display return my_list_display
class DynamicListDisplayLinksChildAdmin(admin.ModelAdmin): class DynamicListDisplayLinksChildAdmin(admin.ModelAdmin):
list_display = ('parent', 'name', 'age') list_display = ('parent', 'name', 'age')
list_display_links = ['parent', 'name'] list_display_links = ['parent', 'name']
@ -89,12 +91,20 @@ class DynamicListDisplayLinksChildAdmin(admin.ModelAdmin):
site.register(Child, DynamicListDisplayChildAdmin) site.register(Child, DynamicListDisplayChildAdmin)
class NoListDisplayLinksParentAdmin(admin.ModelAdmin):
list_display_links = None
site.register(Parent, NoListDisplayLinksParentAdmin)
class SwallowAdmin(admin.ModelAdmin): class SwallowAdmin(admin.ModelAdmin):
actions = None # prevent ['action_checkbox'] + list(list_display) actions = None # prevent ['action_checkbox'] + list(list_display)
list_display = ('origin', 'load', 'speed') list_display = ('origin', 'load', 'speed')
site.register(Swallow, SwallowAdmin) site.register(Swallow, SwallowAdmin)
class DynamicListFilterChildAdmin(admin.ModelAdmin): class DynamicListFilterChildAdmin(admin.ModelAdmin):
list_filter = ('parent', 'name', 'age') list_filter = ('parent', 'name', 'age')
@ -105,6 +115,7 @@ class DynamicListFilterChildAdmin(admin.ModelAdmin):
my_list_filter.remove('parent') my_list_filter.remove('parent')
return my_list_filter return my_list_filter
class DynamicSearchFieldsChildAdmin(admin.ModelAdmin): class DynamicSearchFieldsChildAdmin(admin.ModelAdmin):
search_fields = ('name',) search_fields = ('name',)

View File

@ -19,7 +19,7 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin,
DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin,
FilteredChildAdmin, CustomPaginator, site as custom_site, FilteredChildAdmin, CustomPaginator, site as custom_site,
SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin, SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin,
DynamicSearchFieldsChildAdmin) DynamicSearchFieldsChildAdmin, NoListDisplayLinksParentAdmin)
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)
@ -460,6 +460,16 @@ class ChangeListTests(TestCase):
self.assertEqual(list_display, ('parent', 'name', 'age')) self.assertEqual(list_display, ('parent', 'name', 'age'))
self.assertEqual(list_display_links, ['age']) self.assertEqual(list_display_links, ['age'])
def test_no_list_display_links(self):
"""#15185 -- Allow no links from the 'change list' view grid."""
p = Parent.objects.create(name='parent')
m = NoListDisplayLinksParentAdmin(Parent, admin.site)
superuser = self._create_superuser('superuser')
request = self._mocked_authenticated_request('/parent/', superuser)
response = m.changelist_view(request)
link = reverse('admin:admin_changelist_parent_change', args=(p.pk,))
self.assertNotContains(response, '<a href="%s">' % link)
def test_tuple_list_display(self): def test_tuple_list_display(self):
""" """
Regression test for #17128 Regression test for #17128

View File

@ -967,6 +967,11 @@ class ValidationTests(unittest.TestCase):
ValidationTestModelAdmin.validate(ValidationTestModel) ValidationTestModelAdmin.validate(ValidationTestModel)
class ValidationTestModelAdmin(ModelAdmin):
list_display_links = None
ValidationTestModelAdmin.validate(ValidationTestModel)
def test_list_filter_validation(self): def test_list_filter_validation(self):
class ValidationTestModelAdmin(ModelAdmin): class ValidationTestModelAdmin(ModelAdmin):