Django Class-Based View Mixins: Part 2

It's been a long time since my last post (for a number of reasons) but I'll pick up where I left off in the series about class-based view mixins in Django.

Part 2: Sorting & Filtering

In this post, I'd like to share some mixins that I've found useful for adding functionality to the generic ListView -- sorting and filtering.

To implement sorting in a Django view, you'd normally have to do something like this:

class WidgetListView(ListView):

    def get_queryset(self):
        sort_by = self.request.GET.get('sort_by')
        order = self.request.GET.get('order')
        sort_field = 'id'
        if sort_by == 'name':
            sort_field = 'widget_name'
            qs = super(WidgetListView, self).get_queryset().order_by(
                       sort_field)
        if order == 'desc':
            qs = qs.reverse()
        return qs

Code like this can get repetitive if same functionality is required in several views, so let's try and impove on this a bit with a mixin:

class WidgetListView(ListView, SortMixin):
    default_sort_params = ('name', 'asc')

    def sort_queryset(self, qs, sort_by, order):
        if sort_by == 'name':
            qs = qs.order_by('widget_name')
        if order == 'desc':
            qs = qs.reverse()
        return qs

With the SortMixin, the sort_by and ordering parameters are extracted from the request and passed to the sort_queryset() method, so the implementation is much cleaner, and the sorting functionality is logcally separate from the rest of the view.

The way that sorting works, though, is still completely up to you. The sort_queryset() method has complete control over the result that is passed to the template.

Filtering comes with similar problems. Without a mixin, the view might look something like this:

class WidgetListView(ListView):
    def get_queryset(self):
        qs = super(WidgetListView, self).get_queryset()
        filter_param = self.request.GET.get('filter')
        if filter_param == 'draft':
            qs = qs.filter(published=None)
        return qs

Again, we can abstract this repetitive logic into a mixin:

class WidgetListView(ListView, FilterMixin):
    default_filter_param = 'all'

    def filter_queryset(self, qs, filter_param):
        if filter_param == 'draft':
            qs = qs.filter(published=None)
        return qs

Similarly to the SortMixin, the filter_param is extracted from the request and passed to the filter_queryset() method, which is responsible for filtering the queryset that will be rendered in the template.

The real value of these mixins, though, is that they can easily be combined to provide a ListView which has both sorting and filtering, whilst maintaining separate logic for each in their own methods. This makes the code easier to read, extend, and perhaps most importantly, easier to test.

Here's a full example which combines both of these mixins into one view:

class WidgetListView(ListView, SortMixin, FilterMixin):
    default_sort_params = ('name', 'asc')
    default_filter_param = 'all'

    def sort_queryset(self, qs, sort_by, order):
        if sort_by == 'name':
            qs = qs.order_by('widget_name')
        if order == 'desc':
            qs = qs.reverse()
        return qs

    def filter_queryset(self, qs, filter_param):
        if filter_param == 'draft':
            qs = qs.filter(published=None)
        return qs

Both of these mixins pass the relevent parameters to the template through get_context_data(), so you can display the currently active sort field or filter mode with no hassle.

The code for these mixins is currently available as a GitHub Gist. In the near future, I'll be rolling all the mixins in this series into a Django app that can be installed and reused.