Readonly models in Django admin interface?

DjangoDjango AdminReadonly

Django Problem Overview


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.

Django Solutions


Solution 1 - Django

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 https://stackoverflow.com/questions/7920371/whole-model-as-read-only/7965193#7965193

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 https://stackoverflow.com/questions/6459616/overriding-queryset-delete-in-django/6459797#6459797

Solution 2 - 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

Solution 3 - Django

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">
    <a href="../">{% blocktrans %}Back to list{% endblocktrans %}</a>
  </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><a href="../" class="grp-button grp-default">{% blocktrans %}Back to list{% endblocktrans %}</a></li>
    </ul>
  </footer>
{% endblock %}

Solution 4 - Django

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:

https://stackoverflow.com/questions/7920371/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.

Solution 5 - Django

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

Solution 6 - Django

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.

Solution 7 - Django

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')

Solution 8 - Django

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

Solution 9 - Django

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.

Solution 10 - Django

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

Solution 11 - Django

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

Solution 12 - Django

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

Solution 13 - Django

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

Solution 14 - Django

read-only => views permission

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSteve BennettView Question on Stackoverflow
Solution 1 - DjangoDanny W. AdairView Answer on Stackoverflow
Solution 2 - DjangodarklowView Answer on Stackoverflow
Solution 3 - DjangoPascal PolleunusView Answer on Stackoverflow
Solution 4 - DjangoJosirView Answer on Stackoverflow
Solution 5 - DjangoIván ZugnoniView Answer on Stackoverflow
Solution 6 - DjangogrrrrrrView Answer on Stackoverflow
Solution 7 - Djangocheng10View Answer on Stackoverflow
Solution 8 - DjangoWouterView Answer on Stackoverflow
Solution 9 - DjangoMark ChackerianView Answer on Stackoverflow
Solution 10 - DjangoEerik Sven PuudistView Answer on Stackoverflow
Solution 11 - DjangolastoneisbearfoodView Answer on Stackoverflow
Solution 12 - DjangoTimothy MugayiView Answer on Stackoverflow
Solution 13 - DjangoEnric MiezaView Answer on Stackoverflow
Solution 14 - DjangoXianhong XuView Answer on Stackoverflow