From c6a11efca81f25cd525de97ca0dd1d9ecf6b5120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20M=C3=BCllegger?= Date: Sat, 25 May 2013 20:10:21 +0200 Subject: [PATCH] Adding ability to check for permissions of any registered model admin in the templates. --- djadmin2/permissions.py | 70 +++++++++++++++++++------- djadmin2/viewmixins.py | 12 +---- example/blog/tests/test_permissions.py | 37 ++++++++++++++ 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/djadmin2/permissions.py b/djadmin2/permissions.py index c877e1c..d94b03d 100644 --- a/djadmin2/permissions.py +++ b/djadmin2/permissions.py @@ -162,16 +162,22 @@ class ModelDeletePermission(BasePermission): 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, permission_check): - self._permission_check = permission_check + def __init__(self, view): + self._view = view def __nonzero__(self): - return self._permission_check() + return self._view.has_permission() def __call__(self, obj=None): - return self._permission_check(obj) + return self._view.has_permission(obj) def __unicode__(self): return unicode(bool(self)) @@ -185,11 +191,24 @@ class TemplatePermissionChecker(object): {{ permissions.has_add_permission }} {{ permissions.has_change_permission }} {{ permissions.has_delete_permission|for_object:object }} + {{ permissions.blog_post.has_view_permission }} + {{ permissions.blog_comment.has_add_permission }} + + So in general:: + + {{ permissions.has__permission }} + {{ permissions..has__permission }} 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. + callable (instance of ``TemplatePermission``, that can take an object to + check object-level permissions. + + In the future any view assigned to the admin will be possible to check for + permissions, like with + ``{{ permissions.auth_user.has_change_password_permission }}``. But this + 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$') @@ -200,21 +219,38 @@ class TemplatePermissionChecker(object): 'delete': 'delete_view', } - def __init__(self, request, view): + def __init__(self, request, view, model_admin=None): self.request = request self.view = view + self.model_admin = model_admin - 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 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 + + view_class = getattr(model_admin, view_name) + view = view_class( + request=self.request, + **model_admin.get_default_view_kwargs()) + return TemplatePermission(view) def __getitem__(self, key): match = self.has_named_permission_regex.match(key) - if not match: + 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) + # 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 + model_admin = admin_site.get_admin_by_name(key) + except ValueError: 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)) + return self.__class__(self.request, self.view, model_admin) diff --git a/djadmin2/viewmixins.py b/djadmin2/viewmixins.py index 556a32b..e7e6e0f 100644 --- a/djadmin2/viewmixins.py +++ b/djadmin2/viewmixins.py @@ -20,22 +20,12 @@ class PermissionMixin(object): for permission_class in self.permission_classes] super(PermissionMixin, self).__init__(**kwargs) - def has_permission(self, obj=None, view_name=None): + def has_permission(self, obj=None): ''' Return ``True`` if the permission for this view shall be granted, ``False`` otherwise. Supports object-level permission by passing the related object as first argument. - - You can also check for the permission of a different view in the same - ``ModelAdmin2`` by passing in the attribute name which holds the view - in the ``ModelAdmin2`` instance. ''' - if view_name: - view_class = getattr(self.model_admin, view_name) - view = view_class( - request=self.request, - **self.model_admin.get_default_view_kwargs()) - return view.has_permission(obj) for backend in self.permissions: if not backend.has_permission(self.request, self, obj): return False diff --git a/example/blog/tests/test_permissions.py b/example/blog/tests/test_permissions.py index 62a851f..1323410 100644 --- a/example/blog/tests/test_permissions.py +++ b/example/blog/tests/test_permissions.py @@ -58,6 +58,43 @@ class TemplatePermissionTest(TestCase): result = self.render('{{ permissions.has_add_permission }}', context) self.assertEqual(result, 'True') + def test_admin_traversal_by_name(self): + 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) + + 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_add_permission }}', context) + self.assertEqual(result, 'True') + result = self.render('{{ permissions.blog_post.has_add_permission }}', context) + self.assertEqual(result, 'True') + result = self.render('{{ permissions.blog_post.has_change_permission }}', context) + self.assertEqual(result, 'False') + result = self.render('{{ permissions.auth_user.has_delete_permission }}', context) + self.assertEqual(result, 'False') + + result = self.render( + '{{ permissions.unknown_app.has_add_permission }}', + context) + self.assertEqual(result, '') + + result = self.render( + '{{ permissions.blog_post.has_unvalid_permission }}', + context) + self.assertEqual(result, '') + def test_object_level_permission(self): model_admin = ModelAdmin2(Post, djadmin2.default) request = self.factory.get(reverse('admin2:blog_post_index'))