FieldLevelPermissionsAdmin

  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. CustomQueryManager by zvoase 5 years, 9 months ago
  2. Dynamically insert or append a value to an admin option, e.g. list_display or list_filter by frankban 2 years, 8 months ago
  3. RequestFetchingMixin by eternicode 3 years, 2 months ago
  4. View Permission Decorator Helper by jgeewax 5 years, 9 months ago
  5. Memento by manelvf 3 years, 3 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.

#

(Forgotten your password?)