Jeremy Satterfield
Coding, Brewing and Tulsa Life

Decorators vs Mixins for Django Class-Based Views

I've been huge fan of Django's class-based views (CBVs) since I first tried them out in Django 1.4.  While they're much more complicated then the classic function based views, once you understand how they work, they're much more powerful, flexible and allow for DRYer code. I highly recommend anyone who hasn't delved into CBVs take a look at Class Class-based views or GoDjango.

However, one of the early issues I ran into was for views that required Django user permissions. With function based views, Django's auth application provides decorators to check that users are logged in, have a specific permission, or pass other custom checks the developer can provide. You simply add the decorator to the view...

@permission_required('auth.change_user')
def user_list(request):
	...

And now any user that doesn't have the required permission is redirected to the login page.

For CBVs though, it's not quite that simple, it's the dispatch method that begins the process that you're used to picking up from with function based views. This means that to apply a decorator to the CBV, you have a couple of options.

You can override the dispatch method to apply the decorator:

class UserListView(ListView):
    model = User

    @my_custom_decorator
    def dispatch(self, request, *args, **kwargs):
        return super(UserListView, self).dispatch(request, *args, **kwargs)

You could write a reusable class view decorator:

def class_view_decorator(function_decorator):
    def deco(View):
        View.dispatch = method_decorator(function_decorator)(View.dispatch)
        return View
    return deco

@class_view_decorator(my_custom_decorator)
class UserListView(ListView):
    model = User

This second option is the one that I've lived with for a long time. It's relatively clean and DRY and not much different than what you're used to seeing with function views.

But recently I was pointed toward Django Braces. It's a library of commonly (and not-so-commonly) needed view decorator-like functionality in the form of mixins. I immediately realized that when thinking about the functionality these decorators provide, I was stuck in the old way of doing things rather than looks at these views from an OOP perspective.

Writing these decorators as mixins instead, makes them more flexible, extensible and DRYer, just like the move from function views to CBVs. You're able to build a base mixin that others can inherit from and extend. What would have been arguments to the decorator can are much cleaner as class attributes for the view that's extending it. You can even have views that apply affect the mixin on a one-off basis. Take for instance the <code>UserCheckMixin</code> below.

class UserCheckMixin(object):
    user_check_failure_path = ''  # can be path, url name or reverse_lazy

    def check_user(self, user):
        return True

    def user_check_failed(self, request, *args, **kwargs):
        return redirect(self.user_check_failure_path)

    def dispatch(self, request, *args, **kwargs):
        if not self.check_user(request.user):
            return self.user_check_failed(request, *args, **kwargs)
  return super(UserCheckMixin, self).dispatch(request, *args, **kwargs)

I now simply add the mixin to a view:

class UserListView(UserCheckMixin, ListView):
    model = User
    user_check_failure_path = 'auth_login'

    def check_user(self, user)
        # do some check against the user here and return True or False

You can extend it to build a more useful mixin:

class PermissionRequiredMixin(UserCheckMixin):
    user_check_failure_path = 'auth_login'
    permission_required = None

    def check_user(self, user):
        return user.has_perm(self.permission_required)

class UserListView(PermissionRequredMixin, ListView):
    model = User
    permission_required = 'auth.change_user'
    user_check_failure_path = 'auth_login'

As you can see, once you've defined the PermissionRequiredMixin, it's much cleaner, more object-oriented and I would argue more pythonic.

If you're relying on built-in decorators, the class_view_decorator is a pretty helpful tool to have in your codebase. But for anything custom you may be writing, it really is hard to beat a mixin.

(Comments)

Comments