Django - Annotate with Case/When/Value and related objects - python

I have the two following models:
class Post(models.Model):
content = models.TextField()
class Vote(models.Model):
UP_VOTE = 0
DOWN_VOTE = 1
VOTE_TYPES = (
(UP_VOTE, "Up vote"),
(DOWN_VOTE, "Down vote"),
)
post = models.ForeignKey(Post, related_name="votes")
vote_type = models.PositiveSmallIntegerField(choices=VOTE_TYPES)
I would like to have a score property on Post that returns the sum of the values of the votes to that post, counting votes with UP_VOTE type as 1 and those with DOWN_VOTE as -1.
This is what I’ve tried:
# inside Post
#property
def score(self):
return (
self.votes.all()
.annotate(
value=Case(
When(vote_type=Vote.DOWN_VOTE, then=Value(-1)),
When(vote_type=Vote.UP_VOTE, then=Value(1)),
default=Value("0"),
output_field=models.SmallIntegerField(),
)
)
.aggregate(Sum("value"))["value__sum"]
)
However, this yields None. More specifically, without dereferencing ["value__sum"], this returns {'value__sum': None}.
Is using Case-When-Value the correct approach to my use case? If so, what’s wrong with the code I posted?

The sum of an empty set will be NULL/None by default. As of django-4.0, you can work with the default=… parameter [Django-doc]:
from django.db.models import F, Sum
#property
def score(self):
return self.votes.aggregate(total=Sum(-2*F('vote_type') + 1, default=0))['total']
Prior to django-4.0, you can work with Coalesce [Django-doc]:
from django.db.models import F, Sum, Value
from django.db.models.functions import Coalesce
#property
def score(self):
return self.votes.aggregate(
total=Coalesce(Sum(-2*F('vote_type') + 1), Value(0))
)['total']
although in this simple case, you can just replace None by 0 at the Django/Python layer:
from django.db.models import F, Sum
#property
def score(self):
return self.votes.aggregate(total=Sum(-2*F('vote_type') + 1))['total'] or 0
It might be better to use the "score" of a vote as value, so:
class Vote(models.Model):
UP_VOTE = 1
DOWN_VOTE = -1
# …
vote_type = models.SmallIntegerField(choices=VOTE_TYPES)
This will make the aggregation logic simpler, and will make it easier to later allow for example voting +5, -10, etc.

Related

I got the first result only of the for loop in Django views

i have a PositiveIntegerField in a model, in which i need to loop through that model to check all the values of this field and get its results to use it in my views..
The Problem is when i did that i just get the value of the first row in the database only!
models.py
class RoomType(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE)
room_type = models.ForeignKey(RoomTypesNames, on_delete=models.CASCADE)
room_capacity = models.PositiveIntegerField() ## Thats the field i wanna check its value
views.py
def SearchHotels(request):
x = None
z = None
t = None
if request.method == 'GET':
destination = request.GET.get('cityHotels')
numAdultStr = request.GET.get('numAdult')
numChild = request.GET.get('numChild')
numAdult = int(numAdultStr)
if destination:
q_city2 = Q(hotel__city__name__icontains = destination)
rooms2 = RoomType.objects.filter(q_city2)
################################
### next is my question:
if rooms2:
for r in rooms2:
if r.room_capacity < numAdult and numAdult % r.room_capacity == 0:
x = numAdult / r.room_capacity
### i want to loop through this query and check the values of 'room_capacity' in all models, but i only get the result of only the first row in my database
Probably you should get the last entry of your table unless your order_by is reversed. As #furas mentioned in comments, when you are dealing with multiple entry in a loop, its better to add the calculated values in a list.
But an alternative solution is to use annotate with conditional expression to use the DB to calculate the values for you:
from django.db.models import FloatField, IntegerField, ExpressionWrapper, F, Case, When, Value
room2 = RoomType.objects.filter(q_city2).annotate(
x_modulo=ExpressionWrapper(
numAdult % F('room_capacity'),
output_field=IntegerField()
)
).annotate(
x=Case(
When(
room_capacity__lt=numAdult,
x_modulo=0,
then=numAdult/F('room_capacity')
),
default_value=Value('0'),
output_field=FloatField()
)
)
all_x = []
for r in room2:
all_x.append(r.x)
print(all_x)
# or
print(room2.values('x'))
# filter usage
room2.filter(x__gt=0)
Explanation: In here, I am annotating of x_modulo which is modular value of numAdult and room_capacity. Then I am annotating the value of x which checks if room capacity is less than number of adults and value of x_modulo is 0. Then I am just annotating the fraction of numAdults and room_capacity.

How to update queryset value in django?

I have written a python script in my project. I want to update the value of a field.
Here are my modes
class News_Channel(models.Model):
name = models.TextField(blank=False)
info = models.TextField(blank=False)
image = models.FileField()
website = models.TextField()
total_star = models.PositiveIntegerField(default=0)
total_user = models.IntegerField()
class Meta:
ordering = ["-id"]
def __str__(self):
return self.name
class Count(models.Model):
userId = models.ForeignKey(User, on_delete=models.CASCADE)
channelId = models.ForeignKey(News_Channel, on_delete=models.CASCADE)
rate = models.PositiveIntegerField(default=0)
def __str__(self):
return self.channelId.name
class Meta:
ordering = ["-id"]
This is my python script:
from feed.models import Count, News_Channel
def run():
for i in range(1, 11):
news_channel = Count.objects.filter(channelId=i)
total_rate = 0
for rate in news_channel:
total_rate += rate.rate
print(total_rate)
object = News_Channel.objects.filter(id=i)
print(total_rate)
print("before",object[0].total_star,total_rate)
object[0].total_star = total_rate
print("after", object[0].total_star)
object.update()
After counting the total_rate from the Count table I want to update the total star value in News_Channel table. I am failing to do so and get the data before the update and after the update as zero. Although total_rate has value.
The problem
The reason why this fails is because here object is a QuerySet of News_Channels, yeah that QuerySet might contain exactly one News_Channel, but that is irrelevant.
If you then use object[0] you make a query to the database to fetch the first element and deserialize it into a News_Channel object. Then you set the total_star of that object, but you never save that object. You only call .update() on the entire queryset, resulting in another independent query.
You can fix this with:
objects = News_Channel.objects.filter(id=i)
object = objects[0]
object.total_star = total_rate
object.save()
Or given you do not need any validation, you can boost performance with:
News_Channel.objects.filter(id=i).update(total_star=total_rate)
Updating all News_Channels
If you want to update all News_Channels, you actually better use a Subquery here:
from django.db.models import OuterRef, Sum, Subquery
subq = Subquery(
Count.objects.filter(
channelId=OuterRef('id')
).annotate(
total_rate=Sum('rate')
).order_by('channelId').values('total_rate')[:1]
)
News_Channel.objects.update(total_star=subq)
The reason is that object in your case is a queryset, and after you attempt to update object[0], you don't store the results in the db, and don't refresh the queryset. To get it to work you should pass the field you want to update into the update method.
So, try this:
def run():
for i in range(1, 11):
news_channel = Count.objects.filter(channelId=i)
total_rate = 0
for rate in news_channel:
total_rate += rate.rate
print(total_rate)
object = News_Channel.objects.filter(id=i)
print(total_rate)
print("before",object[0].total_star,total_rate)
object.update(total_star=total_rate)
print("after", object[0].total_star)
News_Channel.total_star can be calculated by using aggregation
news_channel_obj.count_set.aggregate(total_star=Sum('rate'))['total_star']
You can then either use this in your script:
object.total_star = object.count_set.aggregate(total_star=Sum('rate'))['total_star']
Or if you do not need to cache this value because performance is not an issue, you can remove the total_star field and add it as a property on the News_Channel model
#property
def total_star(self):
return self.count_set.aggregate(total_star=Sum('rate'))['total_star']

The `length` and `name` sort params do not meet my requirement

I have a PhysicalServer model:
class PhysicalServer(models.Model):
name = models.CharField(max_length=32)
cabinet = models.ForeignKey(to=Cabinet, on_delete=models.DO_NOTHING, related_name="physical_servers")
physical_server_model = models.ForeignKey(to=PhysicalServerModel, null=True, on_delete=models.DO_NOTHING)
...
class Meta:
ordering = ['-cabinet', '-physical_server_model', 'name']
its list API view is this:
class PhysicalServerListAPIView(ListAPIView):
serializer_class = PhysicalServerListSerializer
permission_classes = [AllowAny]
pagination_class = CommonPagination
def get_queryset(self):
qs = PhysicalServer.objects.filter(**filters)
return qs.annotate(length=Length('name')).order_by('length', 'name') # there if I put the `name` first(order_by('name', 'length')), also inconformity my requirement.
my physicalserver instance name like this below:
My question is, when I use this for list sort:
return qs.annotate(length=Length('name')).order_by('length', 'name')
the result will be:
SE01-A1
SE01-A2
SE01-A3
...
SE01-A9
SE01-C1
SE01-C2
SE01-C3
...
SE01-A10
SE01-A11
SE01-A12
...
if I use the below for sort:
return qs.annotate(length=Length('name')).order_by('name', 'length')
the result will be:
SE01-A1
SE01-A11
SE01-A12
SE01-A13
...
SE01-A2
SE01-A21
...
SE01-A3
...
How can I sort like this:
SE01-A1
SE01-A2
SE01-A3
SE01-A4
...
SE01-A10
...
SE01-C1
SE01-C2
...
?
You'll need to extract the numbers from the strings. Postgres and mysql provide regexp_replace function (I'm not sure about other databases). But Django doesn't provide an implementation, so we'll write our own function:
from django.db.models import Func, Value
class RegexpReplace(Func):
function = 'REGEXP_REPLACE'
def __init__(self, expression, search, replace, **extra):
search = Value(search)
replace = Value(replace)
super(RegexpReplace, self).__init__(expression, search, replace, **extra)
I'll assume you want to split the names until the digits at the end, and then you want to sort using this first half, and then the numbers at the end. (This will work fine for you until you start getting 3-digit numbers before the hyphen, i.e. SE99-A1, SE100-A1).
from django.db.models import F, IntegerField
from django.db.models.functions import Cast
qs = ... # get your queryset
qs.annotate(
letters=RegexpReplace(F('name'), '(.*[a-zA-Z])[0-9]+$', r'\1'),
seq=Cast(
RegexpReplace(F('name'), '.*[a-zA-Z]([0-9]+)$', r'\1'),
IntegerField(),
),
).order_by('letters', 'seq')
Use Substr to separate out the first part of the name from the numbers at the end, and then sort by this new annotation first.
from django.db.models.functions import Substr, Length
qs = qs.annotate(letters=Substr('name', 1, 6), length=Length('name'))
qs = qs.order_by('letters', 'length', 'name')
return qs

Rank of users - Django

How can I rank the users based on their total number of posts.
example First : total number of posts 1-10
Second : total number of posts 11-20
Third : total number of posts 21-30
models.py
from django.db import models
User = get_user_model()
class Post(models.Model):
author = models.ForeignKey(User, on_delete = models.CASCADE)
title = models.CharField(max_length=200)
text = models.TextField()
def get_absolute_url(self):
return reverse("post_detail",kwargs={'pk':self.pk})
def __str__(self):
return self.title
def count_posts_of(user):
return Post.objects.filter(author=user).count()
Ordering Users by the number of posts
We can annotate a User with the number of Posts, and then use order_by on that annotation:
from django.db.models import Count
User.objects.annotate(nposts=Count('post')).order_by('-nposts')
Here the dash (-) means that we sort in descending order (so from many posts to few posts). If you remove the dash, then it orders in ascending order.
Assigning a numerical rank to the Users
We can also assign a numerical rank to each user (so 1-10 map to 1, 11-20 map to 2, etc.) by adding some extra annotation:
from django.db.models import Count, F
from django.db.models.expressions import Func
User.objects.annotate(
nposts=Count('post'),
nrank=Func(F('nposts') / 10, function='CEIL'),
).order_by('-nposts')
Mapping the numeric rank to an textual rank
We can map this rank to a textual rank, by defining for example a #property:
from django.contrib.auth import User
RANK_TEXTS = ['zero', 'first', 'second', 'third']
def rank_text(self):
nrank = getattr(self, 'nrank', None)
if nrank is None:
nrank = (Post.objects(author=self).count() + 9) // 10
return RANK_TEXTS[nrank]
User.rank_text = property(rank_text)
So we monkey patch the User class such that it has a property rank_text. This will first look if we annotated the User with the nrank attribute. If that is not the case, we calculate the nrank manually. Finally we return the textual counterpart.
So we can for example query with:
u1 = User.objects.first()
u1.rank_text # for example "second"
Se here [so-post] how to monkey patch Django models.

How to sort by many custom methods in Django Admin

I want to be able to sort by several custom methods in Django Admin. This question provides solution for one method only.
I tried to modify it:
from django.db import models
class CustomerAdmin(admin.ModelAdmin):
list_display = ('number_of_orders','number_of_somevalue') # added field
def queryset(self, request):
qs = super(CustomerAdmin, self).queryset(request)
qs = qs.annotate(models.Count('order'))
qs = qs.annotate(models.Count('somevalue')) # added line
return qs
def number_of_orders(self, obj):
return obj.order__count
number_of_orders.admin_order_field = 'order__count'
def number_of_somevalue(self, obj): # added method
return obj.somevalue__count
number_of_somevalue.admin_order_field = 'somevalue__count'
and it works incorrectly. It seems that it multiplies the count values instead of counting them separately.
Example:
I have 2 orders and 2 somevalues, but in the panel I see 4 orders and 4 somevalues.
Adding another method with yet another value makes it 8 (2*2*2).
How can I fix it?
You can try this to sort by many custom methods (Tested):
from django.db.models import Count
class CustomerAdmin(admin.ModelAdmin):
# The list display must contain the functions that calculate values
list_display = ('number_of_orders','number_of_somevalue') # added field
# Overwrite queryset in model admin
def queryset(self, request):
qs = super(CustomerAdmin, self).queryset(request)
# The query have to return multiple annotation, for this use distinct=True in the Count function
qs = qs.annotate(number_orders = Count('order', distinct=True)).annotate(number_somevalue = Count('somevalue',distinct=True))
return qs
# This function return the new field calculated in queryset (number_orders)
def number_of_orders(self, obj):
return obj.number_orders
number_of_orders.admin_order_field = 'numberorders' # sortable new column
# And this one will return the another field calculated (number_somevalue)
def number_of_somevalue(self, obj): # added method
return obj.number_somevalue
number_of_somevalue.admin_order_field = 'number_somevalue'# sortable new column

Categories

Resources