[Fixed]-Whole model as read-only

13đź‘Ť

âś…

ModelAdmin provides the hook get_readonly_fields() – the following is untested, my idea being to determine all fields the way ModelAdmin does it, without running into a recursion with the readonly fields themselves:

from django.contrib.admin.util import flatten_fieldsets

class ReadOnlyAdmin(ModelAdmin):
    def get_readonly_fields(self, request, obj=None):
        if self.declared_fieldsets:
            fields = flatten_fieldsets(self.declared_fieldsets)
        else:
            form = self.get_formset(request, obj).form
            fields = form.base_fields.keys()
        return fields

then subclass/mixin this admin whereever it should be a read-only admin.

For add/delete, and to make their buttons disappear, you’ll probably also want to add

    def has_add_permission(self, request):
        # Nobody is allowed to add
        return False
    def has_delete_permission(self, request, obj=None):
        # Nobody is allowed to delete
        return False

P.S.: In ModelAdmin, if has_change_permission (lookup or your override) returns False, you don’t get to the change view of an object – and the link to it won’t even be shown. It would actually be cool if it did, and the default get_readonly_fields() checked the change permission and set all fields to readonly in that case, like above. That way non-changers could at least browse the data… given that the current admin structure assumes view=edit, as jathanism points out, this would probably require the introduction of a “view” permission on top of add/change/delete…

EDIT: regarding setting all fields readonly, also untested but looking promising:

readonly_fields = MyModel._meta.get_all_field_names()

EDIT: Here’s another one

if self.declared_fieldsets:
    return flatten_fieldsets(self.declared_fieldsets)
else:
    return list(set(
        [field.name for field in self.opts.local_fields] +
        [field.name for field in self.opts.local_many_to_many]
    ))

4đź‘Ť

As “view permissions” will not make it into Django 1.11, unfortunately, here’s a solution that makes your ModelAdmin read-only by making both saving model changes and adding model history log entries a no-op.

def false(*args, **kwargs):
    """A simple no-op function to make our changes below readable."""
    return False

class MyModelReadOnlyAdmin(admin.ModelAdmin):
    list_display = [
        # list your admin listview entries here (as usual) 
    ]
    readonly_fields = [
        # list your read-only fields here (as usual)
    ]

    actions = None
    has_add_permission = false
    has_delete_permission = false
    log_change = false
    message_user = false
    save_model = false

(NOTE: Don’t mistake the false no-op helper with the False builtin. If you don’t sympathize with the helper function outside the class move it into the class, call it no_op or something else, or override the affected attributes by usual defs. Less DRY, but if you don’t care…)

This will:

  1. remove the actions drop-down box (with “delete”) in the list view
  2. disallow adding new model entries
  3. disallow deleting existing model entries
  4. avoid creating log entries in the model history
  5. avoid displaying “was changed successfully” messages after saving
  6. avoid saving changeform changes to the database

It will not:

  • remove or replace the two buttons “Save and continue editing” and “SAVE” (which would be nice to improve the user experience)

Note that get_all_field_names (as mentioned in the accepted answer) was removed in Django 1.10.
Tested with Django 1.10.5.

👤Peterino

4đź‘Ť

The selected answer doesn’t work for Django 1.11, and I’ve found a much simpler way to do it so I thought I’d share:

class MyModelAdmin(admin.ModelAdmin):

    def get_readonly_fields(self, request, obj=None):
        return [f.name for f in obj._meta.fields]

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

    def has_add_permission(self, request):
        return False
👤Daniel Quinn

3đź‘Ť

You may customize your ModelAdmin classes with the readonly_fields attribute. See this answer for more.

👤jathanism

0đź‘Ť

I had a similar scenario where:

  1. User should be able to create the model objects
  2. User should be able to view listing of model objects
  3. User SHOULD’NT be able to edit an object once it’s been created

1. Overriding the Change View

Because it’s possible to override the change_view() in a ModelAdmin, we can exploit that to prevent the editing of model instances once they have been created. Here’s an example I’ve used:

def change_view(self, request, object_id, form_url='', extra_context=None):
    messages.error(request, 'Sorry, but editing is NOT ALLOWED')
    return redirect(request.META['HTTP_REFERER'])

2. Conditionally Change Edit Permissions

I also realized that the docs interpret the result of ModelAdmin.has_change_permission() in different ways:

Should return True if editing obj is permitted, False otherwise. If
obj is None, should return True or False to indicate whether editing
of objects of this type is permitted in general (e.g., False will be
interpreted as meaning that the current user is not permitted to edit
any object of this type).

Meaning I could check whether obj is None, in which case I return True, otherwise I return False, and this in effect allows users to view the change-list, but not be able to edit nor view the change_form after the model instance is saved.

def has_change_permission(self, request, obj = None, **kwargs):
    if obj is None:
        return True
    else:
        return False

Though am thinking this might also override any MODEL_can_change permissions allowing unwanted eyes from viewing the change-list?

👤JWL

-1đź‘Ť

According to my test on Django 1.8 we can not use following as noted on answer #3 but it works on Django 1.4:

##     self.get_formset(request, obj)      ##
answer 3 needs fix. Generally, alternative codes for this issue about below section 
##          form = self.get_formset(request, obj).form    ##
##          fields = form.base_fields.keys()              ##

Can be something like:

#~ (A) or
[f.name for f in self.model._meta.fields]

#~ (B) or
MyModel._meta.get_all_field_names()

#~ (C) 
list(set([field.name for field in self.opts.local_fields] +
                        [field.name for field in self.opts.local_many_to_many]         
  ))

Leave a comment