Readonly models in Django admin interface? - django

How can I make a model completely read-only in the admin interface? It's for a kind of log table, where I'm using the admin features to search, sort, filter etc, but there is no need to modify the log.
In case this looks like a duplicate, here's not what I'm trying to do:
I'm not looking for readonly fields (even making every field readonly would still let you create new records)
I'm not looking to create a readonly user: every user should be readonly.

The admin is for editing, not just viewing (you won't find a "view" permission). In order to achieve what you want you'll have to forbid adding, deleting, and make all fields readonly:
class MyAdmin(ModelAdmin):
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
(if you forbid changing you won't even get to see the objects)
For some untested code that tries to automate setting all fields read-only see my answer to Whole model as read-only
EDIT: also untested but just had a look at my LogEntryAdmin and it has
readonly_fields = MyModel._meta.get_all_field_names()
Don't know if that will work in all cases.
EDIT: QuerySet.delete() may still bulk delete objects. To get around this, provide your own "objects" manager and corresponding QuerySet subclass which doesn't delete - see Overriding QuerySet.delete() in Django

Here are two classes I am using to make a model and/or it's inlines read only.
For model admin:
from django.contrib import admin
class ReadOnlyAdmin(admin.ModelAdmin):
readonly_fields = []
def get_readonly_fields(self, request, obj=None):
return list(self.readonly_fields) + \
[field.name for field in obj._meta.fields] + \
[field.name for field in obj._meta.many_to_many]
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
class MyModelAdmin(ReadOnlyAdmin):
pass
For inlines:
class ReadOnlyTabularInline(admin.TabularInline):
extra = 0
can_delete = False
editable_fields = []
readonly_fields = []
exclude = []
def get_readonly_fields(self, request, obj=None):
return list(self.readonly_fields) + \
[field.name for field in self.model._meta.fields
if field.name not in self.editable_fields and
field.name not in self.exclude]
def has_add_permission(self, request):
return False
class MyInline(ReadOnlyTabularInline):
pass

See https://djangosnippets.org/snippets/10539/
class ReadOnlyAdminMixin(object):
"""Disables all editing capabilities."""
change_form_template = "admin/view.html"
def __init__(self, *args, **kwargs):
super(ReadOnlyAdminMixin, self).__init__(*args, **kwargs)
self.readonly_fields = [f.name for f in self.model._meta.get_fields()]
def get_actions(self, request):
actions = super(ReadOnlyAdminMixin, self).get_actions(request)
del_action = "delete_selected"
if del_action in actions:
del actions[del_action]
return actions
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
def save_model(self, request, obj, form, change):
pass
def delete_model(self, request, obj):
pass
def save_related(self, request, form, formsets, change):
pass
templates/admin/view.html
{% extends "admin/change_form.html" %}
{% load i18n %}
{% block submit_buttons_bottom %}
<div class="submit-row">
{% blocktrans %}Back to list{% endblocktrans %}
</div>
{% endblock %}
templates/admin/view.html (for Grappelli)
{% extends "admin/change_form.html" %}
{% load i18n %}
{% block submit_buttons_bottom %}
<footer class="grp-module grp-submit-row grp-fixed-footer">
<header style="display:none"><h1>{% trans "submit options"|capfirst context "heading" %}</h1></header>
<ul>
<li>{% blocktrans %}Back to list{% endblocktrans %}</li>
</ul>
</footer>
{% endblock %}

If you want the user become aware that he/she cannot edit it, 2 pieces are missing on the first solution. You have remove the delete action!
class MyAdmin(ModelAdmin)
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def get_actions(self, request):
actions = super(MyAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
Second: the readonly solution works fine on plain models. But it does NOT work if you have an inherited model with foreign keys. Unfortunately, I don't know the solution for that yet. A good attempt is:
Whole model as read-only
But it does not work for me either.
And a final note, if you want to think on a broad solution, you have to enforce that each inline has to be readonly too.

with django 2.2+, readonly admin can be as simple as:
class ReadOnlyAdminMixin:
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
class LogEntryAdmin(ReadOnlyAdminMixin, admin.ModelAdmin):
list_display = ('id', 'user', 'action_flag', 'content_type', 'object_repr')

Actually you can try this simple solution:
class ReadOnlyModelAdmin(admin.ModelAdmin):
actions = None
list_display_links = None
# more stuff here
def has_add_permission(self, request):
return False
actions = None: avoids showing the dropdown with the "Delete selected ..." option
list_display_links = None: avoids clicking in columns to edit that object
has_add_permission() returning False avoids creating new objects for that model

This was added in to Django 2.1 which was released on 8/1/18!
ModelAdmin.has_view_permission() is just like the existing has_delete_permission, has_change_permission and has_add_permission. You can read about it in the docs here
From the release notes:
This allows giving users read-only access to models in the admin.
ModelAdmin.has_view_permission() is new. The implementation is
backwards compatible in that there isn’t a need to assign the “view”
permission to allow users who have the “change” permission to edit
objects.

If the accepted answer doesn't work for you, try this:
def get_readonly_fields(self, request, obj=None):
readonly_fields = []
for field in self.model._meta.fields:
readonly_fields.append(field.name)
return readonly_fields

Compiling #darklow and #josir 's excellent answers, plus adding a bit more to remove "Save" and "Save and Continue" buttons leads to (in Python 3 syntax):
class ReadOnlyAdmin(admin.ModelAdmin):
"""Provides a read-only view of a model in Django admin."""
readonly_fields = []
def change_view(self, request, object_id, extra_context=None):
""" customize add/edit form to remove save / save and continue """
extra_context = extra_context or {}
extra_context['show_save_and_continue'] = False
extra_context['show_save'] = False
return super().change_view(request, object_id, extra_context=extra_context)
def get_actions(self, request):
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
def get_readonly_fields(self, request, obj=None):
return list(self.readonly_fields) + \
[field.name for field in obj._meta.fields] + \
[field.name for field in obj._meta.many_to_many]
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
and then you use like
class MyModelAdmin(ReadOnlyAdmin):
pass
I've only tried this with Django 1.11 / Python 3.

With Django 2.2 I do it like this:
#admin.register(MyModel)
class MyAdmin(admin.ModelAdmin):
readonly_fields = ('all', 'the', 'necessary', 'fields')
actions = None # Removes the default delete action in list view
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

The accepted answer should work, but this will also preserve the display order of the readonly fields. You also don't have to hardcode the model with this solution.
class ReadonlyAdmin(admin.ModelAdmin):
def __init__(self, model, admin_site):
super(ReadonlyAdmin, self).__init__(model, admin_site)
self.readonly_fields = [field.name for field in filter(lambda f: not f.auto_created, model._meta.fields)]
def has_delete_permission(self, request, obj=None):
return False
def has_add_permission(self, request, obj=None):
return False

I ran into the same requirement when needing to make all fields readonly for certain users in django admin ended up leveraging on django module "django-admin-view-permission" without rolling my own code. If you need more fine grained control to explicitly define which fields then you would need to extend the module. You can check out the plugin in action here

I have written a generic class to handle ReadOnly view depending on User permissions, including inlines ;)
In models.py:
class User(AbstractUser):
...
def is_readonly(self):
if self.is_superuser:
return False
# make readonly all users not in "admins" group
adminGroup = Group.objects.filter(name="admins")
if adminGroup in self.groups.all():
return False
return True
In admin.py:
# read-only user filter class for ModelAdmin
class ReadOnlyAdmin(admin.ModelAdmin):
def __init__(self, *args, **kwargs):
# keep initial readonly_fields defined in subclass
self._init_readonly_fields = self.readonly_fields
# keep also inline readonly_fields
for inline in self.inlines:
inline._init_readonly_fields = inline.readonly_fields
super().__init__(*args,**kwargs)
# customize change_view to disable edition to readonly_users
def change_view( self, request, object_id, form_url='', extra_context=None ):
context = extra_context or {}
# find whether it is readonly or not
if request.user.is_readonly():
# put all fields in readonly_field list
self.readonly_fields = [ field.name for field in self.model._meta.get_fields() if not field.auto_created ]
# readonly mode fer all inlines
for inline in self.inlines:
inline.readonly_fields = [field.name for field in inline.model._meta.get_fields() if not field.auto_created]
# remove edition buttons
self.save_on_top = False
context['show_save'] = False
context['show_save_and_continue'] = False
else:
# if not readonly user, reset initial readonly_fields
self.readonly_fields = self._init_readonly_fields
# same for inlines
for inline in self.inlines:
inline.readonly_fields = self._init_readonly_fields
return super().change_view(
request, object_id, form_url, context )
def save_model(self, request, obj, form, change):
# disable saving model for readonly users
# just in case we have a malicious user...
if request.user.is_readonly():
# si és usuari readonly no guardem canvis
return False
# if not readonly user, save model
return super().save_model( request, obj, form, change )
Then, we can just inherit normally our classes in admin.py:
class ContactAdmin(ReadOnlyAdmin):
list_display = ("name","email","whatever")
readonly_fields = ("updated","created")
inlines = ( PhoneInline, ... )

read-only => views permission
pipenv install django-admin-view-permission
add 'admin_view_permission' to INSTALLED_APPS in the settings.py.like this:
`INSTALLED_APPS = [
'admin_view_permission',
python manage.py migrate
python manage.py runserver 6666
ok.have fun with the 'views' permission

Related

In Django admin how can i hide or remove the Pencil, "+" and "x"?

I can hide all the options in the model base, but is not necessary, on the relation i can't do that i think that exist a simple form (not with css) to remove or hide it
Thanks
I agree with Ivan Camilito Ramirez Verdes's solution :
class MyModelAdmin(admin.ModelAdmin):
list_display = (
'my_field',
)
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
form.base_fields['my_field'].widget.can_change_related = False
form.base_fields['my_field'].widget.can_add_related = False
return form
I presume you wish to disable the ADD, EDIT, and DELETE functionalities from the admin.
The Django Documentation provides the following functions that you can override in your ModelAdmin:
class SomeModelAdmin(admin.ModelAdmin):
def has_add_permission(self, request):
return False
def has_change_permission(self, request):
return False
def has_delete_permission(self, request):
return False

How to make invisiable some item from select

I have Reporter model, and when I create News model, I have to choose reporter for this news, and I want to disappear Jimmy Olson from choose, but he must be in db, but not in choose list. how to make it?
Just override get_form method in your admin.py
def get_form(self, request, obj=None, **kwargs):
form = super(NewsAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['reporter'].queryset = form.base_fields['reporter'].queryset.filter(name='Jimmy Olson')
return form
you can use tabularinlne of django in admin in order to ease of inserting in news model
class NewsInline(admin.TabularInline):
model = News
extra = 0
show_change_link = True
def has_add_permission(self, request, obj=None):
return False
class ReporterAdmin(admin.ModelAdmin):
list_display = ['race', ....]
list_filter = ['race',....]
inlines = [NewsInline, ]
and then register your model in admin
admin.site.register(Reporter, ReporterAdmin)

Django admin: prevent add, change, but not delete

I want to be able to only delete values from admin.
I wrote following code for this:
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 True
However, in this case I can't find link to delete object.
How this can be resolved?
It makes sense that the change list view is disabled. I noticed that visiting /admin/app/model/1/delete/ will let you delete the object.
So you have basically two options:
Create a custom admin page listing the models objects. Each object
with a delete button that links to /admin/app/model/pk/delete/.
Hook this into your admin somehow.
Set has_change_permission
to True and make sure the detail page displays a custom form, all
fields with readonly widgets.
I would go for 2. Because it is less work, gives you all the benefits of the change list page (filters, actions) and keeps the default admin structure. A large benefit is that the user can see what he is about to delete.
I would do something like this (not tested):
class ItemForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs['readonly'] = True
class Meta:
model = Item
exclude = []
class ItemAdmin(admin.ModelAdmin):
form = ItemForm
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return True
def has_delete_permission(self, request, obj=None):
return True

django admin readonly data (no edit, no add)

admin.py
class ChatMessageAdmin(admin.ModelAdmin):
def has_add_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
list_display = ('username','ip', 'timestamp','message',)
search_fields = ['message']
list_filter = ('username','ip')
admin.site.register(ChatMessage,ChatMessageAdmin)
when I rewrite has_change_permission method like upper I can't see the http://[domain]/admin/chat page which shows all the chat entries
How can I rewrite has_change_permission to have access to this page?
Edit
I don't the auth_user can edit the chat entry, but if he wants I want to have the ability to delete it.

django admin make a field read-only when modifying obj but required when adding new obj

In admin I would like to disable a field when modifying object, but make it required when adding new object.
Whats the django way to go about this one?
You can override the admin's get_readonly_fields method:
class MyModelAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if obj: # editing an existing object
return self.readonly_fields + ('field1', 'field2')
return self.readonly_fields
If you want to set all fields as read only just on the change view, override the admin's get_readonly_fields:
def get_readonly_fields(self, request, obj=None):
if obj: # editing an existing object
# All model fields as read_only
return self.readonly_fields + tuple([item.name for item in obj._meta.fields])
return self.readonly_fields
And if you want to hide save buttons on change view:
Change the view
def change_view(self, request, object_id, form_url='', extra_context=None):
''' customize edit form '''
extra_context = extra_context or {}
extra_context['show_save_and_continue'] = False
extra_context['show_save'] = False
extra_context['show_save_and_add_another'] = False # this not works if has_add_permision is True
return super(TransferAdmin, self).change_view(request, object_id, extra_context=extra_context)
Change permissions if user is trying to edit:
def has_add_permission(self, request, obj=None):
# Not too much elegant but works to hide show_save_and_add_another button
if '/change/' in str(request):
return False
return True
This solution has been tested over Django 1.11
A variation based on the previous excellent suggestion of Bernhard Vallant, which also preserves any possible customization provided by the base class (if any):
class MyModelAdmin(BaseModelAdmin):
def get_readonly_fields(self, request, obj=None):
readonly_fields = super(MyModelAdmin, self).get_readonly_fields(request, obj)
if obj: # editing an existing object
return readonly_fields + ['field1', ..]
return readonly_fields
A more pluggable Solution to the great solutions of Bernhard and Mario, adding support for createonly_fields analog to readonly_fields:
class MyModelAdmin(admin.ModelAdmin):
# ModelAdmin configuration as usual goes here
createonly_fields = ['title', ]
def get_readonly_fields(self, request, obj=None):
readonly_fields = list(super(MyModelAdmin, self).get_readonly_fields(request, obj))
createonly_fields = list(getattr(self, 'createonly_fields', []))
if obj: # editing an existing object
readonly_fields.extend(createonly_fields)
return readonly_fields
The situation with inline forms is still not fixed for Django 2.2.x but the solution from John is actually pretty smart.
Code slightly tuned to my situation:
class NoteListInline(admin.TabularInline):
""" Notes list, readonly """
model = Note
verbose_name = _('Note')
verbose_name_plural = _('Notes')
extra = 0
fields = ('note', 'created_at')
readonly_fields = ('note', 'created_at')
def has_add_permission(self, request, obj=None):
""" Only add notes through AddInline """
return False
class NoteAddInline(admin.StackedInline):
""" Notes edit field """
model = Note
verbose_name = _('Note')
verbose_name_plural = _('Notes')
extra = 1
fields = ('note',)
can_delete = False
def get_queryset(self, request):
queryset = super().get_queryset(request)
return queryset.none() # no existing records will appear
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
# ...
inlines = (NoteListInline, NoteAddInline)
# ...
FYI: in case someone else runs into the same two problems I encountered:
You should still declare any permanently readonly_fields in the body of the class, as the readonly_fields class attribute will be accessed from validation (see django.contrib.admin.validation: validate_base(), line.213 appx)
This won't work with Inlines as the obj passed to get_readonly_fields() is the parent obj (I have two rather hacky and low-security solutions using css or js)
You can do this by overriding the formfield_for_foreignkey method of the ModelAdmin:
from django import forms
from django.contrib import admin
from yourproject.yourapp.models import YourModel
class YourModelAdmin(admin.ModelAdmin):
class Meta:
model = YourModel
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
# Name of your field here
if db_field.name == 'add_only':
if request:
add_opts = (self._meta.app_label, self._meta.module_name)
add = u'/admin/%s/%s/add/' % add_opts
if request.META['PATH_INFO'] == add:
field = db_field.formfield(**kwargs)
else:
kwargs['widget'] = forms.HiddenInput()
field = db_field.formfield(**kwargs)
return field
return admin.ModelAdmin(self, db_field, request, **kwargs)
Got a similar problem. I solved it with "add_fieldsets" and "restricted_fieldsets" in the ModelAdmin.
from django.contrib import admin
class MyAdmin(admin.ModelAdmin):
declared_fieldsets = None
restricted_fieldsets = (
(None, {'fields': ('mod_obj1', 'mod_obj2')}),
( 'Text', {'fields': ('mod_obj3', 'mod_obj4',)}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('add_obj1', 'add_obj2', )}),
)
Please see e.g.: http://code.djangoproject.com/svn/django/trunk/django/contrib/auth/admin.py
But this doesn't protect your model from later changes of "add_objX".
If you want this too, I think you have to go the way over the Model class "save" function and check for changes there.
See: www.djangoproject.com/documentation/models/save_delete_hooks/
Greez, Nick