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
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 8 months ago
Comments
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.
#
Oops, actually, you are right, and I meant that really. Changed.
#
Fixed to be compatible with trunk
#
Is there any solution to make the same field behavoir on default Django-admin (not newforms-admin)?
#
I am unable to get this running on Django 1.0.
'MyAdmin' object has no attribute '_request'
#
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
#
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?
#
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.