Login

FieldLevelPermissionsAdmin

Author:
buriy
Posted:
September 11, 2007
Language:
Python
Version:
.96
Score:
5 (after 5 ratings)

Have you ever needed to customize permissions, for example, allow only some fields for editing by some group of users, display some fields as read-only, and some to hide completely? FieldLevelPermissionsAdmin class does this for newforms-admin branch. Not tested well yet (>100 LOC!).

You typically would like to use it this way:

class MyObjectAdmin(FieldLevelPermissionsAdmin):

    def can_view_field(self, request, object, field_name):
        """
        Boolean method, returning True if user allowed to view
        field with name field_name.
        user is stored in the request object,
        object is None only if object does not exist yet
        """
        ...your code...

    def can_change_field(self, request, object, field_name):
        """
        Boolean method, returning True if user allowed to
        change field with name field_name.
        user is stored in the request object,
        object is None only if object does not exist yet
        """
        ...your code...

    def queryset(self, request):
        """ 
        Method of ModelAdmin, override it if you want to change
        list of objects visible by the current user.
        """
        mgr = self.model._default_manager
        if request.user.is_superuser:
            return mgr.all()
        filters = Q(creator=request.user)|Q(owner=request.user)
        return mgr.filter(filters)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
from django import newforms as forms
from django.utils.encoding import force_unicode
from django.contrib.admin.options import ModelAdmin
from django.db.models.fields import NOT_PROVIDED
from decimal import Decimal
from django.template.defaultfilters import linebreaksbr
from django.template.defaultfilters import truncatewords_html

DEBUG = False

class ReadOnlyTextWidget(forms.widgets.Widget):
    """
    This is improved DefaultValueWidget
    from http://www.djangosnippets.org/snippets/323/
    """
    def __init__(self, value, display=None, attrs=None, fk=False):
        #if isinstance(display.widget, label):
        #raise Exception(display.widget.__dict__)
        if DEBUG: print '[i]', display, value
        # this allows to genericly pass in any field object intending to
        # catch ModelChoiceFields, without having to care about the actual
        # type.
        if isinstance(display, forms.Field):
            self.display = display
        else:
            self.display = None
        self.value = value
        super(ReadOnlyTextWidget, self).__init__(attrs)

    #def _has_changed(self, initial, data):
        #print '[H]', initial, data
        #return False

    def value_from_datadict(self, data, files, name):
        value = data.get(name, self.value)
        if DEBUG: print "[d] value for %s = %s" % (name, value)
        #HACK: Decimal can't be converted to Decimal again
        # so returning str instead
        if type(value) is Decimal: return str(value)
        if isinstance(value, forms.Field): return str(value)
        return value

    def render(self, name, value, attrs=None):
        if DEBUG: print "[r] value for %s = %s" % (name, value)
        if isinstance(self.display, forms.ModelChoiceField):
            try:
                value = self.display.queryset.get(pk=value)
            except:
                value = value
        if isinstance(value, list):
            r = ",".join(map(unicode, self.value))
        else:
            r = value
        s = force_unicode(r, strings_only=False)
        return truncatewords_html(linebreaksbr(s), 50)

class ReadOnlyTextWidget(forms.widgets.Widget):
    """
    This is improved DefaultValueWidget
    from http://www.djangosnippets.org/snippets/323/
    """
    def __init__(self, value, display=None, attrs=None, fk=False):
        #if isinstance(display.widget, label):
        #raise Exception(display.widget.__dict__)
        if isinstance(display, forms.ModelChoiceField):
            try:
                self.display = display.queryset.get(pk=value)
            except:
                self.display = value
        # this allows to genericly pass in any field object intending to
        # catch ModelChoiceFields, without having to care about the actual
        # type.
        elif isinstance(display, forms.Field):
            self.display = None
        else:
            self.display = display
        self.value = value
        super(ReadOnlyTextWidget, self).__init__(attrs)

    def value_from_datadict(self, data, files, name):
        value = data.get(name, self.value)
        if DEBUG: print "[d] value for %s = %s" % (name, value)
        #HACK: Decimal can't be converted to Decimal again
        # so returning str instead
        if type(value) is Decimal: return str(value)
        if isinstance(value, forms.Field): return str(value)
        return value

    def render(self, name, value, attrs=None):
        if self.display is None:
            r = self.value
        elif isinstance(self.display, list):
            r = ",".join(map(unicode, self.display))
        else:
            r = self.display
        s = force_unicode(r, strings_only=False)
        return truncatewords_html(linebreaksbr(s), 50)

class FieldLevelPermissionsAdmin(ModelAdmin):
    def get_fieldsets(self, request, obj):
        "Hook for specifying fieldsets for the add form."
        if self.declared_fieldsets:
            fieldsets = self.declared_fieldsets
        else:
            form = self.get_form(request, obj)
            fieldsets = [(None, {'fields': form.base_fields.keys()})]
        for fs in fieldsets:
            fs[1]['fields'] = [f for f in fs[1]['fields'] if self.can_view_field(request, obj, f)]
        return fieldsets

    def get_form(self, request, obj=None):
        superclass = super(RowLevelPermissionsAdmin, self)
        formclass = superclass.get_form(request, obj)
        for name, field in formclass.base_fields.items():
            if request.method == 'POST' and not self.can_change_field(request, obj, name):
                del formclass.base_fields[name]
            elif not self.can_view_field(request, obj, name):
                del formclass.base_fields[name]
        return formclass

    #XXX: To be overriden in child
    def can_view_field(self, request, object, field_name):
        """
        Boolean method, returning True if user allowed to view
        field with name field_name.
        user is stored in the request object,
        object is None only if object does not exist yet
        """
        if request.user.is_superuser:
            return True
        if object is None:
            return request.user.has_add_permission(request)
        else:
            return request.user.has_change_permission(request, object)

    #XXX: To be overriden in child
    def can_change_field(self, request, object, field_name):
        """
        Boolean method, returning True if user allowed to
        change field with name field_name.
        user is stored in the request object,
        object is None only if object does not exist yet
        """
        if self.can_view_field(request, object, field_name):
            return True
        return False

    def formfield_for_dbfield(self, db_field, **kwargs):
        superclass = super(RowLevelPermissionsAdmin, self)
        field = superclass.formfield_for_dbfield(db_field, **kwargs)
        if not field: return None
        default_value = kwargs.get('initial', db_field.default)
        if default_value is NOT_PROVIDED:
            default_value = None
        if not self.can_view_field(self._request, self._object, db_field.name):
            #XXX: Not displayed, but used when default value is provided
            #if default_value:
            field.widget = ReadOnlyTextWidget(default_value, display=field, fk=True)
            #else:
            #    return None
            #return None
        elif not self.can_change_field(self._request, self._object, db_field.name):
            #XXX: Displaying as text
            #return None
            field.widget = ReadOnlyTextWidget(default_value, display=field)
        return field

    def has_add_permission(self, request):
        perm_name = self.opts.app_label + '.' + self.opts.get_add_permission()
        return request.user.has_perm(perm_name)

    def has_change_permission(self, request, obj=None):
        perm_name = self.opts.app_label + '.' + self.opts.get_change_permission()
        if not request.user.has_perm(perm_name): return False
        if obj:
            #TODO: Remove this extra query
            if not self.queryset(request).filter(pk=obj.id).count():
                return False
        return True

    def changelist_view(self, request):
        # useful for list_display customization
        self._action = 'changelist'
        self._request = request
        self._object = None
        superclass = super(RowLevelPermissionsAdmin, self)
        return superclass.changelist_view(request)

    def change_view(self, request, object_id):
        # assignments are required for permission checks later
        self._action = 'change'
        model = self.model
        try:
            #TODO: Get rid of this query (lines copied from parent)
            obj = model._default_manager.get(pk=object_id)
        except model.DoesNotExist:
            # Don't raise Http404 just yet, because we haven't checked
            # permissions yet. We don't want an unauthenticated user to be able
            # to determine whether a given object exists.
            obj = None
        self._object = obj
        self._request = request
        superclass = super(RowLevelPermissionsAdmin, self)
        return superclass.change_view(request, object_id)

    def add_view(self, request):
        # assignments are required for permission checks later
        self._action = 'add'
        self._object = None
        self._request = request
        superclass = super(RowLevelPermissionsAdmin, self)
        return superclass.add_view(request)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 8 months ago

Comments

Archatas (on September 12, 2007):

Actually, what you have implemented is field-level permission management. Row-level permissions deal with specific records in the database, but not with the fields of all records.

#

buriy (on September 12, 2007):

Oops, actually, you are right, and I meant that really. Changed.

#

buriy (on September 28, 2007):

Fixed to be compatible with trunk

#

Dimus (on May 5, 2008):

Is there any solution to make the same field behavoir on default Django-admin (not newforms-admin)?

#

tomZ (on September 27, 2008):

I am unable to get this running on Django 1.0.

'MyAdmin' object has no attribute '_request'

#

buriy (on October 5, 2008):

Ehmmm... snippet was truncated.... fixed.

Can't tell if it's working now as I don't use this code anymore. It was from last version, 1.0b2-compatible

#

mikeamy (on October 9, 2008):

Is there a potential race condition with this?

The code is setting temporary properties on the admin instance. Say some ordinary user logs in and wants to view an admin page. The code sets the temporary variables on their FieldLevelPermissionsAdmin so that, for example, they can't change anything.

Simultaneously, a superuser views the page. The code sets their temporary variables on the same FieldLevelPermissionsAdmin, so that they can see and change everything. This happens before the code for the first user gets to choose formfields.

So what happens is that both users will get superuser access - ie both get to see and change all the fields.

It wouldn't happen often, but that just makes it harder to debug.

Or did I miss something?

#

mikeamy (on October 9, 2008):

BTW the variables are _action, _object and _request.

really the formfield_for_dbfield and a bunch of other methods should pass the request around. But that would take a refactoring of the django admin.

#

Please login first before commenting.