DEV Community

Cover image for Ordering by Choice Label in Django
Constantine
Constantine

Posted on

Ordering by Choice Label in Django

Photo by Edu Grande on Unsplash

Let's say you have a Choice field on your model. And you want to order by that choice labels. It's a no-brainer if values and labels are the same. Because values are stored in the database and it's the one in charge of ordering.

from django.db.models import TextChoices


class State(TextChoices):
    STARTED = 'STARTED', 'Started'
    WORKING = 'WORKING', 'Working'
    FINISHED = 'FINISHED', 'Finished'
Enter fullscreen mode Exit fullscreen mode

This will work out of the box:

from django_filters import (
    FilterSet,
    ...,
    OrderingFilter,
)

class MyFilter(FilterSet):
    ...some fields filtering declaration...

    ordering = OrderingFilter(
        fields: {
            'state': 'state',
        }
    )

    class Meta:
        model = MyModel
Enter fullscreen mode Exit fullscreen mode

But if labels differ from values it just won't work. Because now the database doesn't know anything about labels.

from django.db.models import TextChoices


class State(TextChoices):
    STARTED = 'STARTED', 'Initiated'
    WORKING = 'WORKING', 'Operating'
    FINISHED = 'FINISHED', 'Stopped'
Enter fullscreen mode Exit fullscreen mode

What you'd need to do now is to sort State labels and then create Case > When > Then chains. This way the database will know how you want to order results (but it's still oblivious about the whole "labels" thing).

class MyOrderingFilter(OrderingFilter):
    def filter(self, qs, value):
        state = None
        if value:
            for idx, param in enumerate(value):
                if param.strip('-') == 'state':
                    state = value.pop(idx)

        qs = super(MyOrderingFilter, self).filter(qs, value)
        if state:
            sorted_states = {
                val[0]: idx
                for idx, val in enumerate(
                    sorted(
                        State.choices,
                        key=lambda x: x[1],
                        reverse='-' in state,
                    )
                )
            }
            qs = qs.order_by(
                Case(
                    *[
                        When(state=key, then=Value(val))
                        for key, val in sorted_states.items()
                    ]
                )
            )

        return qs
Enter fullscreen mode Exit fullscreen mode

And then use MyOrderingFilter instead of OrderingFilter in MyFilter from the snippet above. The magic happens inside sorted_states = {...} dict comprehension. We tell our DB that we want our values with label "Initiated" to be first (or rather zero) in order, values with "Operating" to be second and so on. And when we're building qs = qs.order_by(Case(...)) we're just constructing the query itself.

So now your ordering will work according to the choice labels and not it's values.

Top comments (0)