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
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.
In Django 1.7 and later, you can do
class HitAdmin(admin.ModelAdmin):
list_display_links = None
Doing this properly requires two steps:
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.
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.
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.
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:
/add
to the URLFinally you can remove the "Delete selected items" action by modifying the actions
parameter.
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
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.
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.
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.