diff --git a/example/templates/flatpages/default.html b/example/templates/flatpages/default.html
index 0eafa3b..5276975 100644
--- a/example/templates/flatpages/default.html
+++ b/example/templates/flatpages/default.html
@@ -35,6 +35,17 @@
+
The permissions requested for this flatpage:
+
+ {% get_permission_requests flatpage as "all_perm_requests" %}
+ get_permission_requests flatpage as "all_perm_requests"
+ {% for perm_request in all_perm_requests %}
+ - {{ perm_request.user }}: {{ perm_request }} {% permission_request_approve_link perm_request %}{% permission_request_delete_link perm_request %}
+ {% endfor %}
+
+
+
+
Permission form for adding a specific permission "add_flatpage"
{% permission_form flatpage "flatpage_permission.add_flatpage" %}
@@ -47,6 +58,13 @@
{% add_url_for_obj flatpage %}
+Request a kind of access:
+{% permission_request_form flatpage %}
+
+
+Permission requuest form for adding a specific permission "add_flatpage"
+{% permission_request_form flatpage "flatpage_permission.add_flatpage" %}
+
Detailed tests
diff --git a/src/authority/forms.py b/src/authority/forms.py
index f756cb4..870dfda 100644
--- a/src/authority/forms.py
+++ b/src/authority/forms.py
@@ -2,6 +2,7 @@ from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User, Group
+from django.forms.util import ErrorList
from authority import permissions, get_choices_for
from authority.models import Permission
@@ -12,49 +13,76 @@ class BasePermissionForm(forms.ModelForm):
class Meta:
model = Permission
- def __init__(self, perm=None, obj=None, *args, **kwargs):
+ def __init__(self, perm=None, obj=None, approved=False, *args, **kwargs):
self.perm = perm
self.obj = obj
+ self.approved = approved
+ if not self.approved:
+ self.base_fields['user'].widget = forms.HiddenInput()
+ else:
+ self.base_fields['user'].widget = forms.TextInput()
if obj and perm:
self.base_fields['codename'].widget = forms.HiddenInput()
elif obj and not perm:
perm_choices = get_choices_for(self.obj)
- self.base_fields['codename'].widget = forms.Select(choices=perm_choices)
+ self.base_fields['codename'].widget = forms.Select(
+ choices=perm_choices)
super(BasePermissionForm, self).__init__(*args, **kwargs)
def save(self, request, commit=True, *args, **kwargs):
+ if not self.approved:
+ self.instance.user = request.user
self.instance.creator = request.user
self.instance.content_type = ContentType.objects.get_for_model(self.obj)
self.instance.object_id = self.obj.id
self.instance.codename = self.perm
+ self.instance.approved = self.approved
return super(BasePermissionForm, self).save(commit)
+
class UserPermissionForm(BasePermissionForm):
user = forms.CharField(label=_('User'))
class Meta(BasePermissionForm.Meta):
fields = ('user',)
- def clean_user(self):
- username = self.cleaned_data["user"]
- try:
- user = User.objects.get(username__iexact=username)
- except User.DoesNotExist:
- raise forms.ValidationError(
- _("A user with that username does not exist."))
- check = permissions.BasePermission(user=user)
- if check.has_perm(self.perm, self.obj):
+ def clean(self):
+ cleaned_data = self.cleaned_data
+ user = self.cleaned_data.get("user", None)
+ if user:
+ try:
+ user = User.objects.get(username__iexact=user)
+ except User.DoesNotExist:
+ raise forms.ValidationError(
+ _("A user with that username does not exist."))
+ check = permissions.BasePermission(user=user)
+ error_msg = None
if user.is_superuser:
- error_msg = _("The super user %(user)s already has the permission '%(perm)s' for %(object_name)s '%(obj)s'")
- else:
- error_msg = _("The user %(user)s already has the permission '%(perm)s' for %(object_name)s '%(obj)s'")
- raise forms.ValidationError(error_msg % {
- 'object_name': self.obj._meta.object_name.lower(),
- 'perm': self.perm,
- 'obj': self.obj,
- 'user': user,
- })
- return user
+ error_msg = _("The user %(user)s do not need to request " \
+ "access to any permission as it is a super user.")
+ elif check.has_perm(self.perm, self.obj):
+ error_msg = _("The user %(user)s already has the permission " \
+ "'%(perm)s' for %(object_name)s '%(obj)s'")
+ elif check.has_request(self.perm, self.obj):
+ error_msg = _("The user %(user)s already has a permission " \
+ "request '%(perm)s' for %(object_name)s '%(obj)s'")
+
+ if error_msg:
+ msg = error_msg % {
+ 'object_name': self.obj._meta.object_name.lower(),
+ 'perm': self.perm,
+ 'obj': self.obj,
+ 'user': user,
+ }
+ # Only display the error for the user field when it is not hidden
+ if self.approved:
+ self._errors["user"] = ErrorList([msg])
+ else:
+ raise forms.ValidationError(msg)
+ cleaned_data['user'] = user
+
+ return cleaned_data
+
class GroupPermissionForm(BasePermissionForm):
group = forms.CharField(label=_('Group'))
diff --git a/src/authority/managers.py b/src/authority/managers.py
index ab33710..218158a 100644
--- a/src/authority/managers.py
+++ b/src/authority/managers.py
@@ -10,10 +10,10 @@ class PermissionManager(models.Manager):
def get_for_model(self, obj):
return self.filter(content_type=self.get_content_type(obj))
- def for_object(self, obj):
+ def for_object(self, obj, approved=True):
return self.get_for_model(obj).select_related(
'user', 'creator', 'group', 'content_type'
- ).filter(object_id=obj.id)
+ ).filter(object_id=obj.id,approved=approved)
def for_user(self, user, obj, check_groups=True):
perms = self.get_for_model(obj)
@@ -22,15 +22,17 @@ class PermissionManager(models.Manager):
return perms.select_related('user', 'user__groups', 'creator').filter(
Q(user=user) | Q(group__in=user.groups.all()))
- def user_permissions(self, user, perm, obj, check_groups=True):
- return self.for_user(user, obj, check_groups).filter(codename=perm)
+ def user_permissions(self, user, perm, obj, approved=True, check_groups=True):
+ return self.for_user(user, obj, check_groups).filter(codename=perm,
+ approved=approved)
- def group_permissions(self, group, perm, obj):
+ def group_permissions(self, group, perm, obj, approved=True):
"""
Get objects that have Group perm permission on
"""
return self.get_for_model(obj).select_related(
- 'user', 'group', 'creator').filter(group=group, codename=perm)
+ 'user', 'group', 'creator').filter(group=group, codename=perm,
+ approved=approved)
def delete_objects_permissions(self, obj):
"""
@@ -48,3 +50,5 @@ class PermissionManager(models.Manager):
return
perms = self.user_permissions(user, perm, obj).filter(object_id=obj.id)
perms.delete()
+
+
\ No newline at end of file
diff --git a/src/authority/models.py b/src/authority/models.py
index 4b943e6..55e5cbc 100644
--- a/src/authority/models.py
+++ b/src/authority/models.py
@@ -21,15 +21,29 @@ class Permission(models.Model):
group = models.ForeignKey(Group, null=True, blank=True)
creator = models.ForeignKey(User, null=True, blank=True, related_name='created_permissions')
+ approved = models.BooleanField(default=False)
+
objects = PermissionManager()
def __unicode__(self):
return self.codename
class Meta:
+ unique_together = ("codename", "object_id", "content_type", "user", "group")
verbose_name = _('permission')
verbose_name_plural = _('permissions')
permissions = (
('change_foreign_permissions', 'Can change foreign permissions'),
('delete_foreign_permissions', 'Can delete foreign permissions'),
+ ('approve_permission_request', 'Can approve permission requests'),
)
+
+
+ def approve_perm_request(self, creator):
+ """
+ Approve granular permission request setting a Permission entry as
+ approved=True for a specific action from an user on an object instance.
+ """
+ self.approved=True
+ self.creator=creator
+ self.save()
diff --git a/src/authority/permissions.py b/src/authority/permissions.py
index c4f22f8..544bdc8 100644
--- a/src/authority/permissions.py
+++ b/src/authority/permissions.py
@@ -35,7 +35,7 @@ class BasePermission(object):
self.group = group
super(BasePermission, self).__init__(*args, **kwargs)
- def has_user_perms(self, perm, obj, check_groups=True):
+ def has_user_perms(self, perm, obj, approved, check_groups=True):
if self.user:
if self.user.is_superuser:
return True
@@ -43,15 +43,16 @@ class BasePermission(object):
return False
# check if a Permission object exists for the given params
return Permission.objects.user_permissions(self.user, perm, obj,
- check_groups).filter(object_id=obj.id)
+ approved, check_groups).filter(object_id=obj.id)
return False
- def has_group_perms(self, perm, obj):
+ def has_group_perms(self, perm, obj, approved):
"""
Check if group has the permission for the given object
"""
if self.group:
- perms = Permission.objects.group_permissions(self.group, perm, obj)
+ perms = Permission.objects.group_permissions(self.group, perm, obj,
+ approved)
return perms.filter(object_id=obj.id)
return False
@@ -60,12 +61,24 @@ class BasePermission(object):
Check if user has the permission for the given object
"""
if self.user:
- if self.has_user_perms(perm, obj, check_groups):
+ if self.has_user_perms(perm, obj, True, check_groups):
return True
if self.group:
- return self.has_group_perms(perm, obj)
+ return self.has_group_perms(perm, obj, True)
return False
+ def has_request(self, perm, obj, check_groups=True):
+ """
+ Check if user has the permission request for the given object
+ """
+ if self.user:
+ if self.has_user_perms(perm, obj, False, check_groups):
+ return True
+ if self.group:
+ return self.has_group_perms(perm, obj, False)
+ return False
+
+
def can(self, check, generic=False, *args, **kwargs):
if not args:
args = [self.model]
diff --git a/src/authority/templates/authority/permission_delete_link.html b/src/authority/templates/authority/permission_delete_link.html
index baec867..ef83fec 100644
--- a/src/authority/templates/authority/permission_delete_link.html
+++ b/src/authority/templates/authority/permission_delete_link.html
@@ -1,2 +1,2 @@
{% load i18n %}
-{% if delete_url %}{% trans "Revoke permission" %}{% endif %}
\ No newline at end of file
+{% if url %}{% trans "Revoke permission" %}{% endif %}
\ No newline at end of file
diff --git a/src/authority/templates/authority/permission_request_approve_link.html b/src/authority/templates/authority/permission_request_approve_link.html
new file mode 100644
index 0000000..32a32af
--- /dev/null
+++ b/src/authority/templates/authority/permission_request_approve_link.html
@@ -0,0 +1,2 @@
+{% load i18n %}
+{% if url %}{% trans "Approve request" %}{% endif %}
\ No newline at end of file
diff --git a/src/authority/templates/authority/permission_request_delete_link.html b/src/authority/templates/authority/permission_request_delete_link.html
new file mode 100644
index 0000000..660354a
--- /dev/null
+++ b/src/authority/templates/authority/permission_request_delete_link.html
@@ -0,0 +1,2 @@
+{% load i18n %}
+{% if url %}{% trans "Deny request" %}{% endif %}
\ No newline at end of file
diff --git a/src/authority/templates/authority/permission_request_form.html b/src/authority/templates/authority/permission_request_form.html
new file mode 100644
index 0000000..ad5f810
--- /dev/null
+++ b/src/authority/templates/authority/permission_request_form.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+{% if form %}
+
+{% endif %}
diff --git a/src/authority/templatetags/permissions.py b/src/authority/templatetags/permissions.py
index 267d8f1..3b53582 100644
--- a/src/authority/templatetags/permissions.py
+++ b/src/authority/templatetags/permissions.py
@@ -10,6 +10,20 @@ from authority.forms import UserPermissionForm
register = template.Library()
+def _base_link(context, perm, view_name):
+ return {
+ 'next': context['request'].build_absolute_uri(),
+ 'url': reverse(view_name, kwargs={'permission_pk': perm.pk,}),
+ }
+
+def _base_permission_form(context, obj, perm, user, approved, view_name):
+ return {
+ 'form': UserPermissionForm(perm, obj, approved,
+ initial=dict(codename=perm, user=user)),
+ 'form_url': url_for_obj(view_name, obj),
+ 'next': context['request'].build_absolute_uri(),
+ }
+
def next_bit_for(bits, key, if_none=None):
try:
return bits[bits.index(key)+1]
@@ -30,12 +44,16 @@ class ResolverNode(template.Node):
return template.Variable(var).resolve(context)
@register.simple_tag
-def add_url_for_obj(obj):
- return reverse('authority-add-permission', kwargs={
+def url_for_obj(view_name, obj):
+ return reverse(view_name, kwargs={
'app_label': obj._meta.app_label,
'module_name': obj._meta.module_name,
'pk': obj.pk})
+@register.simple_tag
+def add_url_for_obj(obj):
+ return url_for_obj('authority-add-permission', obj)
+
class ComparisonNode(ResolverNode):
"""
Implements a node to provide an "if user/group has permission on object"
@@ -124,14 +142,11 @@ def permission_delete_link(context, perm):
"""
user = context['request'].user
if user.is_authenticated():
- if user.has_perm('delete_foreign_permissions') or user.pk == perm.creator.pk:
- return {
- 'next': context['request'].build_absolute_uri(),
- 'delete_url': reverse('authority-delete-permission', kwargs={
- 'permission_pk': perm.pk,
- })
- }
- return {'delete_url': None}
+ if user.has_perm('authority.delete_foreign_permissions') \
+ or user.pk == perm.creator.pk:
+ return _base_link(context, perm, 'authority-delete-permission')
+ return {'url': None}
+
@register.inclusion_tag('authority/permission_form.html', takes_context=True)
def permission_form(context, obj, perm=None):
@@ -148,19 +163,37 @@ def permission_form(context, obj, perm=None):
user = context['request'].user
if user.is_authenticated():
if user.has_perm('authority.add_permission'):
- return {
- 'form': UserPermissionForm(perm, obj, initial=dict(codename=perm)),
- 'form_url': add_url_for_obj(obj),
- 'next': context['request'].build_absolute_uri(),
- }
+ return _base_permission_form(context, obj, perm, None, True,
+ 'authority-add-permission')
return {'form': None}
+
+@register.inclusion_tag('authority/permission_request_form.html', takes_context=True)
+def permission_request_form(context, obj, perm=None):
+ """
+ Renders an "add permission requests" form for the given object. If no perm
+ is given it will render a select box to choose from.
+
+ Syntax::
+
+ {% permission_request_form [obj] [permission_label].[check_name] %}
+ {% permission_request_form lesson "lesson_permission.add_lesson" %}
+
+ """
+ user = context['request'].user
+ if user.is_authenticated() and not user.is_superuser:
+ return _base_permission_form(context, obj, perm, user, False,
+ 'authority-add-request')
+ return {'form': None}
+
+
class PermissionsForObjectNode(ResolverNode):
- def __init__(self, obj, user, var_name, perm=None, objs=None):
+ def __init__(self, obj, user, var_name, approved, perm=None, objs=None):
self.obj = obj
self.user = user
self.perm = perm
self.var_name = var_name
+ self.approved = approved
def render(self, context):
obj = self.resolve(self.obj, context)
@@ -168,7 +201,7 @@ class PermissionsForObjectNode(ResolverNode):
user = self.resolve(self.user, context)
perms = []
if not isinstance(user, AnonymousUser):
- perms = Permission.objects.for_object(obj)
+ perms = Permission.objects.for_object(obj, self.approved)
if isinstance(user, User):
perms = perms.filter(user=user)
context[var_name] = perms
@@ -196,6 +229,34 @@ def get_permissions(parser, token):
'obj': next_bit_for(bits, 'get_permissions'),
'user': next_bit_for(bits, 'for'),
'var_name': next_bit_for(bits, 'as', '"permissions"'),
+ 'approved': True,
+ }
+ return PermissionsForObjectNode(**kwargs)
+
+
+@register.tag
+def get_permission_requests(parser, token):
+ """
+ Retrieves all permissions requests associated with the given obj and user
+ and assigns the result to a context variable.
+
+ Syntax::
+
+ {% get_permission_requests obj %}
+ {% for perm in permissions %}
+ {{ perm }}
+ {% endfor %}
+
+ {% get_permission_requests obj as "my_permissions" %}
+ {% get_permission_requests obj for request.user as "my_permissions" %}
+
+ """
+ bits = token.contents.split()
+ kwargs = {
+ 'obj': next_bit_for(bits, 'get_permission_requests'),
+ 'user': next_bit_for(bits, 'for'),
+ 'var_name': next_bit_for(bits, 'as', '"permission_requests"'),
+ 'approved': False,
}
return PermissionsForObjectNode(**kwargs)
@@ -247,3 +308,31 @@ def get_permission(parser, token):
'var_name': next_bit_for(bits, 'as', '"permission"'),
}
return PermissionForObjectNode(**kwargs)
+
+
+@register.inclusion_tag('authority/permission_request_delete_link.html', takes_context=True)
+def permission_request_delete_link(context, perm):
+ """
+ Renders a html link to the delete view of the given permission request.
+ Returns no content if the request-user has no permission to delete foreign
+ permissions.
+ """
+ user = context['request'].user
+ if user.is_authenticated():
+ if user.has_perm('authority.delete_permission'):
+ return _base_link(context, perm, 'authority-delete-request')
+ return {'url': None}
+
+
+@register.inclusion_tag('authority/permission_request_approve_link.html', takes_context=True)
+def permission_request_approve_link(context, perm):
+ """
+ Renders a html link to the approve view of the given permission request.
+ Returns no content if the request-user has no permission to delete foreign
+ permissions.
+ """
+ user = context['request'].user
+ if user.is_authenticated():
+ if user.has_perm('authority.approve_permission_request'):
+ return _base_link(context, perm, 'authority-approve-request')
+ return {'url': None}
\ No newline at end of file
diff --git a/src/authority/urls.py b/src/authority/urls.py
index 5a69132..08278f5 100644
--- a/src/authority/urls.py
+++ b/src/authority/urls.py
@@ -1,7 +1,11 @@
from django.conf.urls.defaults import *
-from authority.views import add_permission, delete_permission
+from authority.views import (add_permission, delete_permission,
+ approve_permission_request)
urlpatterns = patterns('',
- url(r'^add/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', add_permission, name="authority-add-permission"),
- url(r'^delete/(?P\d+)/$', delete_permission, name="authority-delete-permission"),
+ url(r'^add/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', add_permission, name="authority-add-permission", kwargs = {'approved': True }),
+ url(r'^add-request/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', add_permission, name="authority-add-request", kwargs = {'approved': False }),
+ url(r'^approve-request/(?P\d+)/$', approve_permission_request, name="authority-approve-request"),
+ url(r'^delete-request/(?P\d+)/$', delete_permission, name="authority-delete-request", kwargs = {'approved': False }),
+ url(r'^delete/(?P\d+)/$', delete_permission, name="authority-delete-permission", kwargs = {'approved': True }),
)
diff --git a/src/authority/views.py b/src/authority/views.py
index 58f5bc6..bf93f7e 100644
--- a/src/authority/views.py
+++ b/src/authority/views.py
@@ -2,56 +2,84 @@ from django.shortcuts import render_to_response, get_object_or_404
from django.views.decorators.http import require_POST
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.db.models.loading import get_model
-from django.utils.translation import ugettext
+from django.utils.translation import ugettext as _
from django.template.context import RequestContext
from django.template import loader
from django.contrib.auth.decorators import login_required
from authority.models import Permission
from authority.forms import UserPermissionForm
-from authority.templatetags.permissions import add_url_for_obj
+from authority.templatetags.permissions import url_for_obj
-@require_POST
@login_required
-def add_permission(request, app_label, module_name, pk, extra_context={},
- template_name='authority/permission_form.html'):
+def add_permission(request, app_label, module_name, pk, approved=False,
+ extra_context={}):
next = request.POST.get('next', '/')
codename = request.POST.get('codename', None)
- if codename is None:
- return HttpResponseForbidden(next)
model = get_model(app_label, module_name)
if model is None:
return permission_denied(request)
obj = get_object_or_404(model, pk=pk)
- form = UserPermissionForm(data=request.POST, obj=obj,
- perm=codename, initial={'codename': codename})
- if form.is_valid():
- form.save(request)
- request.user.message_set.create(
- message=ugettext('You added a permission.'))
- return HttpResponseRedirect(next)
+
+ if approved:
+ template_name = 'authority/permission_form.html'
+ view_name = 'authority-add-permission'
else:
- context = {
- 'form': form,
- 'form_url': add_url_for_obj(obj),
- 'next': next,
- 'perm': codename,
- }
- context.update(extra_context)
- return render_to_response(template_name, context,
- context_instance=RequestContext(request))
+ template_name = 'authority/permission_request_form.html'
+ view_name = 'authority-add-request'
+
+ if request.method == 'POST':
+ if codename is None:
+ return HttpResponseForbidden(next)
+ form = UserPermissionForm(data=request.POST, obj=obj, approved=approved,
+ perm=codename, initial=dict(codename=codename))
+ if form.is_valid():
+ form.save(request)
+ request.user.message_set.create(
+ message=_('You added a permission request.'))
+ return HttpResponseRedirect(next)
+ else:
+ form = UserPermissionForm(obj=obj, perm=codename, approved=approved,
+ initial=dict(codename=codename))
+
+ context = {
+ 'form': form,
+ 'form_url': url_for_obj(view_name, obj),
+ 'next': next,
+ 'perm': codename,
+ }
+ context.update(extra_context)
+ return render_to_response(template_name, context,
+ context_instance=RequestContext(request))
+
@login_required
-def delete_permission(request, permission_pk):
- permission = get_object_or_404(Permission, pk=permission_pk)
- if request.user.has_perm('delete_foreign_permissions') \
- or request.user == permission.creator:
- permission.delete()
+def approve_permission_request(request, permission_pk):
+ perm_request = get_object_or_404(Permission, pk=permission_pk)
+ if request.user.has_perm('authority.approve_permission_request'):
+ perm_request.approve_perm_request(request.user)
request.user.message_set.create(
- message=ugettext('You removed the permission.'))
+ message=_('You approved the permission request.'))
next = request.REQUEST.get('next') or '/'
return HttpResponseRedirect(next)
+
+@login_required
+def delete_permission(request, permission_pk, approved):
+ permission = get_object_or_404(Permission,
+ pk=permission_pk, approved=approved)
+ if request.user.has_perm('authority.delete_foreign_permissions') \
+ or request.user == permission.creator:
+ permission.delete()
+ if approved:
+ msg=_('You removed the permission.')
+ else:
+ msg=_('You removed the permission request.')
+ request.user.message_set.create(message=msg)
+ next = request.REQUEST.get('next') or '/'
+ return HttpResponseRedirect(next)
+
+
def permission_denied(request, template_name=None, extra_context={}):
"""
Default 403 handler.