At s2k digital, we have various django models that have a field for state tracking. This could be a simple field with choices set, or when using django-fsm a so called FSMField, which is based on a CharField. An example models.py could look like this:

from django.db import models
from django_fsm import FSMField

class BlogPost(models.Model):
    STATE_CHOICES = (
        ("new", "new"),
        ("clearance", "in clearance"),
        ("correction", "correction requested"),
        ("done", "done"),
    )
    # Fields
    id = models.AutoField(primary_key=True)
    state = models.FSM(default="new", choices=STATE_CHOICES, blank=False, null=False)
    created_at = models.DateTimeField(auto_now_add=True)
    # some more very important fields
    
    class Meta:
        ordering = ["created_at"]

Most of the time, in addition to tracking state, these fields are also used for sorting. However, not always is it the lexicographic order of the fields’ values that should be used as the basis for sorting, but rather some custom order based on those values.

 

For example, in the above example, we would like to have the following order:

  1. correction
  2. clearance
  3. new
  4. done

Lets extend our models.py to reflect the desired ordering of states and add a custom manager BlogPostManager to our model:

class BlogPost(models.Model):
    STATE_CHOICES = (
        ("new", "new"),
        ("clearance", "in clearance"),
        ("correction", "correction requested"),
        ("done", "done"),
    )

    STATE_ORDERING = {
        "correction": 1,
        "clearance": 2,
        "new": 3,
        "done": 4,
    }

    ...
    ...

    objects = BlogPostManager()

    class Meta:
        # We want our BlogPosts to be ordered primarily according to
        # STATE_ORDERING, and secondarily by created_at
        ordering = ("created_at",)

In our custom BlogPostManager, we will then override the get_queryset method to return a queryset that is ordered according to STATE_ORDERING. We therefore have to annotate our queryset such that the included objects have an IntegerField called state_order, which is then used for ordering / sorting the queryset accordingly.

 

We can achieve all this by utilizing When and Case which are both provided by django.db.models. An example managers.py could look like this:

from django.db import models


class BlogPostManager(models.Manager):
    def get_queryset(self):
        qs = super().get_queryset()
        if qs.exists():
            state_ordering_pairs = self.model.STATE_ORDERING.items()
            whens = [
                models.When(schein__state=k, then=v) for k, v in state_ordering_pairs
            ]
            # annotate queryset with state_order 
            qs = qs.annotate(
                state_order=models.Case(
                    *whens, default=9999, output_field=models.IntegerField()
                )
            )
            # Make sure we apply the ordering specified in BlogPost's Meta class
            # after ordering by state_order
            ordering = ("state_order",) + self.model._meta.ordering
            qs = qs.order_by(*ordering)
        return qs

This will give us the desired ordering of our BlogPost objects. If you want to learn more about When and Case, you can read more about them in the Django documentation.
_

If you are looking for a Django related job, have feedback or questions, feel free to reach out to us on Twitter, LinkedIn or shoot us an email.