Django order_by specific order - django

Is it possible to replicate this kind of specific sql ordering in the django ORM:
order by
(case
when id = 5 then 1
when id = 2 then 2
when id = 3 then 3
when id = 1 then 4
when id = 4 then 5
end) asc
?

Since Django 1.8 you have Conditional Expressions so using extra is not necessary anymore.
from django.db.models import Case, When, Value, IntegerField
SomeModel.objects.annotate(
custom_order=Case(
When(id=5, then=Value(1)),
When(id=2, then=Value(2)),
When(id=3, then=Value(3)),
When(id=1, then=Value(4)),
When(id=4, then=Value(5)),
output_field=IntegerField(),
)
).order_by('custom_order')

It is possible. Since Django 1.8 you can do in the following way:
from django.db.models import Case, When
ids = [5, 2, 3, 1, 4]
order = Case(*[When(id=id, then=pos) for pos, id in enumerate(ids)])
queryset = MyModel.objects.filter(id__in=ids).order_by(order)

You could do it w/ extra() or more plain raw(), but they can not work well w/ more complex situation.
qs.extra(select={'o':'(case when id=5 then 1 when id=2 then 2 when id=3 then 3 when id=1 then 4 when id=4 then 5 end)', order_by='o'}
YourModel.raw('select ... order by (case ...)')
For your code, condition set is very limited, you could sort in Python easily.

Related

Django: Get latest N number of records per group

Let's say I have the following Django model:
class Team(models.Model):
name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
I want to write a query to fetch the latest N number of records per team name.
If N=1, the query is very easy (assuming I'm using postgres because it's the only DB that support distinct(*fields)):
Team.objects.order_by("name", "-created_at").distinct("name")
If N is greater than 1 (let's say 3), then it gets tricky. How can I write this query in Django?
Not sure how you can get duplicate names per team, since you have unique=True. But if you plan to remove that to support non-unique names, you can use subqueries like this:
top_3_per_team_name = Team.objects.filter(
name=OuterRef("name")
).order_by("-created_at")[:3]
Team.objects.filter(
id__in=top_3_per_team_name.values("id")
)
Although this can be a bit slow, so make sure you have the indexes setup.
Also to note, ideally this can be solved by using Window..[Django-doc] functions using DenseRank..[Django-doc] but unfortunately the latest django version can't filter on windows:
from django.db.models import F
from django.db.models.expressions import Window
from django.db.models.functions import DenseRank
Team.objects.annotate(
rank=Window(
expression=DenseRank(),
partition_by=[F('name'),],
order_by=F('created_at').desc()
),
).filter(rank__in=range(1,4)) # 4 is N + 1 if N = 3
With the above you get:
NotSupportedError: Window is disallowed in the filter clause.
But there is a plan to support this on Django 4.2 so theoretically the above should work once that is released.
I'm assuming you'll be getting your N from a get request or something, but as long as you have a number you can try limiting your queryset:
Team.objects.order_by("name", "-created_at").distinct("name")[:3] # for N = 3

Django ORM: Perform conditional `order_by`

Say one has this simple model:
from django.db import models
class Foo(models.Model):
n = models.IntegerField()
In SQL you can perform an order by with a condition
e.g.
select * from foo orber by n=7, n=17, n=3, n
This will sort the rows by first if n is 7, then if n is 14, then if n is 3, and then finally by n ascending.
How does one do the same with the Django ORM? It is not covered in their order_by docs.
You can work with a generic solution that looks like:
from django.db.models import Case, IntegerField, When, Value
items = [7, 17, 3]
Foo.objects.alias(
n_order=Case(
*[When(n=item, then=Value(i)) for i, item in enumerate(items)],
default=Value(len(items)),
output_field=IntegerField()
)
).order_by('n_order', 'n')
This thus constructs a conditional expression chain [Django-doc] that is used first, and if n is not one of these, it will fall back on ordering with n itself.
You can use .annotate() to assign records a custom_order value and use then .order_by() to order the queryset based on this value.
For example:
Foo.objects \
.annotate(custom_order=Case(
When(n=7, then=Value(0)),
When(n=17, then=Value(1)),
When(n=3, then=Value(2)),
default=Value(3),
output_field=IntegerField()
) \
.order_by('custom_order', 'n')

Django filter by many-to-many field doesn't work

There is a model of Article and it has many-to-many categories field. I want to filter it by that field but it doesn't work as i expected. For example:
MyModel.objects.filter(categories__id__in = [0, 1, 2])
it gets model with categories 0 and 1 even if it hasn't category with id 2.
i tried something like this:
MyModel.objects.filter(Q(categories__id = 0) & Q(categories__id = 1) & Q(categories__id = 2))
but it even doesn't work. it doesn't even gets model if it has all of this categories.
By the way, i don't want to use more than 1 filter method
So, is there any solution for me?
Thanks.
P.S: django AND on Q not working for many to many - the same question but author still doesn't get an answer.
You can count if it matches three Categorys and thus only retrieve items where the three match:
from django.db.models import Count
MyModel.objects.filter(
categories__id__in=[0, 1, 2]
).annotate(
category_count=Count('categories')
).filter(category_count=3)

Raw SQL in order by in django model query

I've a simple query like this (I want 1 BHK to come first, then 2BHK, then anything else)
select *
from service_options
order by case space when '1BHK' then 0 when '2BHK' then 1 else 2 end,
space
In Django, how to do it? I've a model named ServiceOption
I tried this but no luck.
ServiceOption.objects.order_by(RawSQL("case space when '1BHK' then 0 when '2BHK' then 1 else 2 end,space"), ()).all()
I don't want to execute raw query with something like
ServiceOption.objects.raw("raw query here")
In Laravel, something like this could easily be pulled off like this
Model::query()->orderByRaw('raw order by query here')->get();
Any input will be appreciated. Thank you in advance.
You can work with a .annotate(…) [Django-doc] and then .order_by(…) [Django-doc]:
from django.db.models import Case, IntegerField, Value, When
ServiceOption.objects.annotate(
sp=Case(
When(space='1BHK', then=Value(0)),
When(space='2BHK', then=Value(1)),
default=Value(2),
output_field=IntegerField()
)
).order_by('sp', 'space')
The raw query would come to this
SELECT *, CASE WHEN "service_options"."space" = 1BHK THEN 0 WHEN "service_options"."space" = 2BHK THEN 1 ELSE 2 END AS "sp" FROM "service_options" ORDER BY "sp" ASC, "service_options"."space" ASC
Since django-3.2 you can work with .alias(…) [Django-doc] to prevent calculating this both as column and in the ORDER BY clause:
from django.db.models import Case, IntegerField, Value, When
ServiceOption.objects.alias(
sp=Case(
When(space='1BHK', then=Value(0)),
When(space='2BHK', then=Value(1)),
default=Value(2),
output_field=IntegerField()
)
).order_by('sp', 'space')
The raw query would come to this
SELECT * FROM "service_options" ORDER BY CASE WHEN ("service_options"."space" = 1BHK) THEN 0 WHEN ("service_options"."space" = 2BHK) THEN 1 ELSE 2 END ASC, "service_options"."space" ASC

Django: How to get the list of month within a daterange in django query

Suppose I have query:
ExampleModel.objects.filter(some_datetime_field__gte=start, some_datetime_field__lte=end)
How do I get the list of all months present within "start" and "end" in the above mentioned query.
For example:
IF
start= 1/10/2018 and end=10/1/2019
Then the output will be:
OCTOBER
NOVEMBER
DECEMBER
JANUARY
Anyone any idea how to perform this?
Thank you in advance
You can extract months and then get their names
from django.db.models.functions import ExtractMonth
months = (
ExampleModel.objects
.filter(some_datetime_field__gte=start, some_datetime_field__lte=end)
.annotate(month=ExtractMonth('some_datetime_field'))
.values_list('month', flat=True)
.distinct()
)
At the end of this code you'll have a list of months(numbers). for example
[1, 3, 6, 8]
And you can get their names using calendar
import calendar
[calendar.month_name[month] for month in months]
You can use annotation and Query Expressions.
import calendar
from django.db.models import Case, When, Value, CharField
conditions = []
for i in range(1, 13):
month_name = calendar.month_name[i]
conditions.append(When(some_datetime_field__month=i, then=Value(month_name)))
# conditions will be like below
# [
# When(some_datetime_field__month=1, then=Value('January')),
# When(some_datetime_field__month=2, then=Value('February')),
# ...
# ]
ExampleModel.objects.annotate(
month_name=Case(*conditions, default=Value(""), output_field=CharField())
).order_by("month_name").values_list("month_name", flat=True).distinct()
# Result will be like
# <ExampleModelQuerySet ['January', 'September']>