55

In Django's admin, I want disable the links provided on the "select item to change" page so that users cannot go anywhere to edit the item. (I am going to limit what the users can do with this list to a set of drop down actions - no actual editing of fields).

I see that Django has the ability to choose which fields display the link, however, I can't see how I can have none of them.

class HitAdmin(admin.ModelAdmin):
    list_display = ('user','ip','user_agent','hitcount')
    search_fields = ('ip','user_agent')
    date_hierarchy = 'created'
    list_display_links = [] # doesn't work, goes to default

Any ideas how to get my object list without any links to edit?

thornomad
  • 6,707
  • 10
  • 53
  • 78

13 Answers13

64

I wanted a Log viewer as a list only.

I got it working like this:

class LogEntryAdmin(ModelAdmin):
    actions = None
    list_display = (
        'action_time', 'user',
        'content_type', 'object_repr', 
        'change_message')

    search_fields = ['=user__username', ]
    fieldsets = [
        (None, {'fields':()}), 
        ]

    def __init__(self, *args, **kwargs):
        super(LogEntryAdmin, self).__init__(*args, **kwargs)
        self.list_display_links = (None, )

It is kind of a mix between both answers.

If you just do self.list_display_links = () it will show the link, Anyway because the template-tag code (templatetags/admin_list.py) checks again to see if the list is empty.

Alireza Savand
  • 3,462
  • 3
  • 26
  • 36
Federico
  • 755
  • 6
  • 6
  • 2
    Just found your post and this is working for me too (setting `self.list_diplay_links = (None,)` in the `__init__`. Thanks! – thornomad Feb 24 '10 at 13:32
  • 3
    non of the answers provide a way to actually disallow changing the log entries. although the user does not see a link, he can access the edit form by typing the url to the edit form and can change the entry. the user has to have `can_change' permission to see the change list view. this introduces a serious security hole. – onurmatik Dec 26 '10 at 16:39
  • 2
    @omat: You can override the ModelAdmin.change_view to redirect to the changelist page, or wherever you like for that matter, should someone try to manually get to the page. I'll provide an example below. – Chris Pratt Apr 29 '11 at 20:57
  • This solution is just hacking the question itself! – Alireza Savand Apr 04 '12 at 15:21
  • 2
    Unfortunately, this doesn't appear to work in Django 2.2. Now Django gives you the error `The value of 'list_display_links[0]' refers to 'None', which is not defined in 'list_display'.` – Cerin Nov 25 '19 at 05:00
  • 1
    @Cerin Just use `list_display_links = None` – NoName Jun 19 '20 at 21:46
  • this doesn't appear to work in Django 3.2. – F.Tamy Dec 13 '22 at 22:17
44

Doing this properly requires two steps:

  • Hide the edit link, so nobody stumbles on the detail page (change view) by mistake.
  • Modify the change view to redirect back to the list view.

The second part is important: if you don't do this then people will still be able to access the change view by entering a URL directly (which presumably you don't want). This is closely related to what OWASP term an "Insecure Direct Object Reference".

As part of this answer I'll build a ReadOnlyMixin class that can be used to provide all the functionality shown.

Hiding the Edit Link

Django 1.7 makes this really easy: you just set list_display_links to None.

class ReadOnlyMixin(): # Add inheritance from "object" if using Python 2
    list_display_links = None

Django 1.6 (and presumably earlier) don't make this so simple. Quite a lot of answers to this question have suggested overriding __init__ in order to set list_display_links after the object has been constructed, but this makes it harder to reuse (we can only override the constructor once).

I think a better option is to override Django's get_list_display_links method as follows:

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().

    We override Django's default implementation to specify no links unless
    these are explicitly set.
    """
    if self.list_display_links or not list_display:
        return self.list_display_links
    else:
        return (None,)

This makes our mixin easy to use: it hides the edit link by default but allows us to add it back in if required for a particular admin view.

Redirecting to the List View

We can change the behaviour of the detail page (change view) by overriding the change_view method. Here's an extension to the technique suggested by Chris Pratt which automatically finds the right page:

enable_change_view = False

def change_view(self, request, object_id, form_url='', extra_context=None):
    """
    The 'change' admin view for this model.

    We override this to redirect back to the changelist unless the view is
    specifically enabled by the "enable_change_view" property.
    """
    if self.enable_change_view:
        return super(ReportMixin, self).change_view(
            request,
            object_id,
            form_url,
            extra_context
        )
    else:
        from django.core.urlresolvers import reverse
        from django.http import HttpResponseRedirect

        opts = self.model._meta
        url = reverse('admin:{app}_{model}_changelist'.format(
            app=opts.app_label,
            model=opts.model_name,
        ))
        return HttpResponseRedirect(url)

Again this is customisable - by toggling enable_change_view to True you can switch the details page back on.

Removing the "Add ITEM" Button

Finally, you might want to override the following methods in order to prevent people adding or deleting new items.

def has_add_permission(self, request):
    return False

def has_delete_permission(self, request, obj=None):
    return False

These changes will:

  • disable the "Add item" button
  • prevent people directly adding items by appending /add to the URL
  • prevent bulk delete

Finally you can remove the "Delete selected items" action by modifying the actions parameter.

Putting it all together

Here's the completed mixin:

from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect

class ReadOnlyMixin(): # Add inheritance from "object" if using Python 2

    actions = None

    enable_change_view = False

    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().

        We override Django's default implementation to specify no links unless
        these are explicitly set.
        """
        if self.list_display_links or not list_display:
            return self.list_display_links
        else:
            return (None,)

    def change_view(self, request, object_id, form_url='', extra_context=None):
        """
        The 'change' admin view for this model.

        We override this to redirect back to the changelist unless the view is
        specifically enabled by the "enable_change_view" property.
        """
        if self.enable_change_view:
            return super(ReportMixin, self).change_view(
                request,
                object_id,
                form_url,
                extra_context
            )
        else:
            opts = self.model._meta
            url = reverse('admin:{app}_{model}_changelist'.format(
                app=opts.app_label,
                model=opts.model_name,
            ))
            return HttpResponseRedirect(url)

    def has_add_permission(self, request):
        return False

    def has_delete_permission(self, request, obj=None):
        return False
simpleigh
  • 2,854
  • 18
  • 19
  • 1
    Just a note for future readers: the `super()` syntax used in this answer is the Py2 syntax. With the Py3 `super()` syntax this mixin is more easily reusable for any model because you won't need to include the class name in the `super()` call as an attribute, it's just `super().change_view(...)`. – Bryson Apr 16 '18 at 19:22
  • This is really the most comprehensive and awesome post! I'd suggest to build a mixin for the logic so you can reuse it and overwrite the change_view like `def change_view(self, *args, **kwargs)` to be more resilient to future changes :) – Ron Oct 06 '21 at 06:24
  • I added my mixin as a separate answer, if somebdoy needs configuration and reusability: https://stackoverflow.com/a/69461048/1331671 – Ron Oct 06 '21 at 06:49
24

In Django 1.7 and later, you can do

class HitAdmin(admin.ModelAdmin):
    list_display_links = None
Blaise
  • 13,139
  • 9
  • 69
  • 97
20

As user, omat, mentioned in a comment above, any attempt to simply remove the links does not prevent users from still accessing the change page manually. However, that, too, is easy enough to remedy:

class MyModelAdmin(admin.ModelAdmin)
    # Other stuff here
    def change_view(self, request, obj=None):
        from django.core.urlresolvers import reverse
        from django.http import HttpResponseRedirect
        return HttpResponseRedirect(reverse('admin:myapp_mymodel_changelist'))
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • 1
    +1 for this method since it allows for one to interact with the `request` object prior to enforcing the "ban". Thus one can for example implement the limit based on the `request.user` or even `request.session`. – JWL Jan 23 '13 at 17:09
8

In your model admin set:

list_display_links = (None,)

That should do it. (Works in 1.1.1 anyway.)

Link to docs: list_display_links

Paolo
  • 20,112
  • 21
  • 72
  • 113
Josh Ourisman
  • 1,078
  • 1
  • 9
  • 16
  • 1
    Hmm - I am using the trunk and I get a `TypeError: getattr(): attribute name must be string [02/Nov/2009 12:05:22] "GET /admin/ HTTP/1.1" 500 2524` error when I try that. Interesting. – thornomad Nov 02 '09 at 11:52
  • Hmm, interesting. I just checked my project where I'm doing that, and it's actually running 1.1 (r11602). I just tried upping my project to trunk (r11706), and it still seems to work fine. I do however have some other admin stuff going on (which you can see the details of here http://joshourisman.com/2009/10/15/django-admin-awesomeness/) such that my ModelAdmin that has list_display_links set to (None,) is not actually in my admin.py... I don't see why that would make a different to this, however. – Josh Ourisman Nov 02 '09 at 15:24
  • It is strange that yours is working; not sure what's different about it ... but, there I am getting pinpoints that line of code every time. Wierd. – thornomad Nov 02 '09 at 22:44
  • What happens if you comment out the other settings in your ModelAdmin and just have that? – Josh Ourisman Nov 03 '09 at 14:41
  • I have tried it with a completely empty modelAdmin and only: `list_display_links = (None,)` and still get the same TypeError ... if it works for you, though, I feel I must be doing something wrong. – thornomad Nov 06 '09 at 12:34
  • Hmm, I just tried adding it to a different project of mine running on Django 1.1.1, and now I'm getting the TypeError too. It's definitely working on the original project however, so I'll take a closer look at it once I get into work. – Josh Ourisman Nov 06 '09 at 13:24
  • Yes, you cannot do this specifically on the ModelAdmin itself. There's check in ModelAdmin.__init__ on its value. Instead, you must override __init__, call super, and then set list_display_links = (None,). See answer by Frederico above. – Chris Pratt Apr 29 '11 at 21:07
5

just write list_display_links = None in your admin

Sameer Yadav
  • 63
  • 1
  • 5
4

Just for the notes, you may modify changelist_view:

class SomeAdmin(admin.ModelAdmin):
    def changelist_view(self, request, extra_context=None):
        self.list_display_links = (None, )
        return super(SomeAdmin, self).changelist_view(request, extra_context=None)

This works fine for me.

rthill
  • 143
  • 3
  • 11
3

In more "recent" versions of Django, since at least 1.9, it is possible to simple determine the add, change and delete permissions on the admin class. See the django admin documentation for reference. Here is an example:

@admin.register(Object)
class Admin(admin.ModelAdmin):

    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False
shezi
  • 560
  • 3
  • 18
2

There isn't a supported way to do this.

Looking at the code, it seems that it automatically sets ModelAdmin.list_display_links to the first element if you don't set it to anything. So the easiest way might be to override the __init__ method in your ModelAdmin subclass to unset that attribute on initialization:

class HitAdmin(admin.ModelAdmin):
    list_display = ('user','ip','user_agent','hitcount')
    search_fields = ('ip','user_agent')
    date_hierarchy = 'created'

    def __init__(self, *args, **kwargs):
        super(HitAdmin, self).__init__(*args, **kwargs)
        self.list_display_links = []

This appears to work, after a very cursory test. I can't guarantee that it won't break anything elsewhere, or that it won't be broken by future changes to Django, though.

Edit after comment:

No need to patch the source, this would work:

    def __init__(self, *args, **kwargs):
        if self.list_display_links:
            unset_list_display = True
        else:
            unset_list_display = False
        super(HitAdmin, self).__init__(*args, **kwargs)
        if unset_list_display:
            self.list_display_links = []

But I highly doubt any patch would be accepted into Django, since this breaks something that the code explicitly does at the moment.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • Thanks for that - it works with me, as well. Do you see an obvious/pythonic way to patch the source that says _if the child has it set to empty, don't do anything // but if the child hasn't set it at all, do something_ ? Could submit it as a patch ... but all my ideas to patch, at this point, are rather hackish. – thornomad Oct 25 '09 at 00:44
  • Thanks - maybe if they added another variable to `ModelAdmin` such as your suggested `unset_list_display_links` [True/False] then they could check that on the same `if` statement they are checking the `list_display_links` ... then again, they may just suggest to override it the way you have done. – thornomad Oct 25 '09 at 11:25
  • On closer inspection, I noticed that the link is being added now to the select box - so, when you select an item, it opens the page! Ugh. – thornomad Oct 25 '09 at 20:23
  • It's part of Django's (sometimes infuriating) admin policy: users are supposed to be trusted. As a result, there's no built in way to restrict access to things users would otherwise have access to. A patch would undoubtedly be denied, but there are still ways around it. – Chris Pratt Apr 29 '11 at 21:11
1

You could also be ridiculously hacky about it (if you didn't want to fuss with overriding init) and provide a value for the first element that basically looks like this:

</a>My non-linked value<a>

I know, I know, not very pretty, but perhaps less anxiety about breaking something elsewhere since all we're doing is changing markup.

Here's some sample code about how this works:

class HitAdmin(admin.ModelAdmin):
    list_display = ('user_no_link','ip','user_agent','hitcount')

    def user_no_link(self, obj):
        return u'</a>%s<a>' % obj
    user_no_link.allow_tags = True
    user_no_link.short_description = "user"

Side Note: You could also improve the readability of the output (since you don't want it to be a link) by returning return u'%s' % obj.get_full_name() which might be kinda neat depending on your use case.

T. Stone
  • 19,209
  • 15
  • 69
  • 97
  • That's an interesting approach too - while the above `__init__` tactic may break it, it seems a little more intuitive ... but this gives me some ideas, anyway. Thanks. – thornomad Oct 25 '09 at 00:45
0

with django 1.6.2 you can do like this:

class MyAdmin(admin.ModelAdmin):

    def get_list_display_links(self, request, list_display):
        return []

it will hide all auto generated links.

truease.com
  • 1,261
  • 2
  • 17
  • 30
0

I overrided get_list_display_links method and action to None.

class ChangeLogAdmin(admin.ModelAdmin):
    actions = None
    list_display = ('asset', 'field', 'before_value', 'after_value', 'operator', 'made_at')

    fieldsets = [
        (None, {'fields': ()}),
    ]

    def __init__(self, model, admin_site):
        super().__init__(model, admin_site)

    def get_list_display_links(self, request, list_display):
        super().get_list_display_links(request, list_display)
        return None
Jake Jeon
  • 123
  • 3
  • 7
0

I build a mixin based on @simpleigh 's solution.

class DeactivatableChangeViewAdminMixin:
    """
    Mixin to be used in model admins to disable the detail page / change view.
    """
    enable_change_view = True

    def can_see_change_view(self, request) -> bool:
        """
        This method determines if the change view is disabled or visible.
        """
        return self.enable_change_view

    def get_list_display_links(self, request, list_display):
        """
        When we don't want to show the change view, there is no need for having a link to it
        """
        if not self.can_see_change_view(request=request):
            return None
        return super().get_list_display_links(request, list_display)

    def change_view(self, request, *args, **kwargs):
        """
        The 'change' admin view for this model.

        We override this to redirect back to the changelist unless the view is
        specifically enabled by the "enable_change_view" property.
        """
        if self.can_see_change_view(request=request):
            return super().change_view(request, *args, **kwargs)
        else:
            opts = self.model._meta
            url = reverse('admin:{app}_{model}_changelist'.format(
                app=opts.app_label,
                model=opts.model_name,
            ))
            return HttpResponseRedirect(url)

The benefit is that you can reuse it and you can furthermore make it conditional

Build for django 3.2.8.

To be used like this for a static approach:

class MyAdmin(DeactivatableChangeViewAdminMixin, admin.ModelAdmin):
  enable_change_view = False

And like this for a non-static one:

class MyAdmin(DeactivatableChangeViewAdminMixin, admin.ModelAdmin):  
    def can_see_change_view(self, request) -> bool:
        return request.user.my_condition
Ron
  • 22,128
  • 31
  • 108
  • 206