From ab778191c65cae481923f9c65749585fd072ebdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20M=C3=BCllegger?= Date: Fri, 24 May 2013 01:02:08 +0200 Subject: [PATCH] Adding the ability to check for permissions in templates. --- djadmin2/constants.py | 7 +- djadmin2/permissions.py | 60 ++++++++++ .../admin2/bootstrap/model_list.html | 17 ++- djadmin2/templatetags/admin2_tags.py | 16 +++ djadmin2/viewmixins.py | 10 ++ example/blog/tests/__init__.py | 3 +- example/blog/tests/test_permissions.py | 104 ++++++++++++++++++ 7 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 example/blog/tests/test_permissions.py diff --git a/djadmin2/constants.py b/djadmin2/constants.py index 11021b0..3951312 100644 --- a/djadmin2/constants.py +++ b/djadmin2/constants.py @@ -1,9 +1,8 @@ from django.conf import settings MODEL_ADMIN_ATTRS = ( - 'list_display', 'list_display_links', 'list_filter', - 'admin', 'has_permission', 'has_add_permission', - 'has_edit_permission', 'has_delete_permission', 'get_actions' - ) + 'list_display', 'list_display_links', 'list_filter', 'admin', + 'index_view', 'detail_view', 'create_view', 'update_view', 'delete_view', + 'get_default_view_kwargs', 'get_actions') ADMIN2_THEME_DIRECTORY = getattr(settings, "ADMIN2_THEME_DIRECTORY", "admin2/bootstrap") diff --git a/djadmin2/permissions.py b/djadmin2/permissions.py index 9e31279..f6322e2 100644 --- a/djadmin2/permissions.py +++ b/djadmin2/permissions.py @@ -14,6 +14,7 @@ interface: The permission classes are then just fancy wrappers of these basic checks of which it can hold multiple. ''' +import re def is_authenticated(request, view, obj=None): @@ -119,3 +120,62 @@ class ModelChangePermission(AdminPermission): class ModelDeletePermission(AdminPermission): permissions = (model_permission('%(app_label)s.delete_%(model_name)s'),) + + +class TemplatePermission(object): + do_not_call_in_templates = True + + def __init__(self, permission_check): + self._permission_check = permission_check + + def __nonzero__(self): + return self._permission_check() + + def __call__(self, obj=None): + return self._permission_check(obj) + + def __unicode__(self): + return unicode(bool(self)) + + +class TemplatePermissionChecker(object): + ''' + Can be used in the template like:: + + {{ permissions.has_view_permission }} + {{ permissions.has_add_permission }} + {{ permissions.has_change_permission }} + {{ permissions.has_delete_permission|for_object:object }} + + The attribute access of ``has_create_permission`` will be done via a + dictionary lookup (implemented in ``__getitem__``). This will return a + callable that can be passed in an object to check object-level + permissions. + ''' + has_named_permission_regex = re.compile('^has_(?P\w+)_permission$') + + view_name_mapping = { + 'view': 'detail_view', + 'add': 'create_view', + 'change': 'update_view', + 'delete': 'delete_view', + } + + def __init__(self, request, view): + self.request = request + self.view = view + + def get_permission_check(self, view_name): + def permission_check(obj=None): + return self.view.has_permission(obj, view_name=view_name) + return permission_check + + def __getitem__(self, key): + match = self.has_named_permission_regex.match(key) + if not match: + raise KeyError + 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 TemplatePermission(self.get_permission_check(view_name)) diff --git a/djadmin2/templates/admin2/bootstrap/model_list.html b/djadmin2/templates/admin2/bootstrap/model_list.html index 5e501b4..8b907fe 100644 --- a/djadmin2/templates/admin2/bootstrap/model_list.html +++ b/djadmin2/templates/admin2/bootstrap/model_list.html @@ -30,9 +30,9 @@ 0 of {{ object_list|length }} selected @@ -46,14 +46,13 @@ - {{ obj }} - {% trans "Detail" %} - {# if has_edit_permission #} + {{ obj }} {% trans "Detail" %} + {% if permissions.has_change_permission %} {% trans "Edit" %} - {# endif #} - {# if has_delete_permission #} + {% endif %} + {% if permissions.has_delete_permission %} {% trans "Delete" %} - {# endif #} + {% endif %} {% endfor %} diff --git a/djadmin2/templatetags/admin2_tags.py b/djadmin2/templatetags/admin2_tags.py index 4e342ff..07861cc 100644 --- a/djadmin2/templatetags/admin2_tags.py +++ b/djadmin2/templatetags/admin2_tags.py @@ -43,3 +43,19 @@ def formset_visible_fieldlist(formset): Returns the labels of a formset's visible fields as an array. """ return [f.label for f in formset.forms[0].visible_fields()] + + +@register.filter +def for_object(callable, 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']) + """ + if callable == '': + return callable + return callable(obj) diff --git a/djadmin2/viewmixins.py b/djadmin2/viewmixins.py index 3f381d6..556a32b 100644 --- a/djadmin2/viewmixins.py +++ b/djadmin2/viewmixins.py @@ -41,6 +41,16 @@ class PermissionMixin(object): return False return True + def get_context_data(self, **kwargs): + context = super(PermissionMixin, self).get_context_data(**kwargs) + permission_checker = permissions.TemplatePermissionChecker( + self.request, + self) + context.update({ + 'permissions': permission_checker, + }) + return context + class Admin2Mixin(PermissionMixin): # are set in the ModelAdmin2 class when creating the view via diff --git a/example/blog/tests/__init__.py b/example/blog/tests/__init__.py index 09b4aa2..8df4500 100644 --- a/example/blog/tests/__init__.py +++ b/example/blog/tests/__init__.py @@ -1,5 +1,6 @@ from test_auth_admin import * from test_apiviews import * from test_builtin_api_resources import * -from test_views import * +from test_permissions import * from test_modelforms import * +from test_views import * diff --git a/example/blog/tests/test_permissions.py b/example/blog/tests/test_permissions.py new file mode 100644 index 0000000..62a851f --- /dev/null +++ b/example/blog/tests/test_permissions.py @@ -0,0 +1,104 @@ +from django.contrib.auth.models import User, Permission +from django.core.urlresolvers import reverse +from django.template import Template, Context +from django.test import TestCase +from django.test.client import RequestFactory + + +import djadmin2 +from djadmin2.models import ModelAdmin2 +from djadmin2.permissions import TemplatePermissionChecker +from blog.models import Post + + +class TemplatePermissionTest(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.user = User( + username='admin', + is_staff=True) + self.user.set_password('admin') + self.user.save() + + def render(self, template, context): + template = Template(template) + context = Context(context) + return template.render(context) + + def test_permission_wrapper(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) + context = { + 'permissions': permissions, + } + + result = self.render( + '{{ permissions.has_unvalid_permission }}', + context) + self.assertEqual(result, '') + + result = self.render('{{ permissions.has_add_permission }}', context) + self.assertEqual(result, 'False') + + 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 + + permissions['has_add_permission']() + result = self.render('{{ permissions.has_add_permission }}', context) + self.assertEqual(result, 'True') + + 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) + + post = Post.objects.create(title='Hello', body='world') + context = { + 'post': post, + 'permissions': permissions, + } + + result = self.render( + '{% load admin2_tags %}' + '{{ permissions.has_unvalid_permission|for_object:post }}', + context) + self.assertEqual(result, '') + + result = self.render( + '{% load admin2_tags %}' + '{{ permissions.has_add_permission|for_object:post }}', + context) + self.assertEqual(result, 'False') + + 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 + + # 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|for_object:post }}', + context) + self.assertEqual(result, 'False')