50

I'm trying to make all fields readonly without listing them explicitly.

Something like:

class CustomAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, request, obj=None):
        if request.user.is_superuser:
            return self.readonly_fields

        return self.fields

The problem is CustomAdmin.fields is not set at this point.

Any ideas?

yprez
  • 14,854
  • 11
  • 55
  • 70

11 Answers11

70

Since django 2.1, you can prevent editing, while allowing viewing, by returning False from the ModelAdmin's has_change_permission method, like this:

class CustomAdmin(admin.ModelAdmin):
    def has_change_permission(self, request, obj=None):
        return False

(This will not work before django 2.1, as it will also deny permission to any user trying only to view.)

Jeff Learman
  • 2,914
  • 1
  • 22
  • 31
  • 1
    Such a beautiful and nice solution. Why not more up-votes? – Erik Kalkoken Oct 03 '19 at 22:32
  • @ErikKalkoken: this works [only since version 2.1 of django](https://docs.djangoproject.com/en/2.1/releases/2.1/#model-view-permission) where the [view permission](https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.has_view_permission) was added. Before then (so up to and including django 2.0), the solution suggested by this answer will make the admin site deny permission to all users trying to view the relevant model in the admin. Agree that it's the neatest solution for recent django. – Jonathan Dec 03 '19 at 12:39
  • 1
    Great answer! For some applications, you might want to add `has_delete_permission` and `has_add_permission` methods too – andrewdotn Jan 21 '21 at 21:03
  • Not that inlines can't be added – krafter Dec 19 '21 at 13:17
44

Careful, self.model._meta.fields are not necessarily the same fields that CustomAdmin has!

"All fields of the Admin" would look more like this:

from django.contrib import admin
from django.contrib.admin.utils import flatten_fieldsets

class CustomAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, request, obj=None):
        if request.user.is_superuser:
            return self.readonly_fields

        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]
            ))
vishes_shell
  • 22,409
  • 6
  • 71
  • 81
Danny W. Adair
  • 12,498
  • 4
  • 43
  • 49
  • Thanks this works great. Just had to remove the many_to_many because it makes it look weird (they're registered as inlines anyway and have their own permissions. This feels much safer then getting `model._meta`. – yprez Dec 12 '12 at 07:42
  • 1
    No worries. Careful if you ever have an m2m that's not an inline. Though it should make itself known :-) – Danny W. Adair Dec 12 '12 at 10:20
  • Thank you. This is the most reasonable answer I found. – Greg Wang Mar 17 '14 at 04:28
  • Note, this won't work with a mix of readonly fields and custom fieldsets. – Cerin Feb 19 '15 at 18:58
  • What do you mean by custom fieldsets? If you declared fieldsets then all of the fields should be returned as readonly. – Danny W. Adair Feb 20 '15 at 01:19
  • 6
    `declared_fieldsets` is deprecated since django 1.7 and will be removed in django 1.9; replacing it with `get_fieldsets(req,obj)` reaches recursion limit. also, `admin.util` is renamed to `admin.utils` – Afriza N. Arief May 07 '15 at 09:28
  • Seeing as `declared_fieldsets` is deprecated and not available anymore in Django 1.7, what's a good approach to it nowadays? – Thor Dec 27 '16 at 14:32
  • 2
    This worked for me with Django 1.10 ` def get_readonly_fields(self, request, obj=None): if request.user.is_superuser: return self.readonly_fields return list(set( [field.name for field in self.opts.local_fields] + [field.name for field in self.opts.local_many_to_many] ))` – user2111922 Mar 18 '17 at 17:16
27

Ok, now there's this:

class CustomAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, request, obj=None):
        # ...

        return [f.name for f in self.model._meta.fields]

Still looking for a less ugly way.

yprez
  • 14,854
  • 11
  • 55
  • 70
12

You could iterate through the model meta fields:

def get_readonly_fields(self, request, obj=None):
    if obj:
        self.readonly_fields = [field.name for field in obj.__class__._meta.fields]
    return self.readonly_fields
Hedde van der Heide
  • 21,841
  • 13
  • 71
  • 100
6

For Inlines (Tab or Stack)

def get_readonly_fields(self, request, obj=None):
    fields = []
    for field in self.model._meta.get_all_field_names():
        if field != 'id':
            fields.append(field)
    return fields

def has_add_permission(self, request):
    return False
yprez
  • 14,854
  • 11
  • 55
  • 70
Ohad the Lad
  • 1,889
  • 1
  • 15
  • 24
  • return [field for field in self.model._meta.get_all_field_names() if field != 'id'] ### thanks YPREZ – Ohad the Lad Feb 28 '18 at 07:47
  • 2
    The `self.model._meta.get_all_field_names()` methodology has been deprecated in Django 1.10+: https://docs.djangoproject.com/en/2.1/ref/models/meta/#migrating-from-the-old-api –  Dec 23 '19 at 14:25
4

This worked for me with Django 1.10

def get_readonly_fields(self, request, obj=None):
    if request.user.is_superuser:
        return self.readonly_fields

    return list(set(
        [field.name for field in self.opts.local_fields] +
        [field.name for field in self.opts.local_many_to_many]
    ))
user2111922
  • 891
  • 9
  • 9
4

If someone still lookign for better way, you can use it like this:

@admin.register(ClassName)
class ClassNameAdmin(admin.ModelAdmin):
    readonly_fields = [field.name for field in ClassName._meta.fields]

ClassName is your Model class.

B. Okba
  • 1,021
  • 12
  • 16
0

My requirement was similar . I needed only one field to be shown as read-only . And this worked fine:

class ChoiceInline(admin.TabularInline):
    model = Choice
    extra = 1
    fields = ['choice_text', 'votes']
    readonly_fields = ['votes']

class QuestionAdmin(admin.ModelAdmin):
    #fields = ['pub_date', 'question_text']
    fieldsets = [
        (None, {'fields': ['question_text']}),
        ('Date Information', {'fields': ['pub_date']}),
    ]
    search_fields = ['question_text']


    inlines = [ChoiceInline]

Refer: C:\Python27\Lib\site-packages\django\contrib\admin\options.py

Arindam Roychowdhury
  • 5,927
  • 5
  • 55
  • 63
0

Say you have defined user_mode as;

Admin, Customers and Staff

If you'd like to deny a staff (and of course, customers) the privilege of deleting a customer, product, order etc...

Your code goes;

def get_readonly_fields(self, request: HttpRequest, obj=None):
    if request.user.user_mode != "Admin":
        return self.readonly_fields + ['user_mode']
    return super().get_readonly_fields(request, obj)

where user_mode = a model field holding the type of user.

N.B: my code uses "Pylance" (like typescript in JS)

Chukwunazaekpere
  • 906
  • 1
  • 6
  • 13
-1

With get_fieldsets you get all fields from the form

def get_readonly_fields(self, request, obj=None):
    readonly = []
    for fs in self.get_fieldsets(request, obj):
        if len(fs) > 1:
            readonly += fs[1].get('fields', [])
    return readonly
-1
@admin.register(Hero)
class HeroAdmin(admin.ModelAdmin, ExportCsvMixin):
    ...
    readonly_fields = ["father", "mother", "spouse"]

reference : https://books.agiliq.com/projects/django-admin-cookbook/en/latest/changeview_readonly.html

ali Saen
  • 311
  • 1
  • 3
  • 9