diff --git a/djadmin2/permissions.py b/djadmin2/permissions.py index d94b03d..348d796 100644 --- a/djadmin2/permissions.py +++ b/djadmin2/permissions.py @@ -161,44 +161,36 @@ class ModelDeletePermission(BasePermission): permissions = (model_permission('{app_label}.delete_{model_name}'),) -class TemplatePermission(object): - ''' - A small wrapper around the permission check of a specific view. This is - used in the template since we don't know if the permission will be used - as a boolean expression or if the user will append a ``for_object`` filter - to test for object level permission. - ''' - do_not_call_in_templates = True - - def __init__(self, view): - self._view = view - - def __nonzero__(self): - return self._view.has_permission() - - def __call__(self, obj=None): - return self._view.has_permission(obj) - - def __unicode__(self): - return unicode(bool(self)) - - class TemplatePermissionChecker(object): ''' - Can be used in the template like:: + Can be used in the template like: + + .. code-block:: html+django {{ permissions.has_view_permission }} {{ permissions.has_add_permission }} {{ permissions.has_change_permission }} - {{ permissions.has_delete_permission|for_object:object }} + {{ permissions.has_delete_permission }} {{ permissions.blog_post.has_view_permission }} {{ permissions.blog_comment.has_add_permission }} - So in general:: + So in general: + + .. code-block:: html+django {{ permissions.has__permission }} {{ permissions..has__permission }} + And using object-level permissions: + + .. code-block:: html+django + + {% load admin2_tags %} + {{ permissions.has_delete_permission|for_object:object }} + {% with permissions|for_object:object as object_permissions %} + {{ object_permissions.has_delete_permission }} + {% endwith %} + The attribute access of ``has_create_permission`` will be done via a dictionary lookup (implemented in ``__getitem__``). This will return a callable (instance of ``TemplatePermission``, that can take an object to @@ -210,7 +202,7 @@ class TemplatePermissionChecker(object): needs an interface beeing implemented like suggested in: https://github.com/twoscoops/django-admin2/issues/142 ''' - has_named_permission_regex = re.compile('^has_(?P\w+)_permission$') + _has_named_permission_regex = re.compile('^has_(?P\w+)_permission$') view_name_mapping = { 'view': 'detail_view', @@ -219,38 +211,79 @@ class TemplatePermissionChecker(object): 'delete': 'delete_view', } - def __init__(self, request, view, model_admin=None): - self.request = request - self.view = view - self.model_admin = model_admin + def __init__(self, request, model_admin, view=None, obj=None): + self._request = request + self._model_admin = model_admin + self._view = view + self._obj = obj - def get_template_permission_object(self, view_name): - if self.model_admin is None: - model_admin = self.view.model_admin - else: - model_admin = self.model_admin + def clone(self): + return self.__class__( + request=self._request, + model_admin=self._model_admin, + view=self._view, + obj=self._obj) + def bind_admin(self, admin): + ''' + Return a clone of the permission wrapper with a new model_admin bind + to it. + ''' + new_permissions = self.clone() + new_permissions._model_admin = admin + return new_permissions + + def bind_object(self, obj): + ''' + Return a clone of the permission wrapper with a new object bind + to it for object-level permissions. + ''' + new_permissions = self.clone() + new_permissions._obj = obj + return new_permissions + + def get_view_by_name(self, view_name): + view_name = self.view_name_mapping[view_name] + model_admin = self._model_admin view_class = getattr(model_admin, view_name) view = view_class( - request=self.request, + request=self._request, **model_admin.get_default_view_kwargs()) - return TemplatePermission(view) + return view + + ######################################### + # interface exposed to the template users def __getitem__(self, key): - match = self.has_named_permission_regex.match(key) + match = self._has_named_permission_regex.match(key) if match: # the key was a has_*_permission, so get the *has permission # wrapper* view_name = match.groupdict()['name'] if view_name not in self.view_name_mapping: raise KeyError - view_name = self.view_name_mapping[view_name] - return self.get_template_permission_object(view_name) + view = self.get_view_by_name(view_name) + return self.bind_view(view) # the name might be a named object admin. So get that one and try to # check the permission there for further traversal try: - admin_site = self.view.model_admin.admin + admin_site = self._model_admin.admin model_admin = admin_site.get_admin_by_name(key) except ValueError: raise KeyError - return self.__class__(self.request, self.view, model_admin) + return self.bind_admin(model_admin) + + def __nonzero__(self): + # if no view is bound we will return false, since we don't know which + # permission to check we stay save in disallowing the access + if self._view is None: + return False + if self._obj is None: + return self._view.has_permission() + else: + return self._view.has_permission(self._obj) + + def __unicode__(self): + if self._view is None: + return '' + return unicode(bool(self)) diff --git a/djadmin2/templatetags/admin2_tags.py b/djadmin2/templatetags/admin2_tags.py index 07861cc..ac8fbbc 100644 --- a/djadmin2/templatetags/admin2_tags.py +++ b/djadmin2/templatetags/admin2_tags.py @@ -46,16 +46,26 @@ def formset_visible_fieldlist(formset): @register.filter -def for_object(callable, obj): +def for_object(permissions, obj): """ - Applies the provided argument ``obj`` to the piped value. Example:: - - {{ view.has_permission|for_object:object }} - - Translates roughly into the following python code:: - - context['view'].has_permission(context['object']) + Only useful in the permission handling. This filter binds a new object to + the permission handler to check for object-level permissions. """ - if callable == '': - return callable - return callable(obj) + # some permission check has failed earlier, so we don't bother trying to + # bind a new object to it. + if permissions == '': + return permissions + return permissions.bind_object(obj) + + +@register.filter +def for_admin(permissions, admin): + """ + Only useful in the permission handling. This filter binds a new admin to + the permission handler to allow checking views of an arbitrary admin. + """ + # some permission check has failed earlier, so we don't bother trying to + # bind a new admin to it. + if permissions == '': + return permissions + return permissions.bind_admin(admin) diff --git a/djadmin2/viewmixins.py b/djadmin2/viewmixins.py index e7e6e0f..e54e796 100644 --- a/djadmin2/viewmixins.py +++ b/djadmin2/viewmixins.py @@ -34,8 +34,7 @@ class PermissionMixin(object): def get_context_data(self, **kwargs): context = super(PermissionMixin, self).get_context_data(**kwargs) permission_checker = permissions.TemplatePermissionChecker( - self.request, - self) + self.request, self.model_admin) context.update({ 'permissions': permission_checker, }) diff --git a/example/blog/tests/test_permissions.py b/example/blog/tests/test_permissions.py index 1323410..6c19aa6 100644 --- a/example/blog/tests/test_permissions.py +++ b/example/blog/tests/test_permissions.py @@ -29,10 +29,7 @@ class TemplatePermissionTest(TestCase): model_admin = ModelAdmin2(Post, djadmin2.default) request = self.factory.get(reverse('admin2:blog_post_index')) request.user = self.user - view = model_admin.index_view( - request=request, - model_admin=model_admin) - permissions = TemplatePermissionChecker(request, view) + permissions = TemplatePermissionChecker(request, model_admin) context = { 'permissions': permissions, } @@ -54,7 +51,6 @@ class TemplatePermissionTest(TestCase): if hasattr(self.user, '_perm_cache'): del self.user._perm_cache - permissions['has_add_permission']() result = self.render('{{ permissions.has_add_permission }}', context) self.assertEqual(result, 'True') @@ -68,10 +64,7 @@ class TemplatePermissionTest(TestCase): model_admin = ModelAdmin2(Post, djadmin2.default) request = self.factory.get(reverse('admin2:blog_post_index')) request.user = self.user - view = model_admin.index_view( - request=request, - model_admin=model_admin) - permissions = TemplatePermissionChecker(request, view) + permissions = TemplatePermissionChecker(request, model_admin) context = { 'permissions': permissions, } @@ -95,14 +88,59 @@ class TemplatePermissionTest(TestCase): context) self.assertEqual(result, '') + def test_admin_binding(self): + user_admin = djadmin2.default.get_admin_by_name('auth_user') + post_admin = djadmin2.default.get_admin_by_name('blog_post') + request = self.factory.get(reverse('admin2:auth_user_index')) + request.user = self.user + permissions = TemplatePermissionChecker(request, user_admin) + + post = Post.objects.create(title='Hello', body='world') + context = { + 'post': post, + 'post_admin': post_admin, + 'permissions': permissions, + } + + result = self.render( + '{% load admin2_tags %}' + '{{ permissions|for_admin:post_admin }}', + context) + self.assertEqual(result, '') + + result = self.render( + '{% load admin2_tags %}' + '{{ permissions.has_add_permission }}' + '{% with permissions|for_admin:post_admin as permissions %}' + '{{ permissions.has_add_permission }}' + '{% endwith %}', + context) + self.assertEqual(result, 'FalseFalse') + + post_add_permission = Permission.objects.get( + content_type__app_label='blog', + content_type__model='post', + codename='add_post') + self.user.user_permissions.add(post_add_permission) + # invalidate the users permission cache + if hasattr(self.user, '_perm_cache'): + del self.user._perm_cache + + result = self.render( + '{% load admin2_tags %}' + '{{ permissions.has_add_permission }}' + '{% with permissions|for_admin:post_admin as permissions %}' + '{{ permissions.has_add_permission }}' + '{% endwith %}' + '{{ permissions.blog_post.has_add_permission }}', + context) + self.assertEqual(result, 'FalseTrueTrue') + def test_object_level_permission(self): model_admin = ModelAdmin2(Post, djadmin2.default) request = self.factory.get(reverse('admin2:blog_post_index')) request.user = self.user - view = model_admin.index_view( - request=request, - model_admin=model_admin) - permissions = TemplatePermissionChecker(request, view) + permissions = TemplatePermissionChecker(request, model_admin) post = Post.objects.create(title='Hello', body='world') context = { @@ -133,9 +171,19 @@ class TemplatePermissionTest(TestCase): # object level permission are not supported by default. So this will # return ``False``. - permissions['has_add_permission']() result = self.render( '{% load admin2_tags %}' + '{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission|for_object:post }}', context) - self.assertEqual(result, 'False') + self.assertEqual(result, 'TrueFalse') + + # binding an object and then checking for a specific view also works + result = self.render( + '{% load admin2_tags %}' + '{{ permissions.has_add_permission }}' + '{% with permissions|for_object:post as permissions %}' + '{{ permissions.has_add_permission }}' + '{% endwith %}', + context) + self.assertEqual(result, 'TrueFalse')