diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index c5096076a9..521b2371a6 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -794,7 +794,7 @@ class ModelAdmin(BaseModelAdmin): 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: + if self.list_display_links or self.list_display_links is None or not list_display: return self.list_display_links else: # Use only the first item in list_display as link diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 2b1fd8ad34..f1c11831ab 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -178,6 +178,14 @@ def items_for_result(cl, result, form): """ 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 pk = cl.lookup_opts.pk.attname for field_name in cl.list_display: @@ -216,7 +224,7 @@ def items_for_result(cl, result, form): result_repr = mark_safe(' ') row_class = mark_safe(' class="%s"' % ' '.join(row_classes)) # 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' first = False diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 495f72f727..e4a12211b5 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -257,8 +257,10 @@ class ModelAdminValidator(BaseValidator): % (cls.__name__, idx, field)) 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 cls.list_display_links is None: + return check_isseq(cls, 'list_display_links', cls.list_display_links) for idx, field in enumerate(cls.list_display_links): if field not in cls.list_display: @@ -344,15 +346,16 @@ class ModelAdminValidator(BaseValidator): raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " "'%s' which is not defined in 'list_display'." % (cls.__name__, idx, field_name)) - if field_name in cls.list_display_links: - raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" - " and '%s.list_display_links'" - % (field_name, cls.__name__, cls.__name__)) - if not cls.list_display_links and cls.list_display[0] in cls.list_editable: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" - " the first field in list_display, '%s', which can't be" - " used unless list_display_links is set." - % (cls.__name__, idx, cls.list_display[0])) + if cls.list_display_links is not None: + if field_name in cls.list_display_links: + raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" + " and '%s.list_display_links'" + % (field_name, cls.__name__, cls.__name__)) + if not cls.list_display_links and cls.list_display[0] in cls.list_editable: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" + " the first field in list_display, '%s', which can't be" + " used unless list_display_links is set." + % (cls.__name__, idx, cls.list_display[0])) if not field.editable: raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " "field, '%s', which isn't editable through the admin." diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 2e02785e81..8873375174 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -648,19 +648,21 @@ subclass:: .. attribute:: ModelAdmin.list_display_links - Set ``list_display_links`` to control which fields in ``list_display`` - should be linked to the "change" page for an object. + Use ``list_display_links`` to control if and which fields in + :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 field specified in ``list_display`` -- to the change page for each item. - But ``list_display_links`` lets you change which columns are linked. Set - ``list_display_links`` to a list or tuple of fields (in the same - format as ``list_display``) to link. + But ``list_display_links`` lets you change this: - ``list_display_links`` 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: If you want to use - ``list_display_links``, you must define ``list_display``. + * Set it to ``None`` to get no links at all. + * Set it to a list or tuple of fields (in the same format as + ``list_display``) whose columns you want converted to links. + + 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 linked on the change list page:: @@ -669,7 +671,17 @@ subclass:: list_display = ('first_name', 'last_name', 'birthday') 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 @@ -1242,9 +1254,13 @@ templates used by the :class:`ModelAdmin` views: 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. + It is expected to return either ``None`` or 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. + + .. versionchanged:: 1.7 + + ``None`` was added as a valid ``get_list_display_links()`` return value. .. method:: ModelAdmin.get_fields(self, request, obj=None) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index a978bb8ac1..d613220508 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -164,6 +164,10 @@ Minor features new :func:`~django.contrib.admin.register` decorator to register a :class:`~django.contrib.admin.ModelAdmin`. +* You may specify :meth:`ModelAdmin.list_display_links + ` ``= None`` to disable + links on the change list page grid. + :mod:`django.contrib.auth` ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index a63f2b60ce..b42f025b79 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -7,6 +7,7 @@ from .models import (Event, Child, Parent, Genre, Band, Musician, Group, site = admin.AdminSite(name="admin") + class CustomPaginator(Paginator): def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True): super(CustomPaginator, self).__init__(queryset, 5, orphans=2, @@ -80,6 +81,7 @@ class DynamicListDisplayChildAdmin(admin.ModelAdmin): my_list_display.remove('parent') return my_list_display + class DynamicListDisplayLinksChildAdmin(admin.ModelAdmin): list_display = ('parent', 'name', 'age') list_display_links = ['parent', 'name'] @@ -89,12 +91,20 @@ class DynamicListDisplayLinksChildAdmin(admin.ModelAdmin): site.register(Child, DynamicListDisplayChildAdmin) + +class NoListDisplayLinksParentAdmin(admin.ModelAdmin): + list_display_links = None + +site.register(Parent, NoListDisplayLinksParentAdmin) + + class SwallowAdmin(admin.ModelAdmin): actions = None # prevent ['action_checkbox'] + list(list_display) list_display = ('origin', 'load', 'speed') site.register(Swallow, SwallowAdmin) + class DynamicListFilterChildAdmin(admin.ModelAdmin): list_filter = ('parent', 'name', 'age') @@ -105,6 +115,7 @@ class DynamicListFilterChildAdmin(admin.ModelAdmin): my_list_filter.remove('parent') return my_list_filter + class DynamicSearchFieldsChildAdmin(admin.ModelAdmin): search_fields = ('name',) diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 2477edb55a..f9ef079904 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -19,7 +19,7 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, FilteredChildAdmin, CustomPaginator, site as custom_site, SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin, - DynamicSearchFieldsChildAdmin) + DynamicSearchFieldsChildAdmin, NoListDisplayLinksParentAdmin) from .models import (Event, Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow, UnorderedObject, OrderedObject, CustomIdUser) @@ -460,6 +460,16 @@ class ChangeListTests(TestCase): self.assertEqual(list_display, ('parent', 'name', '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, '' % link) + def test_tuple_list_display(self): """ Regression test for #17128 diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 53c977a890..930563f5ef 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -967,6 +967,11 @@ class ValidationTests(unittest.TestCase): ValidationTestModelAdmin.validate(ValidationTestModel) + class ValidationTestModelAdmin(ModelAdmin): + list_display_links = None + + ValidationTestModelAdmin.validate(ValidationTestModel) + def test_list_filter_validation(self): class ValidationTestModelAdmin(ModelAdmin):