Django access mixin for active users only

As of Django 1.9, a number of new view mixin classes have been included in the contrib.auth application. These make access control much clearer for classed based views. This approach has been available via the third-party app django-braces. It’s also been an approach I have hand-rolled in the past and is a welcome departure from decorator-based access control.

Currently, the three mixins provided by the contrib.auth app are:

As demonstrated in the documentation, it’s easy to incorporate these into your views:

from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
    permission_required = 'polls.can_vote'
    # Or multiple of permissions:
    permission_required = ('polls.can_open', 'polls.can_edit')

Theses mixin classes all inherit from a generic AccessMixin which makes rolling your own access-based mixins easy.

Shortcomings

One caveat I noticed recently is that if you have a registration system that marks users as inactive until they verify their email address, they will be redirected to the login page any time they want to access a protected view. This is confusing. It would be better to show them a separate page that explains the verification process and allows them to resend the activation email.

So the following mixin restricts views to only those users that are logged in and have active accounts (i.e. their User.is_active setting is True). If the user isn’t logged-in we redirect them to the login page. If they are logged-in but they haven’t activated their account, we redirect them to a separate view. In both situations, for further clarity we also use the Django messaging framework to send the user a message when the are redirected. This is useful if you have a registration system that initially creates inactive users and waits for them to verify their account (for example when using the django-registration application).

(Note that we could just use a mixin based on the UserPassesTestMixin for this but it doesn’t really give us the control we want in terms of redirects and messages)

from django.contrib.auth.mixins import AccessMixin
from django.contrib import messages
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from django.shortcuts import redirect
from django.contrib.auth.views import redirect_to_login


class ActiveOnlyMixin(AccessMixin):
    """
    A view mixin that only allows users that are active on the site.
    """
    permission_denied_message = ''
    not_activated_message = ''
    not_activated_redirect = ''

    def get_not_activated_message(self):
        return self.not_activated_message

    def handle_not_activated(self):
        """ Deal with users that are logged in but not activated yet. """
        message = self.get_not_activated_message()
        if self.raise_exception:
            raise PermissionDenied(message)
        messages.error(self.request, message)
        return redirect(self.get_not_activated_redirect())

    def get_not_activated_redirect(self):
        """ Get the url name to redirect to if the user isn't activated. """
        if not self.not_activated_redirect:
            raise ImproperlyConfigured(
                '{0} is missing the not_activated_redirect attribute. Define {0}.not_activated_redirect, or override '
                '{0}.get_not_activated_redirect().'.format(self.__class__.__name__))
        return self.not_activated_redirect

    def handle_no_permission(self):
        """ Overwrite to allow a message to be sent. """
        message = self.get_permission_denied_message()
        if self.raise_exception:
            raise PermissionDenied(message)
        messages.error(self.request, message)
        return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name())

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            return self.handle_no_permission()
        if not request.user.is_active:
            return self.handle_not_activated()
        return super(ActiveOnlyMixin, self).dispatch(request, *args, **kwargs)

and to use it:

from myapp.mixins import ActiveOnlyMixin

class MyView(ActiveOnlyMixin, View):
    permission_denied_message = 'You must be logged in to view this page'
    not_activated_message = 'You haven\'t activated your account yet'
    # You will need to implement this view
    not_activated_redirect = 'myotherapp:inactive_registration'