Skip to content
This repository has been archived by the owner on Jan 26, 2021. It is now read-only.

Add view decorators for checking membership, authorship and admin rights #16

Merged
merged 1 commit into from
Jul 11, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
Sphinx==1.2.2
django-nose==1.2
flake8==2.1.0
mock==1.0.1
121 changes: 121 additions & 0 deletions systers_portal/dashboard/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from django.utils.decorators import available_attrs
from django.utils.functional import wraps

from dashboard.models import Community


def membership_required(model, *lookup_vars, **kwargs):
"""Decorator for views that checks that the user is a member of the
community by passing the Community model directly or a model which has a
reference to that community, i.e. the model object belongs to that
community.

>>> from dashboard.models import Resource
>>> membership_required(Community, "id__exact", "id")
<function decorator at 0x...>
>>> membership_required(Resource, "id__exact", "id")
<function decorator at 0x...>

:param model: Community model or model that has a community field
referencing Community model
:param lookup_vars: string field lookup name and a string variable name
passed to the view
:returns: inner decorator function
:raises ValueError: if the value of the second element from request kwargs
is missing or None
"""

def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
lookup, varname = lookup_vars
value = kwargs.get(varname, None)
if value is None:
raise ValueError("The lookup value can't be 'None'.")
obj = get_object_or_404(model, **{lookup: value})
systeruser = request.user.systeruser
user_communities = systeruser.member_of_community.filter(
members=systeruser)
obj_community = obj if model == Community else obj.community
if obj_community in user_communities:
return view_func(request, *args, **kwargs)
else:
raise PermissionDenied
return _wrapped_view
return decorator


def admin_required(model, *lookup_vars, **kwargs):
"""Decorator for views that checks that the user is admin of the community
by passing the Community model directly or a model which has a reference to
that community, i.e. the model object belongs to that community.

>>> from dashboard.models import News
>>> admin_required(Community, "id__exact", "id")
<function decorator at 0x...>
>>> admin_required(News, "id__exact", "id")
<function decorator at 0x...>

:param model: Community model or model that has a community field
referencing Community model
:param lookup_vars: string field lookup name and a string variable name
passed to the view
:returns: inner decorator function
:raises ValueError: if the value of the second element from request kwargs
is missing or None
"""

def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
lookup, varname = lookup_vars
value = kwargs.get(varname, None)
if value is None:
raise ValueError("The lookup value can't be 'None'.")
obj = get_object_or_404(model, **{lookup: value})
systeruser = request.user.systeruser
obj_community = obj if model == Community else obj.community
community_admin = obj_community.community_admin
if community_admin == systeruser:
return view_func(request, *args, **kwargs)
else:
raise PermissionDenied
return _wrapped_view
return decorator


def authorship_required(model, *lookup_vars, **kwargs):
"""Decorator for views that checks that the user is the author of the model.

>>> from dashboard.models import News, Resource
>>> authorship_required(News, "id__exact", "id")
<function decorator at 0x...>
>>> authorship_required(Resource, "id__exact", "id")
<function decorator at 0x...>

:param model: model object with author field referencing SysterUser model
:param lookup_vars: string field lookup name and a string variable name
passed to the view
:returns: inner decorator function
:raises ValueError: if the value of the second element from request kwargs
is missing or None
"""

def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
lookup, varname = lookup_vars
value = kwargs.get(varname, None)
if value is None:
raise ValueError("The lookup value can't be 'None'.")
obj = get_object_or_404(model, **{lookup: value})
systeruser = request.user.systeruser
obj_author = obj.author
if obj_author == systeruser:
return view_func(request, *args, **kwargs)
else:
raise PermissionDenied
return _wrapped_view
return decorator
56 changes: 55 additions & 1 deletion systers_portal/dashboard/tests.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import mock
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for using mock :)


from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.test import TestCase
from django.contrib.auth.models import Group
from cms.models.pagemodel import Page
from cms.api import create_page

from dashboard.decorators import (membership_required, admin_required,
authorship_required)
from dashboard.management import (content_contributor_permissions,
content_manager_permissions,
user_content_manager_permissions,
Expand All @@ -12,7 +17,7 @@
ResourceType, CommunityPage)


class DashboardTestCase(TestCase):
class DashboardModelsTestCase(TestCase):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed theDashboardTestCase class to a more specific name. I assume we will have DashboardFormsTestCase, DashboardViewsTestCase and all of them will require different setup methods.

If the idea is bad and we want to keep all the tests in one class, I will revert this change.


def setUp(self):
self.auth_user = User.objects.create(username='foo', password='foobar')
Expand Down Expand Up @@ -208,3 +213,52 @@ def test_group_permissions(self):
group_permissions = [p.codename for p in
list(group.permissions.all())]
self.assertItemsEqual(group_permissions, permissions[i])


class DashboardDecoratorsTestCase(TestCase):
def setUp(self):
self.auth_user_foo = User.objects.create_user(username="foo",
password="foobar")
self.user_foo = SysterUser(user=self.auth_user_foo)
self.user_foo.save()
self.auth_user_bar = User.objects.create_user(username="bar",
password="foobar")
self.user_bar = SysterUser(user=self.auth_user_bar)
self.user_bar.save()
self.community = Community(community_admin=self.user_foo)
self.community.save()
self.community.members.add(self.user_foo)
self.community.save()
self.resource = Resource(community=self.community,
author=self.user_foo)
self.resource.save()

def test_decorators(self):
request = mock.MagicMock()
view = mock.MagicMock(return_value='foo response')

test_objects = [self.community, self.resource, ]
mockup_tests = [
{"user": self.auth_user_foo, "success": True},
{"user": self.auth_user_bar, "success": False},
]
tests = []
for obj in test_objects:
for mockup_test in mockup_tests:
tests.append(mockup_test)
tests[-1]["model"] = type(obj)
tests[-1]["object"] = obj
decorators = [membership_required, admin_required, authorship_required]
for decorator in decorators:
for test in tests:
request.user = test["user"]
request_kwargs = {"id": test["object"].id}
decorated = decorator(test["model"], "id__exact", "id")
wrapped = decorated(view)

if test["success"]:
response = wrapped(request, **request_kwargs)
self.assertEqual(response, view.return_value)
else:
self.assertRaises(PermissionDenied, wrapped, request,
**request_kwargs)