Reputation: 457
I'm populating a dropdown with values defined in my models.py, something like:
rank_choices = (
('King', 'King')
('Prince', 'Prince')
('Duke', 'Duke')
('Baron', 'Baron')
('Lackey', 'Lackey')
When I'm displaying a list of people with a rank at a particular time, I want them to appear in descending rank.
Current query (merely alphabetic):
attendees = Rank.objects.filter(feast__id=feast__id).exclude(rank='Lackey').order_by('rank')
How could I change this to order rank by its position in the list?
I'm considering this:
rank_choices = (
(0, 'King')
(1, 'Prince')
(2, 'Duke')
(3, 'Baron')
(4, 'Lackey')
But even if I can get the verbose values back from the numeric values, any changes in the ordering would return incorrect values for data pre-change. Is there a better way?
Chosen solution
Inspired by wim's answer, I ended up doing a data migration to change the ranks to numeric values and sorting as follows.
ranks = sorted(ranks, key=lambda x: int(x.rank))
I'm getting the verbose values back by importing the rank_choices from my models into my views and replacing the numeric values with the corresponding titles after the sorting.
Upvotes: 1
Views: 2201
Reputation: 362537
It looks like you have rank
stored in a CharField in database. It's not simple to do a custom order_by without changing the schema. Therefore, for simple cases, you may consider to make the ordering in python code:
rank_lookup = {rank: n for n,(rank,rank) in enumerate(rank_choices[::-1])}
ranks = Rank.objects.filter(...).values_list('rank', flat=1)
ranks = sorted(ranks, key=rank_lookup.get)
After the call to sorted
, the queryset will be evaluated and you will have instead a python list.
If this is not satisfactory, it is possible (but not pretty) in the Django ORM to get the result you want in a queryset by using a Case
/When
construct:
>>> for rank, rank in rank_choices:
... Rank.objects.create(rank=rank)
...
>>> Rank.objects.order_by('rank') # alphabetical
[<Rank: Baron>, <Rank: Duke>, <Rank: King>, <Rank: Lackey>, <Rank: Prince>]
>>> rank_lookup = {rank: n for n,(rank,rank) in enumerate(rank_choices)}
>>> cases = [When(rank=rank, then=Value(rank_lookup[rank])) for rank in rank_lookup]
>>> cases
[<When: WHEN <Q: (AND: ('rank', 'King'))> THEN Value(0)>,
<When: WHEN <Q: (AND: ('rank', 'Lackey'))> THEN Value(4)>,
<When: WHEN <Q: (AND: ('rank', 'Baron'))> THEN Value(3)>,
<When: WHEN <Q: (AND: ('rank', 'Prince'))> THEN Value(1)>,
<When: WHEN <Q: (AND: ('rank', 'Duke'))> THEN Value(2)>]
>>>
>>> Rank.objects.annotate(my_order=Case(*cases, output_field=IntegerField())).order_by('my_order')
[<Rank: King>, <Rank: Prince>, <Rank: Duke>, <Rank: Baron>, <Rank: Lackey>]
>>> Rank.objects.annotate(my_order=Case(*cases, output_field=IntegerField())).order_by('-my_order')
[<Rank: Lackey>, <Rank: Baron>, <Rank: Duke>, <Rank: Prince>, <Rank: King>]
Thanks to user "mu is too short" in the comments for inspiring this idea. This will work in Django 1.8+ because of the new features in conditional expressions.
For users unfortunate enough to be stuck on older versions of Django, the same idea is possible by constructing a raw sql fragment to pass in using Queryset.extra
.
Upvotes: 2