Commits

Jannis Leidel  committed 300d2e3

Moved authority app from src/ to root directory

  • Participants
  • Parent commits 2c8994c

Comments (0)

Files changed (49)

 .installed.cfg
 bin/*
 develop-eggs/*.egg-link
-src/example/example.db
+example/example.db
 docs/build
 TODO
 example/example.db
 include LICENSE
 include buildout.cfg
 include bootstrap.py
-recursive-include src/authority/templates/authority *.html
-recursive-include src/authority/templates/admin *.html
+recursive-include authority/templates/authority *.html
+recursive-include authority/templates/admin *.html
 recursive-include docs *.txt *.html
 recursive-exclude docs/build *.txt
 prune docs/build/html/_sources

File authority/__init__.py

+import sys
+from authority.sites import site, get_check, get_choices_for, register, unregister
+
+LOADING = False
+
+def autodiscover():
+    """
+    Goes and imports the permissions submodule of every app in INSTALLED_APPS
+    to make sure the permission set classes are registered correctly.
+    """
+    global LOADING
+    if LOADING:
+        return
+    LOADING = True
+
+    import imp
+    from django.conf import settings
+
+    for app in settings.INSTALLED_APPS:
+        try:
+            __import__(app)
+            app_path = sys.modules[app].__path__
+        except AttributeError:
+            continue
+        try:
+            imp.find_module('permissions', app_path)
+        except ImportError:
+            continue
+        __import__("%s.permissions" % app)
+        app_path = sys.modules["%s.permissions" % app]
+    LOADING = False

File authority/admin.py

+from django import forms, template
+from django.http import HttpResponseRedirect
+from django.utils.translation import ugettext, ungettext, ugettext_lazy as _
+from django.shortcuts import render_to_response
+from django.utils.encoding import force_unicode
+from django.utils.safestring import mark_safe
+from django.forms.formsets import all_valid
+from django.contrib import admin
+from django.contrib.admin import helpers
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import PermissionDenied
+
+try:
+    from django.contrib.admin import actions
+except ImportError:
+    actions = False
+
+from authority.models import Permission
+from authority.widgets import GenericForeignKeyRawIdWidget
+from authority import get_choices_for
+
+class PermissionInline(generic.GenericTabularInline):
+    model = Permission
+    raw_id_fields = ('user', 'group', 'creator')
+    extra = 1
+
+    def formfield_for_dbfield(self, db_field, **kwargs):
+        if db_field.name == 'codename':
+            perm_choices = get_choices_for(self.parent_model)
+            kwargs['label'] = _('permission')
+            kwargs['widget'] = forms.Select(choices=perm_choices)
+            return db_field.formfield(**kwargs)
+        return super(PermissionInline, self).formfield_for_dbfield(db_field, **kwargs)
+
+class ActionPermissionInline(PermissionInline):
+    raw_id_fields = ()
+    template = 'admin/edit_inline/action_tabular.html'
+
+class ActionErrorList(forms.util.ErrorList):
+    def __init__(self, inline_formsets):
+        for inline_formset in inline_formsets:
+            self.extend(inline_formset.non_form_errors())
+            for errors_in_inline_form in inline_formset.errors:
+                self.extend(errors_in_inline_form.values())
+
+def edit_permissions(modeladmin, request, queryset):
+    opts = modeladmin.model._meta
+    app_label = opts.app_label
+
+    # Check that the user has the permission to edit permissions
+    if not (request.user.is_superuser or
+            request.user.has_perm('authority.change_permission') or
+            request.user.has_perm('authority.change_foreign_permissions')):
+        raise PermissionDenied
+
+    inline = ActionPermissionInline(queryset.model, modeladmin.admin_site)
+    formsets = []
+    for obj in queryset:
+        prefixes = {}
+        FormSet = inline.get_formset(request, obj)
+        prefix = "%s-%s" % (FormSet.get_default_prefix(), obj.pk)
+        prefixes[prefix] = prefixes.get(prefix, 0) + 1
+        if prefixes[prefix] != 1:
+            prefix = "%s-%s-%s" % (prefix, prefixes[prefix])
+        if request.POST.get('post'):
+            formset = FormSet(data=request.POST, files=request.FILES,
+                              instance=obj, prefix=prefix)
+        else:
+            formset = FormSet(instance=obj, prefix=prefix)
+        formsets.append(formset)
+
+    media = modeladmin.media
+    inline_admin_formsets = []
+    for formset in formsets:
+        fieldsets = list(inline.get_fieldsets(request))
+        inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
+        inline_admin_formsets.append(inline_admin_formset)
+        media = media + inline_admin_formset.media
+
+    ordered_objects = opts.get_ordered_objects()
+    if request.POST.get('post'):
+        if all_valid(formsets):
+            for formset in formsets:
+                formset.save()
+        # redirect to full request path to make sure we keep filter
+        return HttpResponseRedirect(request.get_full_path())
+
+    context = {
+        'errors': ActionErrorList(formsets),
+        'title': ugettext('Permissions for %s') % force_unicode(opts.verbose_name_plural),
+        'inline_admin_formsets': inline_admin_formsets,
+        'root_path': modeladmin.admin_site.root_path,
+        'app_label': app_label,
+        'change': True,
+        'ordered_objects': ordered_objects,
+        'form_url': mark_safe(''),
+        'opts': opts,
+        'target_opts': queryset.model._meta,
+        'content_type_id': ContentType.objects.get_for_model(queryset.model).id,
+        'save_as': False,
+        'save_on_top': False,
+        'is_popup': False,
+        'media': mark_safe(media),
+        'show_delete': False,
+        'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
+        'queryset': queryset,
+        "object_name": force_unicode(opts.verbose_name),
+    }
+    template_name = getattr(modeladmin, 'permission_change_form_template', [
+        "admin/%s/%s/permission_change_form.html" % (app_label, opts.object_name.lower()),
+        "admin/%s/permission_change_form.html" % app_label,
+        "admin/permission_change_form.html"
+    ])
+    return render_to_response(template_name, context,
+                              context_instance=template.RequestContext(request))
+edit_permissions.short_description = _("Edit permissions for selected %(verbose_name_plural)s")
+
+class PermissionAdmin(admin.ModelAdmin):
+    list_display = ('codename', 'content_type', 'user', 'group', 'approved')
+    list_filter = ('approved', 'content_type')
+    search_fields = ('user__username', 'group__name', 'codename')
+    raw_id_fields = ('user', 'group', 'creator')
+    generic_fields = ('content_object',)
+    actions = ['approve_permissions']
+    fieldsets = (
+        (None, {'fields': ('codename', ('content_type', 'object_id'))}),
+        (_('Permitted'), {'fields': ('approved', 'user', 'group')}),
+        (_('Creation'), {'fields': ('creator', 'date_requested', 'date_approved')}),
+    )
+
+    def formfield_for_dbfield(self, db_field, **kwargs):
+        # For generic foreign keys marked as generic_fields we use a special widget
+        if db_field.name in [f.fk_field for f in self.model._meta.virtual_fields if f.name in self.generic_fields]:
+            for gfk in self.model._meta.virtual_fields:
+                if gfk.fk_field == db_field.name:
+                    return db_field.formfield(
+                        widget=GenericForeignKeyRawIdWidget(
+                            gfk.ct_field, self.admin_site._registry.keys()))
+        return super(PermissionAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+
+    def queryset(self, request):
+        user = request.user
+        if (user.is_superuser or
+                user.has_perm('permissions.change_foreign_permissions')):
+            return super(PermissionAdmin, self).queryset(request)
+        return super(PermissionAdmin, self).queryset(request).filter(creator=user)
+
+    def approve_permissions(self, request, queryset):
+        for permission in queryset:
+            permission.approve(request.user)
+        message = ungettext("%(count)d permission successfully approved.",
+            "%(count)d permissions successfully approved.", len(queryset))
+        self.message_user(request, message % {'count': len(queryset)})
+    approve_permissions.short_description = _("Approve selected permissions")
+
+admin.site.register(Permission, PermissionAdmin)
+
+if actions:
+    admin.site.add_action(edit_permissions)

File authority/decorators.py

+import inspect
+from django.http import HttpResponseRedirect
+from django.utils.http import urlquote
+from django.utils.functional import wraps
+from django.db.models import Model, get_model
+from django.shortcuts import get_object_or_404
+from django.conf import settings
+from django.contrib.auth import REDIRECT_FIELD_NAME
+
+from authority import get_check
+from authority.views import permission_denied
+
+def permission_required(perm, *lookup_variables, **kwargs):
+    """
+    Decorator for views that checks whether a user has a particular permission
+    enabled, redirecting to the log-in page if necessary.
+    """
+    login_url = kwargs.pop('login_url', settings.LOGIN_URL)
+    redirect_field_name = kwargs.pop('redirect_field_name', REDIRECT_FIELD_NAME)
+    redirect_to_login = kwargs.pop('redirect_to_login', True)
+    def decorate(view_func):
+        def decorated(request, *args, **kwargs):
+            if request.user.is_authenticated():
+                params = []
+                for lookup_variable in lookup_variables:
+                    if isinstance(lookup_variable, basestring):
+                        value = kwargs.get(lookup_variable, None)
+                        if value is None:
+                            continue
+                        params.append(value)
+                    elif isinstance(lookup_variable, (tuple, list)):
+                        model, lookup, varname = lookup_variable
+                        value = kwargs.get(varname, None)
+                        if value is None:
+                            continue
+                        if isinstance(model, basestring):
+                            model_class = get_model(*model.split("."))
+                        else:
+                            model_class = model
+                        if model_class is None:
+                            raise ValueError(
+                                "The given argument '%s' is not a valid model." % model)
+                        if (inspect.isclass(model_class) and
+                                not issubclass(model_class, Model)):
+                            raise ValueError(
+                                'The argument %s needs to be a model.' % model)
+                        obj = get_object_or_404(model_class, **{lookup: value})
+                        params.append(obj)
+                check = get_check(request.user, perm)
+                granted = False
+                if check is not None:
+                    granted = check(*params)
+                if granted or request.user.has_perm(perm):
+                    return view_func(request, *args, **kwargs)
+            if redirect_to_login:
+                path = urlquote(request.get_full_path())
+                tup = login_url, redirect_field_name, path
+                return HttpResponseRedirect('%s?%s=%s' % tup)
+            return permission_denied(request)
+        return wraps(view_func)(decorated)
+    return decorate
+
+def permission_required_or_403(perm, *args, **kwargs):
+    """
+    Decorator that wraps the permission_required decorator and returns a
+    permission denied (403) page instead of redirecting to the login URL.
+    """
+    kwargs['redirect_to_login'] = False
+    return permission_required(perm, *args, **kwargs)

File authority/exceptions.py

+class AuthorityException(Exception):
+    pass
+
+class NotAModel(AuthorityException):
+    def __init__(self, object):
+        super(NotAModel, self).__init__(
+            "Not a model class or instance")
+
+class UnsavedModelInstance(AuthorityException):
+    def __init__(self, object):
+        super(UnsavedModelInstance, self).__init__(
+            "Model instance has no pk, was it saved?")

File authority/fixtures/tests.json

+[
+    {
+        "pk": 1, 
+        "model": "auth.user", 
+        "fields": {
+            "username": "jezdez", 
+            "first_name": "", 
+            "last_name": "", 
+            "is_active": true, 
+            "is_superuser": false, 
+            "is_staff": false, 
+            "last_login": "2009-11-02 03:06:19", 
+            "groups": [], 
+            "user_permissions": [], 
+            "password": "", 
+            "email": "", 
+            "date_joined": "2009-11-02 03:06:19"
+        }
+    }
+]

File authority/forms.py

+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.utils.safestring import mark_safe
+
+from authority import permissions, get_choices_for
+from authority.models import Permission
+
+class BasePermissionForm(forms.ModelForm):
+    codename = forms.CharField(label=_('Permission'))
+
+    class Meta:
+        model = Permission
+
+    def __init__(self, perm=None, obj=None, approved=False, *args, **kwargs):
+        self.perm = perm
+        self.obj = obj
+        self.approved = approved
+        if obj and perm:
+            self.base_fields['codename'].widget = forms.HiddenInput()
+        elif obj and (not perm or not approved):
+            perms = get_choices_for(self.obj)
+            self.base_fields['codename'].widget = forms.Select(choices=perms)
+        super(BasePermissionForm, self).__init__(*args, **kwargs)
+
+    def save(self, request, commit=True, *args, **kwargs):
+        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 __init__(self, *args, **kwargs):
+        if not kwargs.get('approved', False):
+            self.base_fields['user'].widget = forms.HiddenInput()
+        super(UserPermissionForm, self).__init__(*args, **kwargs)
+
+    def clean_user(self):
+        username = self.cleaned_data["user"]
+        try:
+            user = User.objects.get(username__iexact=username)
+        except User.DoesNotExist:
+            raise forms.ValidationError(
+                mark_safe(_("A user with that username does not exist.")))
+        check = permissions.BasePermission(user=user)
+        error_msg = None
+        if user.is_superuser:
+            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.requested_perm(self.perm, self.obj):
+            error_msg = _("The user %(user)s already requested the permission"
+                          " '%(perm)s' for %(object_name)s '%(obj)s'")
+        if error_msg:
+            error_msg = error_msg % {
+                'object_name': self.obj._meta.object_name.lower(),
+                'perm': self.perm,
+                'obj': self.obj,
+                'user': user,
+            }
+            raise forms.ValidationError(mark_safe(error_msg))
+        return user
+
+
+class GroupPermissionForm(BasePermissionForm):
+    group = forms.CharField(label=_('Group'))
+
+    class Meta(BasePermissionForm.Meta):
+        fields = ('group',)
+
+    def clean_group(self):
+        groupname = self.cleaned_data["group"]
+        try:
+            group = Group.objects.get(name__iexact=groupname)
+        except Group.DoesNotExist:
+            raise forms.ValidationError(
+                mark_safe(_("A group with that name does not exist.")))
+        check = permissions.BasePermission(group=group)
+        if check.has_perm(self.perm, self.obj):
+            raise forms.ValidationError(mark_safe(
+                _("This group already has the permission '%(perm)s' for %(object_name)s '%(obj)s'") % {
+                    'perm': self.perm,
+                    'object_name': self.obj._meta.object_name.lower(),
+                    'obj': self.obj,
+                }))
+        return group

File authority/managers.py

+from django.db import models
+from django.db.models import Q
+from django.contrib.contenttypes.models import ContentType
+
+class PermissionManager(models.Manager):
+
+    def get_content_type(self, obj):
+        return ContentType.objects.get_for_model(obj)
+
+    def get_for_model(self, obj):
+        return self.filter(content_type=self.get_content_type(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,approved=approved)
+
+    def for_user(self, user, obj, check_groups=True):
+        perms = self.get_for_model(obj)
+        if not check_groups:
+            return perms.select_related('user', 'creator').filter(user=user)
+        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, 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, 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, 
+                                               approved=approved)
+
+    def delete_objects_permissions(self, obj):
+        """
+        Delete permissions related to an object instance
+        """
+        perms = self.for_object(obj)
+        perms.delete()
+
+    def delete_user_permissions(self, user, perm, obj, check_groups=False):
+        """
+        Remove granular permission perm from user on an object instance
+        """
+        user_perms = self.user_permissions(user, perm, obj, check_groups=False)
+        if not user_perms.filter(object_id=obj.id):
+            return
+        perms = self.user_permissions(user, perm, obj).filter(object_id=obj.id)
+        perms.delete()
+
+                              

File authority/models.py

+from datetime import datetime
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.contrib.auth.models import User, Group
+from django.utils.translation import ugettext_lazy as _
+
+from authority.managers import PermissionManager
+
+class Permission(models.Model):
+    """
+    A granular permission model, per-object permission in other words.
+    This kind of permission is associated with a user/group and an object
+    of any content type.
+    """
+    codename = models.CharField(_('codename'), max_length=100)
+    content_type = models.ForeignKey(ContentType, related_name="row_permissions")
+    object_id = models.PositiveIntegerField()
+    content_object = generic.GenericForeignKey('content_type', 'object_id')
+
+    user = models.ForeignKey(User, null=True, blank=True, related_name='granted_permissions')
+    group = models.ForeignKey(Group, null=True, blank=True)
+    creator = models.ForeignKey(User, null=True, blank=True, related_name='created_permissions')
+
+    approved = models.BooleanField(_('approved'), default=False, help_text=_("Designates whether the permission has been approved and treated as active. Unselect this instead of deleting permissions."))
+
+    date_requested = models.DateTimeField(_('date requested'), default=datetime.now)
+    date_approved = models.DateTimeField(_('date approved'), blank=True, null=True)
+
+    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_requests', 'Can approve permission requests'),
+        )
+
+    def save(self, *args, **kwargs):
+        # Make sure the approval date is always set
+        if self.approved and not self.date_approved:
+            self.date_approved = datetime.now()
+        super(Permission, self).save(*args, **kwargs)
+
+    def approve(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()

File authority/permissions.py

+import copy
+
+from django.db.models.base import Model, ModelBase
+from django.template.defaultfilters import slugify
+from django.contrib.auth.models import Permission as DjangoPermission 
+from django.contrib.contenttypes.models import ContentType
+
+from authority.exceptions import NotAModel, UnsavedModelInstance
+from authority.models import Permission
+
+class PermissionMetaclass(type):
+    """
+    Used to generate the default set of permission checks "add", "change" and
+    "delete".
+    """
+    def __new__(cls, name, bases, attrs):
+        new_class = super(
+            PermissionMetaclass, cls).__new__(cls, name, bases, attrs)
+        if not new_class.label:
+            new_class.label = "%s_permission" % new_class.__name__.lower()
+        new_class.label = slugify(new_class.label)
+        if new_class.checks is None:
+            new_class.checks = []
+        # force check names to be lower case
+        new_class.checks = [check.lower() for check in new_class.checks]
+        return new_class
+
+class BasePermission(object):
+    """
+    Base Permission class to be used to define app permissions.
+    """
+    __metaclass__ = PermissionMetaclass
+
+    checks = ()
+    label = None
+    generic_checks = ['add', 'browse', 'change', 'delete']
+
+    def __init__(self, user=None, group=None, *args, **kwargs):
+        self.user = user
+        self.group = group
+        super(BasePermission, self).__init__(*args, **kwargs)
+
+    def has_user_perms(self, perm, obj, approved, check_groups=True):
+        if self.user:
+            if self.user.is_superuser:
+                return True
+            if not self.user.is_active:
+                return False
+            # check if a Permission object exists for the given params
+            return Permission.objects.user_permissions(self.user, perm, obj,
+                approved, check_groups).filter(object_id=obj.id)
+        return False
+
+    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, 
+                                                         approved)
+            return perms.filter(object_id=obj.id)
+        return False
+
+    def has_perm(self, perm, obj, check_groups=True, approved=True):
+        """
+        Check if user has the permission for the given object
+        """
+        if self.user:
+            if self.has_user_perms(perm, obj, approved, check_groups):
+                return True
+        if self.group:
+            return self.has_group_perms(perm, obj, approved)
+        return False
+
+    def requested_perm(self, perm, obj, check_groups=True):
+        """
+        Check if user requested a permission for the given object
+        """
+        return self.has_perm(perm, obj, check_groups, False)
+
+    def can(self, check, generic=False, *args, **kwargs):
+        if not args:
+            args = [self.model]
+        perms = False
+        for obj in args:
+            # skip this obj if it's not a model class or instance
+            if not isinstance(obj, (ModelBase, Model)):
+                continue
+            # first check Django's permission system
+            if self.user:
+                perm = '%s.%s' % (obj._meta.app_label, check.lower())
+                if generic:
+                    perm = '%s_%s' % (perm, obj._meta.object_name.lower())
+                perms = perms or self.user.has_perm(perm)
+            perm = '%s.%s' % (self.label, check.lower())
+            if generic:
+                perm = '%s_%s' % (perm, obj._meta.object_name.lower())
+            # then check authority's per object permissions
+            if not isinstance(obj, ModelBase) and isinstance(obj, self.model):
+                # only check the authority if obj is not a model class
+                perms = perms or self.has_perm(perm, obj)
+        return perms
+
+    def get_django_codename(self, check, model, generic=False, without_left=False):
+        if without_left:
+            perm = check
+        else:
+            perm = '%s.%s' % (model._meta.app_label, check.lower())
+        if generic:
+            perm = '%s_%s' % (perm, model._meta.object_name.lower())
+        return perm
+
+    def get_codename(self, check, model, generic=False):
+        perm = '%s.%s' % (self.label, check.lower())
+        if generic:
+            perm = '%s_%s' % (perm, model._meta.object_name.lower())
+        return perm      
+
+    def assign(self, check=None, content_object=None, generic=False):
+        '''
+        Assign a permission to a user.
+
+        To assign permission for all checks: let check=None.
+        To assign permission for all objects: let content_object=None.
+        
+        If generic is True then "check" will be suffixed with _modelname.
+        '''
+        result = []
+
+        if not content_object:
+            content_objects = (self.model,)
+        elif not isinstance(content_object, (list, tuple)):
+            content_objects = (content_object,)
+        else:
+            content_objects = content_object
+
+        if not check:
+            print "Not passing a check argument is not supported"
+            print "I can't get the god damn test to pass"
+            #checks = self.generic_checks + getattr(self, 'checks', [])
+            checks = self.checks
+        elif not isinstance(check, (list, tuple)):
+            checks = (check,)
+        else:
+            checks = check
+
+        for content_object in content_objects:
+            # raise an exception before adding any permission
+            # i think django does not rollback by default
+            if not isinstance(content_object, (Model, ModelBase)):
+                raise NotAModel(content_object)
+            elif isinstance(content_object, Model) and \
+                not content_object.pk:
+                #not getattr(content_object, 'pk', False):
+                raise UnsavedModelInstance(content_object)
+            
+            content_type = ContentType.objects.get_for_model(content_object)
+
+            for check in checks:
+                if isinstance(content_object, Model):
+                    # make an authority per object permission
+                    codename = self.get_codename(check, content_object, generic)
+
+                    try:
+                        perm = Permission.objects.get(
+                            user = self.user,
+                            codename = codename,
+                            approved = True,
+                            content_type = content_type,
+                            object_id = content_object.pk
+                        )
+                    except Permission.DoesNotExist:
+                        perm = Permission(
+                            user = self.user,
+                            content_object = content_object,
+                            codename = codename,
+                            approved = True
+                        )
+                        perm.save()
+
+                    result.append(perm)
+
+                elif isinstance(content_object, ModelBase):
+                    # make a django permission
+                    codename = self.get_django_codename(check, content_object, generic, without_left=True)
+                    try:
+                        perm = DjangoPermission.objects.get(codename=codename)
+                    except DjangoPermission.DoesNotExist:
+                        name = check
+                        if '_' in name:
+                            name = name[0:name.find('_')]
+                        perm = DjangoPermission(
+                            name = name,
+                            codename = codename,
+                            content_type = content_type)
+                        perm.save()
+                    self.user.user_permissions.add(perm)
+                    result.append(perm)
+
+        return result

File authority/sites.py

+from inspect import getmembers, ismethod
+from django.db import models
+from django.db.models.base import ModelBase
+from django.utils.translation import ugettext_lazy as _
+from django.core.exceptions import ImproperlyConfigured
+
+from authority.permissions import BasePermission
+
+class AlreadyRegistered(Exception):
+    pass
+
+class NotRegistered(Exception):
+    pass
+
+class PermissionSite(object):
+    """
+    A dictionary that contains permission instances and their labels.
+    """
+    _registry = {}
+    _choices = {}
+
+    def get_permission_by_label(self, label):
+        for perm_cls in self._registry.values():
+            if perm_cls.label == label:
+                return perm_cls
+        return None
+
+    def get_permissions_by_model(self, model):
+        return [perm for perm in self._registry.values() if perm.model == model]
+
+    def get_check(self, user, label):
+        perm_label, check_name = label.split('.')
+        perm_cls = self.get_permission_by_label(perm_label)
+        if perm_cls is None:
+            return None
+        perm_instance = perm_cls(user)
+        return getattr(perm_instance, check_name, None)
+
+    def get_labels(self):
+        return [perm.label for perm in self._registry.values()]
+
+    def get_choices_for(self, obj, default=models.BLANK_CHOICE_DASH):
+        model_cls = obj
+        if not isinstance(obj, ModelBase):
+            model_cls = obj.__class__
+        if model_cls in self._choices:
+            return self._choices[model_cls]
+        choices = [] + default
+        for perm in self.get_permissions_by_model(model_cls):
+            for name, check in getmembers(perm, ismethod):
+                if name in perm.checks:
+                    signature = '%s.%s' % (perm.label, name)
+                    label = getattr(check, 'short_description', signature)
+                    choices.append((signature, label))
+        self._choices[model_cls] = choices
+        return choices
+
+    def register(self, model_or_iterable, permission_class=None, **options):
+        if not permission_class:
+            permission_class = BasePermission
+
+        if isinstance(model_or_iterable, ModelBase):
+            model_or_iterable = [model_or_iterable]
+
+        if permission_class.label in self.get_labels():
+            raise ImproperlyConfigured(
+                "The name of %s conflicts with %s" % (permission_class,
+                     self.get_permission_by_label(permission_class.label)))
+
+        for model in model_or_iterable:
+            if model in self._registry:
+                raise AlreadyRegistered(
+                    'The model %s is already registered' % model.__name__)
+            if options:
+                options['__module__'] = __name__
+                permission_class = type("%sPermission" % model.__name__,
+                    (permission_class,), options)
+
+            permission_class.model = model
+            self.setup(model, permission_class)
+            self._registry[model] = permission_class
+
+    def unregister(self, model_or_iterable):
+        if isinstance(model_or_iterable, ModelBase):
+            model_or_iterable = [model_or_iterable]
+        for model in model_or_iterable:
+            if model not in self._registry:
+                raise NotRegistered('The model %s is not registered' % model.__name__)
+            del self._registry[model]
+
+    def setup(self, model, permission):
+        for check_name in permission.checks:
+            check_func = getattr(permission, check_name, None)
+            if check_func is not None:
+                func = self.create_check(check_name, check_func)
+                func.__name__ = check_name
+                func.short_description = getattr(check_func, 'short_description',
+                    _("%(object_name)s permission '%(check)s'") % {
+                        'object_name': model._meta.object_name,
+                        'check': check_name})
+                setattr(permission, check_name, func)
+            else:
+                permission.generic_checks.append(check_name)
+        for check_name in permission.generic_checks:
+            func = self.create_check(check_name, generic=True)
+            object_name = model._meta.object_name
+            func_name = "%s_%s" % (check_name, object_name.lower())
+            func.short_description = _("Can %(check)s this %(object_name)s") % {
+                'object_name': model._meta.object_name.lower(),
+                'check': check_name}
+            func.check_name = check_name
+            if func_name not in permission.checks:
+                permission.checks.append(func_name)
+            setattr(permission, func_name, func)
+        setattr(model, "permissions", PermissionDescriptor())
+
+    def create_check(self, check_name, check_func=None, generic=False):
+        def check(self, *args, **kwargs):
+            granted = self.can(check_name, generic, *args, **kwargs)
+            if check_func and not granted:
+                return check_func(self, *args, **kwargs)
+            return granted
+        return check
+
+class PermissionDescriptor(object):
+    def get_content_type(self, obj=None):
+        ContentType = models.get_model("contenttypes", "contenttype")
+        if obj:
+            return ContentType.objects.get_for_model(obj)
+        else:
+            raise Exception("Dude, impossible arguments to PermissionDescriptor.get_content_type!")
+
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        ct = self.get_content_type(instance)
+        return ct.row_permissions.all()
+
+site = PermissionSite()
+get_check = site.get_check
+get_choices_for = site.get_choices_for
+register = site.register
+unregister = site.unregister

File authority/templates/admin/edit_inline/action_tabular.html

+{% load i18n %}
+<div class="inline-group">
+  <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
+{{ inline_admin_formset.formset.management_form }}
+<fieldset class="module">
+   <h2>{{ inline_admin_formset.formset.instance }}</h2>
+   {{ inline_admin_formset.formset.non_form_errors }}
+   <table>
+     <thead><tr>
+     {% for field in inline_admin_formset.fields %}
+       {% if not field.is_hidden %}
+         <th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst }}</th>
+        {% endif %}
+     {% endfor %}
+     {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
+     </tr></thead>
+
+     {% for inline_admin_form in inline_admin_formset %}
+        {% if inline_admin_form.form.non_field_errors %}
+        <tr><td colspan="{{ inline_admin_form.field_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
+        {% endif %}
+        <tr class="{% cycle row1,row2 %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}">
+
+        <td class="original">
+          {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
+          {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
+          {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
+            </p>{% endif %}
+          {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
+          {{ inline_admin_form.fk_field.field }}
+          {% spaceless %}
+          {% for fieldset in inline_admin_form %}
+            {% for line in fieldset %}
+              {% for field in line %}
+                {% if field.is_hidden %} {{ field.field }} {% endif %}
+              {% endfor %}
+            {% endfor %}
+          {% endfor %}
+          {% endspaceless %}
+        </td>
+
+        {% for fieldset in inline_admin_form %}
+          {% for line in fieldset %}
+            {% for field in line %}
+              <td class="{{ field.field.name }}">
+              {{ field.field.errors.as_ul }}
+              {{ field.field }}
+              </td>
+            {% endfor %}
+          {% endfor %}
+        {% endfor %}
+
+        {% if inline_admin_formset.formset.can_delete %}
+          <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
+        {% endif %}
+
+        </tr>
+
+     {% endfor %}
+
+   </table>
+
+</fieldset>
+  </div>
+
+</div>

File authority/templates/admin/permission_change_form.html

+{% extends "admin/base_site.html" %}
+{% load i18n admin_modify adminmedia %}
+
+{% block extrahead %}{{ block.super }}
+<script type="text/javascript" src="../../../jsi18n/"></script>
+{{ media }}
+{% endblock %}
+
+{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />{% endblock %}
+
+{% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %}
+
+{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
+
+{% block breadcrumbs %}{% if not is_popup %}
+<div class="breadcrumbs">
+     <a href="../../">{% trans "Home" %}</a> &rsaquo;
+     <a href="../">{{ app_label|capfirst|escape }}</a> &rsaquo; 
+     <a href="./">{{ opts.verbose_name_plural|capfirst }}</a>  &rsaquo;
+     {% trans "Permissions" %}
+</div>
+{% endif %}{% endblock %}
+
+{% block content %}<div id="content-main">
+<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">
+<div>
+{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
+{% if save_on_top %}{% submit_row %}{% endif %}
+{% if errors %}
+    <p class="errornote">
+    {% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
+    </p>
+    <ul class="errorlist">{% for error in adminform.form.non_field_errors %}<li>{{ error }}</li>{% endfor %}</ul>
+{% endif %}
+
+{% for fieldset in adminform %}
+  {% include "admin/includes/fieldset.html" %}
+{% endfor %}
+
+{% for inline_admin_formset in inline_admin_formsets %}
+    {% include inline_admin_formset.opts.template %}
+{% endfor %}
+
+{% block after_related_objects %}{% endblock %}
+
+<div class="submit-row">
+  <input type="submit" value="{% trans 'Save' %}" class="default" name="_save" {{ onclick_attrib }}/>
+</div>
+
+</div>
+{% for obj in queryset %}
+<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}" />
+{% endfor %}
+<input type="hidden" name="action" value="edit_permissions" />
+<input type="hidden" name="post" value="yes" />
+</form></div>
+{% endblock %}

File authority/templates/authority/403.html

+{% load i18n %}
+
+<h1>{% trans 'Permission denied <em>(403)</em>' %}</h1>
+<p>{% trans "You don't have sufficient permissions for this page." %}</p>

File authority/templates/authority/permission_delete_link.html

+{% load i18n %}
+{% if url %}<a href="{{ url }}?next={{ next }}">{% trans "Revoke permission" %}</a>{% endif %}

File authority/templates/authority/permission_form.html

+{% load i18n %}
+{% if form %}
+<form method="post" enctype="multipart/form-data" action="{{ form_url }}" class="add-permission">
+    <input type="hidden" name="next" value="{{ next }}"/>
+    {{ form.as_p }}
+    <p class="submit">
+    {% if approved %}
+        <input type="submit" value="{% trans "Grant permission" %}"/>
+    {% else %}
+        <input type="submit" value="{% trans "Request permission" %}"/>
+    {% endif %}
+    </p>
+</form>
+{% endif %}

File authority/templates/authority/permission_request_approve_link.html

+{% load i18n %}
+{% if url %}<a href="{{ url }}?next={{ next }}">{% trans "Approve request" %}</a>{% endif %}

File authority/templates/authority/permission_request_delete_link.html

+{% load i18n %}
+{% if url %}<a href="{{ url }}?next={{ next }}">{% if is_requestor %}{% trans "Withdraw request" %}{% else %}{% trans "Deny request" %}{% endif %}</a>{% endif %}

File authority/templatetags/__init__.py

Empty file added.

File authority/templatetags/permissions.py

+from django import template
+from django.core.urlresolvers import reverse
+from django.core.exceptions import ImproperlyConfigured
+from django.contrib.auth.models import User, AnonymousUser
+from django.core.urlresolvers import reverse
+
+from authority import get_check
+from authority import permissions
+from authority.models import Permission
+from authority.forms import UserPermissionForm
+
+register = template.Library()
+
+@register.simple_tag
+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)
+
+@register.simple_tag
+def request_url_for_obj(obj):
+    return url_for_obj('authority-add-permission-request', obj)
+
+
+class ResolverNode(template.Node):
+    """
+    A small wrapper that adds a convenient resolve method.
+    """
+    def resolve(self, var, context):
+        """Resolves a variable out of context if it's not in quotes"""
+        if var is None:
+            return var
+        if var[0] in ('"', "'") and var[-1] == var[0]:
+            return var[1:-1]
+        else:
+            return template.Variable(var).resolve(context)
+
+    @classmethod
+    def next_bit_for(cls, bits, key, if_none=None):
+        try:
+            return bits[bits.index(key)+1]
+        except ValueError:
+            return if_none
+
+
+class PermissionComparisonNode(ResolverNode):
+    """
+    Implements a node to provide an "if user/group has permission on object"
+    """
+    @classmethod
+    def handle_token(cls, parser, token):
+        bits = token.contents.split()
+        if 5 < len(bits) < 3:
+            raise template.TemplateSyntaxError("'%s' tag takes three, "
+                                                "four or five arguments" % bits[0])
+        end_tag = 'endifhasperm'
+        nodelist_true = parser.parse(('else', end_tag))
+        token = parser.next_token()
+        if token.contents == 'else': # there is an 'else' clause in the tag
+            nodelist_false = parser.parse((end_tag,))
+            parser.delete_first_token()
+        else:
+            nodelist_false = template.NodeList()
+        if len(bits) == 3: # this tag requires at most 2 objects . None is given
+            objs = (None, None)
+        elif len(bits) == 4:# one is given
+            objs = (bits[3], None)
+        else: #two are given
+            objs = (bits[3], bits[4])
+        return cls(bits[2], bits[1], nodelist_true, nodelist_false, *objs)
+
+    def __init__(self, user, perm, nodelist_true, nodelist_false, *objs):
+        self.user = user
+        self.objs = objs
+        self.perm = perm
+        self.nodelist_true = nodelist_true
+        self.nodelist_false = nodelist_false
+
+    def render(self, context):
+        try:
+            user = self.resolve(self.user, context)
+            perm = self.resolve(self.perm, context)
+            if self.objs:
+                objs = []
+                for obj in self.objs:
+                    if obj is not None:
+                        objs.append(self.resolve(obj, context))
+            else:
+                objs = None
+            check = get_check(user, perm)
+            if check is not None:
+                if check(*objs):
+                    # return True if check was successful
+                    return self.nodelist_true.render(context)
+        # If the app couldn't be found
+        except (ImproperlyConfigured, ImportError):
+            return ''
+        # If either variable fails to resolve, return nothing.
+        except template.VariableDoesNotExist:
+            return ''
+        # If the types don't permit comparison, return nothing.
+        except (TypeError, AttributeError):
+            return ''
+        return self.nodelist_false.render(context)
+
+@register.tag
+def ifhasperm(parser, token):
+    """
+    This function provides functionality for the 'ifhasperm' template tag
+
+    Syntax::
+
+        {% ifhasperm PERMISSION_LABEL.CHECK_NAME USER *OBJS %}
+            lalala
+        {% else %}
+            meh
+        {% endifhasperm %}
+
+        {% if hasperm "poll_permission.change_poll" request.user %}
+            lalala
+        {% else %}
+            meh
+        {% endifhasperm %}
+
+    """
+    return PermissionComparisonNode.handle_token(parser, token)
+
+
+class PermissionFormNode(ResolverNode):
+
+    @classmethod
+    def handle_token(cls, parser, token, approved):
+        bits = token.contents.split()
+        tag_name = bits[0]
+        kwargs = {
+            'obj': cls.next_bit_for(bits, 'for'),
+            'perm': cls.next_bit_for(bits, 'using', None),
+            'template_name': cls.next_bit_for(bits, 'with', ''),
+            'approved': approved,
+        }
+        return cls(**kwargs)
+
+    def __init__(self, obj, perm=None, approved=False, template_name=None):
+        self.obj = obj
+        self.perm = perm
+        self.approved = approved
+        self.template_name = template_name
+
+    def render(self, context):
+        obj = self.resolve(self.obj, context)
+        perm = self.resolve(self.perm, context)
+        if self.template_name:
+            template_name = [self.resolve(obj, context) for obj in self.template_name.split(',')]
+        else:
+            template_name = 'authority/permission_form.html'
+        request = context['request']
+        extra_context = {}
+        if self.approved:
+            if (request.user.is_authenticated() and
+                    request.user.has_perm('authority.add_permission')):
+                extra_context = {
+                    'form_url': url_for_obj('authority-add-permission', obj),
+                    'next': request.build_absolute_uri(),
+                    'approved': self.approved,
+                    'form': UserPermissionForm(perm, obj, approved=self.approved,
+                                               initial=dict(codename=perm)),
+                }
+        else:
+            if request.user.is_authenticated() and not request.user.is_superuser:
+                extra_context = {
+                    'form_url': url_for_obj('authority-add-permission-request', obj),
+                    'next': request.build_absolute_uri(),
+                    'approved': self.approved,
+                    'form': UserPermissionForm(perm, obj,
+                        approved=self.approved, initial=dict(
+                        codename=perm, user=request.user.username)),
+                }
+        return template.loader.render_to_string(template_name, extra_context,
+                            context_instance=template.RequestContext(request))
+
+@register.tag
+def permission_form(parser, token):
+    """
+    Renders an "add permissions" form for the given object. If no object
+    is given it will render a select box to choose from.
+
+    Syntax::
+
+        {% permission_form for OBJ using PERMISSION_LABEL.CHECK_NAME [with TEMPLATE] %}
+        {% permission_form for lesson using "lesson_permission.add_lesson" %}
+
+    """
+    return PermissionFormNode.handle_token(parser, token, approved=True)
+
+@register.tag
+def permission_request_form(parser, token):
+    """
+    Renders an "add permissions" form for the given object. If no object
+    is given it will render a select box to choose from.
+
+    Syntax::
+
+        {% permission_request_form for OBJ and PERMISSION_LABEL.CHECK_NAME [with TEMPLATE] %}
+        {% permission_request_form for lesson using "lesson_permission.add_lesson" with "authority/permission_request_form.html" %}
+
+    """
+    return PermissionFormNode.handle_token(parser, token, approved=False)
+
+
+class PermissionsForObjectNode(ResolverNode):
+
+    @classmethod
+    def handle_token(cls, parser, token, approved, name):
+        bits = token.contents.split()
+        tag_name = bits[0]
+        kwargs = {
+            'obj': cls.next_bit_for(bits, tag_name),
+            'user': cls.next_bit_for(bits, 'for'),
+            'var_name': cls.next_bit_for(bits, 'as', name),
+            'approved': approved,
+        }
+        return cls(**kwargs)
+
+    def __init__(self, obj, user, var_name, approved, perm=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)
+        var_name = self.resolve(self.var_name, context)
+        user = self.resolve(self.user, context)
+        perms = []
+        if not isinstance(user, AnonymousUser):
+            perms = Permission.objects.for_object(obj, self.approved)
+            if isinstance(user, User):
+                perms = perms.filter(user=user)
+        context[var_name] = perms
+        return ''
+
+@register.tag
+def get_permissions(parser, token):
+    """
+    Retrieves all permissions associated with the given obj and user
+    and assigns the result to a context variable.
+    
+    Syntax::
+
+        {% get_permissions obj %}
+        {% for perm in permissions %}
+            {{ perm }}
+        {% endfor %}
+
+        {% get_permissions obj as "my_permissions" %}
+        {% get_permissions obj for request.user as "my_permissions" %}
+
+    """
+    return PermissionsForObjectNode.handle_token(parser, token, approved=True,
+                                                 name='"permissions"')
+
+@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" %}
+
+    """
+    return PermissionsForObjectNode.handle_token(parser, token,
+                                                 approved=False,
+                                                 name='"permission_requests"')
+
+class PermissionForObjectNode(ResolverNode):
+
+    @classmethod
+    def handle_token(cls, parser, token, approved, name):
+        bits = token.contents.split()
+        tag_name = bits[0]
+        kwargs = {
+            'perm': cls.next_bit_for(bits, tag_name),
+            'user': cls.next_bit_for(bits, 'for'),
+            'objs': cls.next_bit_for(bits, 'and'),
+            'var_name': cls.next_bit_for(bits, 'as', name),
+            'approved': approved,
+        }
+        return cls(**kwargs)
+
+    def __init__(self, perm, user, objs, approved, var_name):
+        self.perm = perm
+        self.user = user
+        self.objs = objs
+        self.var_name = var_name
+        self.approved = approved
+
+    def render(self, context):
+        objs = [self.resolve(obj, context) for obj in self.objs.split(',')]
+        var_name = self.resolve(self.var_name, context)
+        perm = self.resolve(self.perm, context)
+        user = self.resolve(self.user, context)
+        granted = False
+        if not isinstance(user, AnonymousUser):
+            if self.approved:
+                check = get_check(user, perm)
+                if check is not None:
+                    granted = check(*objs)
+            else:
+                check = permissions.BasePermission(user=user)
+                for obj in objs:
+                    granted = check.requested_perm(perm, obj)
+                    if granted:
+                        break
+        context[var_name] = granted
+        return ''
+
+@register.tag
+def get_permission(parser, token):
+    """
+    Performs a permission check with the given signature, user and objects
+    and assigns the result to a context variable.
+
+    Syntax::
+
+        {% get_permission PERMISSION_LABEL.CHECK_NAME for USER and *OBJS [as VARNAME] %}
+
+        {% get_permission "poll_permission.change_poll" for request.user and poll as "is_allowed" %}
+        {% get_permission "poll_permission.change_poll" for request.user and poll,second_poll as "is_allowed" %}
+        
+        {% if is_allowed %}
+            I've got ze power to change ze pollllllzzz. Muahahaa.
+        {% else %}
+            Meh. No power for meeeee.
+        {% endif %}
+
+    """
+    return PermissionForObjectNode.handle_token(parser, token,
+                                                approved=True,
+                                                name='"permission"')
+
+@register.tag
+def get_permission_request(parser, token):
+    """
+    Performs a permission request check with the given signature, user and objects
+    and assigns the result to a context variable.
+
+    Syntax::
+
+        {% get_permission_request PERMISSION_LABEL.CHECK_NAME for USER and *OBJS [as VARNAME] %}
+
+        {% get_permission_request "poll_permission.change_poll" for request.user and poll as "asked_for_permissio" %}
+        {% get_permission_request "poll_permission.change_poll" for request.user and poll,second_poll as "asked_for_permissio" %}
+        
+        {% if asked_for_permissio %}
+            Dude, you already asked for permission!
+        {% else %}
+            Oh, please fill out this 20 page form and sign here.
+        {% endif %}
+
+    """
+    return PermissionForObjectNode.handle_token(parser, token,
+                                                 approved=False,
+                                                 name='"permission_request"')
+
+def base_link(context, perm, view_name):
+    return {
+        'next': context['request'].build_absolute_uri(),
+        'url': reverse(view_name, kwargs={'permission_pk': perm.pk,}),
+    }
+
+@register.inclusion_tag('authority/permission_delete_link.html', takes_context=True)
+def permission_delete_link(context, perm):
+    """
+    Renders a html link to the delete view of the given permission. 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_foreign_permissions') \
+            or user.pk == perm.creator.pk:
+            return base_link(context, perm, 'authority-delete-permission')
+    return {'url': None}
+
+@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():
+        link_kwargs = base_link(context, perm,
+                                'authority-delete-permission-request')
+        if user.has_perm('authority.delete_permission'):
+            link_kwargs['is_requestor'] = False
+            return link_kwargs
+        if not perm.approved and perm.user == user:
+            link_kwargs['is_requestor'] = True
+            return link_kwargs
+    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_requests'):
+            return base_link(context, perm,
+                'authority-approve-permission-request')
+    return {'url': None}

File authority/tests.py

+from django.test import TestCase
+from django.contrib.auth.models import User
+from django.contrib.auth.models import Permission as DjangoPermission
+
+import authority
+from authority import permissions
+from authority.models import Permission
+from authority.exceptions import NotAModel, UnsavedModelInstance
+
+class UserPermission(permissions.BasePermission):
+    checks = ('browse',)
+    label = 'user_permission'
+authority.register(User, UserPermission)
+
+class BehaviourTest(TestCase):
+    '''
+    self.user will be given:
+    - django permission add_user (test_add)
+    - authority to delete_user which is him (test_delete)
+
+    This permissions are given in the test case and not in the fixture, for
+    later reference.
+    '''
+    
+    fixtures = ['tests.json',]
+
+    def setUp(self):
+        self.user = User.objects.get(username='jezdez')
+        self.check = UserPermission(self.user)
+
+    def test_no_permission(self):
+        self.assertFalse(self.check.add_user())
+        self.assertFalse(self.check.delete_user())
+        self.assertFalse(self.check.delete_user(self.user))
+
+    def test_add(self):
+        # setup
+        perm = DjangoPermission.objects.get(codename='add_user')
+        self.user.user_permissions.add(perm)
+
+        # test
+        self.assertTrue(self.check.add_user())
+
+    def test_delete(self):
+        perm = Permission(
+            user=self.user,
+            content_object=self.user,
+            codename='user_permission.delete_user',
+            approved=True
+        )
+        perm.save()
+
+        # test
+        self.assertFalse(self.check.delete_user())
+        self.assertTrue(self.check.delete_user(self.user))
+
+class AssignBehaviourTest(TestCase):
+   '''
+   self.user will be given:
+   - permission add_user (test_add),
+   - permission delete_user for him (test_delete),
+   - all existing codenames permissions: a/b/c/d (test_all),
+   '''
+
+   fixtures = ['tests.json',]
+   
+   def setUp(self):
+       self.user = User.objects.get(username='jezdez')
+       self.check = UserPermission(self.user)
+
+   def test_add(self):
+       result = self.check.assign(check='add_user')
+       
+       self.assertTrue(isinstance(result[0], DjangoPermission))
+       self.assertTrue(self.check.add_user())
+
+   def test_delete(self):
+       result = self.check.assign(content_object=self.user, check='delete_user')
+
+       self.assertTrue(isinstance(result[0], Permission))
+       self.assertFalse(self.check.delete_user())
+       self.assertTrue(self.check.delete_user(self.user))
+   
+   def test_all(self):
+       result = self.check.assign(content_object=self.user)
+
+       self.assertTrue(isinstance(result, list))
+       self.assertTrue(self.check.browse_user())
+       self.assertTrue(self.check.delete_user())
+       self.assertTrue(self.check.add_user())
+       self.assertTrue(self.check.change_user())
+
+class GenericAssignBehaviourTest(TestCase):
+    '''
+    self.user will be given:
+    - permission add (test_add),
+    - permission delete for him (test_delete),
+    '''
+
+    fixtures = ['tests.json',]
+    
+    def setUp(self):
+        self.user = User.objects.get(username='jezdez')
+        self.check = UserPermission(self.user)
+
+    def test_add(self):
+        result = self.check.assign(check='add', generic=True)
+        
+        self.assertTrue(isinstance(result[0], DjangoPermission))
+        self.assertTrue(self.check.add_user())
+
+    def test_delete(self):
+        result = self.check.assign(content_object=self.user, check='delete', generic=True)
+
+        self.assertTrue(isinstance(result[0], Permission))
+        self.assertFalse(self.check.delete_user())
+        self.assertTrue(self.check.delete_user(self.user))
+
+class AssignExceptionsTest(TestCase):
+    '''
+    Tests that exceptions are thrown if assign() was called with inconsistent arguments.
+    '''    
+    
+    fixtures = ['tests.json',]
+
+    def setUp(self):
+        self.user = User.objects.get(username='jezdez')
+        self.check = UserPermission(self.user)
+    
+    def test_unsaved_model(self):
+        try:
+            self.check.assign(content_object=User())
+        except UnsavedModelInstance:
+            return True
+        self.fail()
+
+    def test_not_model_content_object(self):
+        try:
+            self.check.assign(content_object='fail')
+        except NotAModel:
+            return True
+        self.fail()

File authority/urls.py

+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('authority.views',
+    url(r'^permission/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$',
+        view='add_permission',
+        name="authority-add-permission",
+        kwargs={'approved': True}
+    ),
+    url(r'^permission/delete/(?P<permission_pk>\d+)/$',
+        view='delete_permission',
+        name="authority-delete-permission",
+        kwargs={'approved': True}
+    ),
+    url(r'^request/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$',
+        view='add_permission',
+        name="authority-add-permission-request",
+        kwargs={'approved': False}
+    ),
+    url(r'^request/approve/(?P<permission_pk>\d+)/$',
+        view='approve_permission_request',
+        name="authority-approve-permission-request"
+    ),
+    url(r'^request/delete/(?P<permission_pk>\d+)/$',
+        view='delete_permission',
+        name="authority-delete-permission-request",
+        kwargs={'approved': False}
+    ),
+)

File authority/views.py

+from datetime import datetime
+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 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 url_for_obj
+
+def get_next(request, obj=None):
+    next = request.REQUEST.get('next')
+    if not next:
+        if obj and hasattr(obj, 'get_absolute_url'):
+            next = obj.get_absolute_url()
+        else:
+            next = '/'
+    return next
+
+@login_required
+def add_permission(request, app_label, module_name, pk, approved=False,
+                   template_name = 'authority/permission_form.html',
+                   extra_context={}, form_class=UserPermissionForm):
+    codename = request.POST.get('codename', None)
+    model = get_model(app_label, module_name)
+    if model is None:
+        return permission_denied(request)
+    obj = get_object_or_404(model, pk=pk)
+    next = get_next(request, obj)
+    if approved:
+        if not request.user.has_perm('authority.add_permission'):
+            return HttpResponseRedirect(
+                url_for_obj('authority-add-permission-request', obj))
+        view_name = 'authority-add-permission'
+    else:
+        view_name = 'authority-add-permission-request'
+    if request.method == 'POST':
+        if codename is None:
+            return HttpResponseForbidden(next)
+        form = form_class(data=request.POST, obj=obj, approved=approved,
+                          perm=codename, initial=dict(codename=codename))
+        if not approved:
+            # Limit permission request to current user
+            form.data['user'] = request.user
+        if form.is_valid():
+            permission = form.save(request)
+            request.user.message_set.create(
+                message=_('You added a permission request.'))
+            return HttpResponseRedirect(next)
+    else:
+        form = form_class(obj=obj, approved=approved, perm=codename,
+                          initial=dict(codename=codename))
+    context = {
+        'form': form,
+        'form_url': url_for_obj(view_name, obj),
+        'next': next,
+        'perm': codename,
+        'approved': approved,
+    }
+    context.update(extra_context)
+    return render_to_response(template_name, context,
+                              context_instance=RequestContext(request))
+
+@login_required
+def approve_permission_request(request, permission_pk):
+    requested_permission = get_object_or_404(Permission, pk=permission_pk)
+    if request.user.has_perm('authority.approve_permission_requests'):
+        requested_permission.approve(request.user)
+        request.user.message_set.create(
+            message=_('You approved the permission request.'))
+    next = get_next(request, requested_permission)
+    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 = get_next(request)
+    return HttpResponseRedirect(next)
+
+def permission_denied(request, template_name=None, extra_context={}):
+    """
+    Default 403 handler.
+
+    Templates: `403.html`
+    Context:
+        request_path
+            The path of the requested URL (e.g., '/app/pages/bad_page/')
+    """
+    if template_name is None:
+        template_name = ('403.html', 'authority/403.html')
+    context = {
+        'request_path': request.path,
+    }
+    context.update(extra_context)
+    return HttpResponseForbidden(loader.render_to_string(template_name, context,
+                                 context_instance=RequestContext(request)))

File authority/widgets.py

+from django import forms
+from django.conf import settings
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.admin.widgets import ForeignKeyRawIdWidget
+
+generic_script = """
+<script type="text/javascript">
+function showGenericRelatedObjectLookupPopup(ct_select, triggering_link, url_base) {
+    var url = content_types[ct_select.options[ct_select.selectedIndex].value];
+    if (url != undefined) {
+        triggering_link.href = url_base + url;
+        return showRelatedObjectLookupPopup(triggering_link);
+    }
+    return false;
+}
+</script>
+"""
+
+class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
+    def __init__(self, ct_field, cts=[], attrs=None):
+        self.ct_field = ct_field
+        self.cts = cts
+        forms.TextInput.__init__(self, attrs)
+
+    def render(self, name, value, attrs=None):
+        if attrs is None:
+            attrs = {}
+        related_url = '../../../'
+        params = self.url_parameters()
+        if params:
+            url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.iteritems()])
+        else:
+            url = ''
+        if 'class' not in attrs:
+            attrs['class'] = 'vForeignKeyRawIdAdminField'
+        output = [forms.TextInput.render(self, name, value, attrs)]
+        output.append("""%(generic_script)s
+            <a href="%(related)s%(url)s" class="related-lookup" id="lookup_id_%(name)s" onclick="return showGenericRelatedObjectLookupPopup(document.getElementById('id_%(ct_field)s'), this, '%(related)s%(url)s');"> """
+             % {'generic_script': generic_script, 'related': related_url, 'url': url, 'name': name, 'ct_field': self.ct_field})
+        output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
+
+        from django.contrib.contenttypes.models import ContentType
+        content_types = """
+        <script type="text/javascript">
+        var content_types = new Array();
+        %s
+        </script>
+        """ % ('\n'.join(["content_types[%s] = '%s/%s/';" % (ContentType.objects.get_for_model(ct).id, ct._meta.app_label, ct._meta.object_name.lower()) for ct in self.cts]))
+        return mark_safe(u''.join(output) + content_types)
+
+    def url_parameters(self):
+        return {}
     license='BSD',
     url='http://bitbucket.org/jezdez/django-authority/',
     download_url='http://bitbucket.org/jezdez/django-authority/downloads/',
-    packages=find_packages('src'),
-    package_dir = {'': 'src'},
+    packages=find_packages(),
     classifiers=[
         'Development Status :: 3 - Alpha',
         'Environment :: Web Environment',

File src/authority/__init__.py

-import sys
-from authority.sites import site, get_check, get_choices_for, register, unregister
-
-LOADING = False
-
-def autodiscover():
-    """
-    Goes and imports the permissions submodule of every app in INSTALLED_APPS
-    to make sure the permission set classes are registered correctly.
-    """
-    global LOADING
-    if LOADING:
-        return
-    LOADING = True
-
-    import imp
-    from django.conf import settings
-
-    for app in settings.INSTALLED_APPS:
-        try:
-            __import__(app)
-            app_path = sys.modules[app].__path__
-        except AttributeError:
-            continue
-        try:
-            imp.find_module('permissions', app_path)
-        except ImportError:
-            continue
-        __import__("%s.permissions" % app)
-        app_path = sys.modules["%s.permissions" % app]
-    LOADING = False

File src/authority/admin.py

-from django import forms, template
-from django.http import HttpResponseRedirect
-from django.utils.translation import ugettext, ungettext, ugettext_lazy as _
-from django.shortcuts import render_to_response
-from django.utils.encoding import force_unicode
-from django.utils.safestring import mark_safe
-from django.forms.formsets import all_valid
-from django.contrib import admin
-from django.contrib.admin import helpers
-from django.contrib.contenttypes import generic
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import PermissionDenied
-
-try:
-    from django.contrib.admin import actions
-except ImportError:
-    actions = False
-
-from authority.models import Permission
-from authority.widgets import GenericForeignKeyRawIdWidget
-from authority import get_choices_for
-
-class PermissionInline(generic.GenericTabularInline):
-    model = Permission
-    raw_id_fields = ('user', 'group', 'creator')
-    extra = 1
-
-    def formfield_for_dbfield(self, db_field, **kwargs):
-        if db_field.name == 'codename':
-            perm_choices = get_choices_for(self.parent_model)
-            kwargs['label'] = _('permission')
-            kwargs['widget'] = forms.Select(choices=perm_choices)
-            return db_field.formfield(**kwargs)
-        return super(PermissionInline, self).formfield_for_dbfield(db_field, **kwargs)
-
-class ActionPermissionInline(PermissionInline):
-    raw_id_fields = ()
-    template = 'admin/edit_inline/action_tabular.html'
-
-class ActionErrorList(forms.util.ErrorList):
-    def __init__(self, inline_formsets):
-        for inline_formset in inline_formsets:
-            self.extend(inline_formset.non_form_errors())
-            for errors_in_inline_form in inline_formset.errors:
-                self.extend(errors_in_inline_form.values())
-
-def edit_permissions(modeladmin, request, queryset):
-    opts = modeladmin.model._meta
-    app_label = opts.app_label
-
-    # Check that the user has the permission to edit permissions
-    if not (request.user.is_superuser or
-            request.user.has_perm('authority.change_permission') or
-            request.user.has_perm('authority.change_foreign_permissions')):
-        raise PermissionDenied
-
-    inline = ActionPermissionInline(queryset.model, modeladmin.admin_site)
-    formsets = []
-    for obj in queryset:
-        prefixes = {}
-        FormSet = inline.get_formset(request, obj)
-        prefix = "%s-%s" % (FormSet.get_default_prefix(), obj.pk)
-        prefixes[prefix] = prefixes.get(prefix, 0) + 1
-        if prefixes[prefix] != 1:
-            prefix = "%s-%s-%s" % (prefix, prefixes[prefix])
-        if request.POST.get('post'):
-            formset = FormSet(data=request.POST, files=request.FILES,
-                              instance=obj, prefix=prefix)
-        else:
-            formset = FormSet(instance=obj, prefix=prefix)
-        formsets.append(formset)
-
-    media = modeladmin.media
-    inline_admin_formsets = []
-    for formset in formsets:
-        fieldsets = list(inline.get_fieldsets(request))
-        inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
-        inline_admin_formsets.append(inline_admin_formset)
-        media = media + inline_admin_formset.media
-
-    ordered_objects = opts.get_ordered_objects()
-    if request.POST.get('post'):
-        if all_valid(formsets):
-            for formset in formsets:
-                formset.save()
-        # redirect to full request path to make sure we keep filter
-        return HttpResponseRedirect(request.get_full_path())
-
-    context = {
-        'errors': ActionErrorList(formsets),
-        'title': ugettext('Permissions for %s') % force_unicode(opts.verbose_name_plural),
-        'inline_admin_formsets': inline_admin_formsets,
-        'root_path': modeladmin.admin_site.root_path,
-        'app_label': app_label,
-        'change': True,