The Django Admin, especially it's dealing w/ change
permission, is a bit coarse-grained. The internal method ModelAdmin.has_change_permission()
covers the checking of view
permission which is lacked actually.
Second, the GuardedModelAdmin
brings ownership-checking (through user_can_access_owned_objects_only
) and forms for managing row-level permissions. It does not provide any other row-level accessing policy to the Django Admin.
For the typical row-level permission scene in the Django Admin, I would like to suggest the code, here I've introduced an optional 'view' permission:
class ExtendedGuardedModelAdmin(GuardedModelAdmin):
def queryset(self, request):
qs = super(ExtendedGuardedModelAdmin, self).queryset(request)
# Check global permission
if super(ExtendedGuardedModelAdmin, self).has_change_permission(request) \
or (not self.list_editable and self.has_view_permission(request)):
return qs
# No global, filter by row-level permissions. also use view permission if the changelist is not editable
if self.list_editable:
return get_objects_for_user(request.user, [self.opts.get_change_permission()], qs)
else:
return get_objects_for_user(request.user, [self.opts.get_change_permission(), self.get_view_permission(
)], qs, any_perm=True)
def has_change_permission(self, request, obj=None):
if super(ExtendedGuardedModelAdmin, self).has_change_permission(request, obj):
return True
if obj is None:
# Here check global 'view' permission or if there is any changeable items
return self.has_view_permission(request) or self.queryset(request).exists()
else:
# Row-level checking
return request.user.has_perm(self.opts.get_change_permission(), obj)
def get_view_permission(self):
return 'view_%s' % self.opts.object_name.lower()
def has_view_permission(self, request, obj=None):
return request.user.has_perm(self.opts.app_label + '.' + self.get_view_permission(), obj)
def has_delete_permission(self, request, obj=None):
return super(ExtendedGuardedModelAdmin, self).has_delete_permission(request, obj) \
or (obj is not None and request.user.has_perm(self.opts.get_delete_permission(), obj))
In this way, you could achieve more-flexible permission checking, user-permissions now are global, user-obj-permissions are row-level based:
joe.user_permissions.add(add_task)
joe could add new tasks (there is no row-level 'add' permission)
joe.user_permissions.add(change_task)
joe could change ALL of the tasks
joe.user_permissions.add(delete_task)
joe could delete ALL of the tasks
assign(Task._meta.get_change_permission(), joe, obj)
joe could change the Task obj, see the changelist containing the obj as well as other changeable tasks.
assign(Task._meta.get_delete_permission(), joe, obj)
joe could delete the Task obj
assign('view_task', joe, obj)
[optional] joe could view the Task obj(you might want to check this permission at customized Admin view page)
joe.user_permissions.add(Permission.objects.get(codename='view_task', ...))
[optional] joe could view All the tasks in the changelist, as long as the changelist is not inline-editable. This is useful if joe could pick up items from raw_id_fields w/o having change permission.
- ...
You could ignore 'view' permission safely if it's useless for you.
Currently, django.contrib.admin.util.get_deleted_objects
does not honor the obj during the checking of permission, if you need to check row-level permission during delete, patch the get_deleted_objects
by modifying the if not user.has_perm(p):
line to if not user.has_perm(p, obj):
. The relative tickets are 13539 & 16862