Django - Query count of each distinct status - django

I have a model Model that has Model.status field. The status field can be of value draft, active or cancelled.
Is it possible to get a count of all objects based on their status? I would prefer to do that in one query instead of this:
Model.objects.filter(status='draft').count()
Model.objects.filter(status='active').count()
Model.objects.filter(status='cancelled').count()
I think that aggregate could help.

Yes, you can work with:
from django.db.models import Count
Model.objects.values('status').annotate(
count=Count('pk')
).order_by('count')
This will return a QuerSet of dictionaries:
<QuerySet [
{'status': 'active', 'count': 25 },
{'status': 'cancelled', 'count': 14 },
{'status': 'draft', 'count': 13 }
]>
This will however not list statuses for which no Model is present in the database.
Or you can make use of an aggregate with filter=:
from django.db.models import Count, Q
Model.objects.aggregate(
nactive=Count('pk', filter=Q(status='active')),
ncancelled=Count('pk', filter=Q(status='cancelled')),
ndraft=Count('pk', filter=Q(status='draft'))
)
This will return a dictionary:
{
'nactive': 25,
'ncancelled': 25,
'ndraft': 13
}
items for which it can not find a Model will be returned as None.

Related

How to retrieve integer from database objects

I'm fairly new to Django and try to get a database for scientific reviews up and running. One of the things I would like to show is how many reviews have been published in each year.
In this example I used the following data:
year: number of reviews published
1999: 1
2019: 4
2022: 5
models.py
class Review(models.Model):
title = models.CharField(max_length=100,null=True)
doi = models.URLField(max_length=200,unique=True,null=True)
author = models.CharField("First author",max_length=100,null=True)
year = models.PositiveIntegerField(null=True)
views.py
from django.shortcuts import render
from .models import Review
from django.db.models import Count
def about(response):
reviews_year_count = Review.objects.values('year').annotate(Count('year'))
context = {'reviews_year_count':reviews_year_count}
return render(response, "main/about.html", context)
about.html
<html>
<body>
{{reviews_year_count|join:", "}}
</body>
</html>
output on localhost
Currently:
{'year': 1999, 'year__count': 1}, {'year': 2019, 'year__count': 4}, {'year': 2022, 'year__count': 5}
Aim:
1, 4, 5
What I can't figure out is how to get rid of the {'year': 1999, 'year__count': } and only have the integer value there. I looked at Django documentation pages. One of the things I tried was add defer() behind reviews_year_count = Review.objects.values('year').annotate(Count('year')) but calling defer() after values() doesn’t make sense according to documentation.
My aim is to use the data as input for a chart using chartsjs.
You can use python to get only list of values:
reviews_year_count = Review.objects.values('year').annotate(Count('year'))
reviews_year_count = [i['year__count'] for i in reviews_year_count]
It is possible to do with django ORM using values too

django model find where count on filter is minimum

Hi there I have A model which I save ip in four column:
class IPAbstract(Audit):
first = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(254)])
second = models.PositiveSmallIntegerField(validators=[MinValueValidator(0), MaxValueValidator(254)])
third = models.PositiveSmallIntegerField(validators=[MinValueValidator(0), MaxValueValidator(254)])
forth = models.PositiveSmallIntegerField(validators=[MinValueValidator(0), MaxValueValidator(254)])
class Meta:
abstract = True
which All my ips are start with 10.0.0.0,20.0.0.0,30.0.0.0,40.0.0.0
I want find which my ip range has minimum rows,
i can do this:
IPAbstract.objects.filter(first=10).count()
IPAbstract.objects.filter(first=20).count()
IPAbstract.objects.filter(first=30).count()
IPAbstract.objects.filter(first=40).count()
and compare them to each other to find which has minimum rows.
I want see if there is there another way to find my answer?
You can use:
from django.db.models import Count
IPAbstract.objects.values('first').annotate(
count=Count('pk')
).order_by('number')
This will generate a QuerySet of dictionaries that look like:
<QuerySet [
{ 'first': 30, 'count': 5 },
{ 'first': 10, 'count': 9 },
{ 'first': 40, 'count': 11 },
{ 'first': 20, 'count': 20 },
]>
values for first that do not appear in the IPAbstract records will of course not be listed.
You can thus obtain the dictionary with the smallest value for 'count' with:
from django.db.models import Count
IPAbstract.objects.values('first').annotate(
count=Count('pk')
).order_by('first', 'number').first()

Django filter to check if there are any other booking between the given dates

I am trying to make a hotel booking app. So I have two models, i.e., Room and Booking.
# models
class Room(models.Model):
name = models.CharField(max_length=50)
class Booking(models.Model):
room = models.ForeignKey(Room, on_delete=models.CASCADE)
booked_for_datetime = models.DateTimeField()
booked_till_datetime = models.DateTimeField()
I would like to inform if the room is unavailable when there is already another booking for the required room, or else if it is actually available then create a booking.
I am working with django rest framework, so I will have to apply these validation on the create and update method, maybe something like this:
# serializers
def roomAvailable(validated_data):
available = False
# ...
# validation check here...
# ...
return available
class BookingSerializer(serializers.ModelSerializer):
class Meta:
model = Booking
fields = '__all__'
def create(self, validated_data):
if roomAvailable(validated_data):
return Booking.objects.create(**validated_data)
else:
raise serializers.ValidationError({
"detail": "Room is not available for these dates."
})
def update(self, instance, validated_data):
if roomAvailable(validated_data):
...
instance.save()
else:
raise serializers.ValidationError({
"detail": "Room is not available for these dates."
})
return instance
Eg:
Room 101 was already booked between 01-12-2019 to 04-12-2019.
I would like to block the booking if someone's trying to book the same room (101)
between 29-11-2019 to 01-12-2019
between 29-11-2019 to 02-12-2010
between 30-11-2019 to 05-12-2019
between 01-12-2019 to 04-12-2019
between 02-12-2019 to 03-12-2019
between 04-12-2019 to 07-12-2019
For now this is how I am filtering, but I am not sure if this is the best approach as I might be missing other bookings which will lead to double bookings, and maybe there is another way to approach this problem?
def roomAvailable(validated_data):
available = True
...
if room.booking_set.filter(
Q(booked_till_datetime__date__gte=validated_data['booked_for_datetime'],
booked_till_datetime__date__lte=validated_data['booked_till_datetime']) |
Q(booked_for_datetime__date__gte=validated_data['booked_for_datetime'],
booked_for_datetime__date__lte=validated_data['booked_till_datetime']) |
Q(booked_for_datetime__date__gte=validated_data['booked_for_datetime'],
booked_till_datetime__date__lte=validated_data['booked_till_datetime']) |
Q(booked_for_datetime__date__lte=validated_data['booked_for_datetime'],
booked_till_datetime__date__gte=validated_data['booked_till_datetime'])
).exists():
available = False
return available
Could you please help me how can I check if there are any booking between the new dates?
Short answer
start_date = '2019-11-29'
end_date = '2019-12-01'
filter_params = dict(booked_for_datetime__date__lte=end_date, booked_till_datetime__date__gte=start_date) # just for redability
is_occupied = Booking.objects.filter(**filter_params, room__name=101).exists()
Notes:
1. is_occupied will be a boolean
2. I have used the date filter, since OP provided only date, not a datetime
Explanation
Someone booked the room (101) between 01-12-2019 and 06-12-2019, which is marked in Red. According to the conditions given in the OP, the available slots are marked in Green.
Logic
input "end time" should be less than available booked_for_datetime__date
input "start time" should be greater than available booked_till_datetime__date
Shell output
In [2]: date_list = [
...: dict(start='2019-11-29', end='2019-12-01'),
...: dict(start='2019-11-29', end='2019-12-02'),
...: dict(start='2019-11-30', end='2019-12-05'),
...: dict(start='2019-12-01', end='2019-12-04'),
...: dict(start='2019-12-02', end='2019-12-03'),
...: dict(start='2019-12-02', end='2019-12-04'),
...: dict(start='2019-12-03', end='2019-12-04'),
...: dict(start='2019-12-02', end='2019-12-05'),
...: dict(start='2019-12-04', end='2019-12-06'),
...: dict(start='2019-12-04', end='2019-12-07'),
...: dict(start='2019-12-06', end='2019-12-07'),
...: dict(start='2019-12-07', end='2019-12-10'),
...: dict(start='2019-11-29', end='2019-11-30'),
...: ]
In [3]:
In [3]: Booking.objects.all()
Out[3]: <QuerySet [<Booking: Booking object (1)>]>
In [4]: Booking.objects.all()[0].__dict__
Out[4]:
{'_state': <django.db.models.base.ModelState at 0x7efee64d5a58>,
'id': 1,
'room_id': 1,
'booked_for_datetime': datetime.datetime(2019, 12, 1, 2, 45, 9, tzinfo=<UTC>),
'booked_till_datetime': datetime.datetime(2019, 12, 5, 2, 45, 8, tzinfo=<UTC>)}
In [5]: # booked the room from 01-12-2019 to 05-12-2019
In [6]: for date in date_list:
...: start_date, end_date = date.values()
...: filter_params = dict(booked_for_datetime__date__lte=end_date, booked_till_datetime__date__gte=start_date)
...: is_occupied = Booking.objects.filter(**filter_params, room__name=101).exists()
...: if is_occupied:
...: print('Not Available on this range, start: {} end: {}'.format(start_date, end_date))
...: else:
...: print('Available on this range, start: {} end: {}'.format(start_date, end_date))
...:
Not Available on this range, start: 2019-11-29 end: 2019-12-01
Not Available on this range, start: 2019-11-29 end: 2019-12-02
Not Available on this range, start: 2019-11-30 end: 2019-12-05
Not Available on this range, start: 2019-12-01 end: 2019-12-04
Not Available on this range, start: 2019-12-02 end: 2019-12-03
Not Available on this range, start: 2019-12-02 end: 2019-12-04
Not Available on this range, start: 2019-12-03 end: 2019-12-04
Not Available on this range, start: 2019-12-02 end: 2019-12-05
Not Available on this range, start: 2019-12-04 end: 2019-12-06
Not Available on this range, start: 2019-12-04 end: 2019-12-07
Available on this range, start: 2019-12-06 end: 2019-12-07
Available on this range, start: 2019-12-07 end: 2019-12-10
Available on this range, start: 2019-11-29 end: 2019-11-30
Rather than looking for the bookings that overlap the given date range, it's easier to exclude those that don't:
def roomAvailable(validated_data):
...
bookings = room.booking_set.exclude(
booked_for_datetime__date__gt=validated_data['booked_till_datetime'],
).exclude(
booked_till_datetime__date__lt=validated_data['booked_for_datetime'],
)
return not bookings.exists()
In this way you are basically excluding from your queryset those bookings that end before or start after the given date period, if there is any bookings that don't respect this condition, then the room is not available.
Note that the two conditions need to live in two different exclude to have an OR between them, if you put them together what you will obtain is an AND
which is not what you want.
def roomAvailable(validated_data):
overlapping_booking_exists = Booking.objects.filter(
(
Q(booked_for_datetime__lte=validated_data["booked_for_datetime"])
& Q(booked_till_datetime__gt=validated_data["booked_for_datetime"])
) | (
Q(booked_for_datetime__lt=validated_data["booked_till_datetime"])
& Q(booked_till_datetime__gte=validated_data["booked_till_datetime"])
),
room_id=validated_data["room"]
).exists()
return not overlapping_booking_exists
first of All I suggest to use DateField instead of DateTimeField; if you're using PostgreSQL as DB you could also use DateRangeField.
This queryset is more or less what do you need
qs = Room.objects.exclude(
booking__booked_for_datetime__lte=validated_data,
booking__booked_till_datetime__gte=validated_data
)
If you need "just" to know if there're available rooms you can do
qs.Exists()
with
qs.values('id', 'name')
you get the id and name of all available rooms

Performance of django API view pagination

I am new to python django. I am using APIView. I am looking at pagination code. I have looked through many codes but in all those, i have a concern.
They all get all the data from the table and then paginate that data.
zones = Zone.objects.all()
paginator = Paginator(zones, 2)
page = 2
zones = paginator.page(page)
serializer = ZoneSerializer(zones, many=True)
return {"data": serializer.data, 'count': zones.paginator.count, "code": status.HTTP_200_OK, "message": 'OK'}
My expectation is that i don't get all the records and then paginate using paginator. Otherwise i will have to write my own code to handle it.
It is not true that it gets all the records from database.
Look at this ( using django shell ). Note the LIMIT:
from django.db import connection
from apps.question.models import Question
from django.core.paginator import Paginator
p = Paginator(Question.objects.all(),2)
print(connection.queries)
[]
p.page(1)[0] # Accessing one element in page
print(connection.queries)
[{'sql': 'SELECT COUNT(*) AS "__count" FROM "question"',
'time': '0.001'},
{'sql': 'SELECT <all fields> FROM "question" ORDER BY "question"."id" DESC LIMIT 2',
'time': '0.000'},
]
Note: I removed the list of all fields from the 2nd query so it fits nicely here.

Wrongly big numbers when use multiple of Sum, Count aggregations in annotate

I have these models:
User:
email = EmailField()
Payment:
user = ForeignKey(User)
sum = DecimalField()
GuestAccount:
user = ForeignKey(User)
guest = ForeignKey(User)
I want to get user emails, amount of money that came from every user
and number of its guests accounts.
My query:
User.objects.annotate(
money=Sum('payment__sum'),
guests_number=Count('guestaccount')
).values('email', 'money', 'guests_number')
But money and guests_number in the result of the query are bigger then they really are:
{'guests_number': 0, 'email': 'a#b.cd', 'money': None}
{'guests_number': 20, 'email': 'user1#mail.com', 'money': Decimal('6600.00')}
{'guests_number': 4, 'email': 'user1000#test.com', 'money': Decimal('2500.00')}
{'guests_number': 0, 'email': 'zzzz#bbbbb.com', 'money': None}
I noticed that I get correct data if I split the query into 2 separate queries:
User.objects.annotate(money=Sum('payment__sum')).values('email', 'money')
User.objects.annotate(guests_number=Count('guestaccount')).values('email', 'guests_number')
Correct result of 1st half:
{'email': 'a#b.cd', 'money': None}
{'email': 'user1#mail.com', 'money': Decimal('1650.00')}
{'email': 'user1000#test.com', 'money': Decimal('1250.00')}
{'email': 'zzzz#bbbbb.com', 'money': None}
Correct result of 2nd half:
{'email': 'a#b.cd', 'guests_number': 0}
{'email': 'user1#mail.com', 'guests_number': 4}
{'email': 'user1000#test.com', 'guests_number': 2}
{'email': 'zzzz#bbbbb.com', 'guests_number': 0}
Also I noticed that I can add distinct=True in Count aggregation:
User.objects.annotate(
money=Sum('payment__sum'),
guests_number=Count('guestaccount', distinct=True)
).values('email', 'money', 'guests_number')
It fixes guests_number:
{'guests_number': 0, 'email': 'a#b.cd', 'money': None}
{'guests_number': 4, 'email': 'user1#mail.com', 'money': Decimal('6600.00')}
{'guests_number': 2, 'email': 'user1000#test.com', 'money': Decimal('2500.00')}
{'guests_number': 0, 'email': 'zzzz#bbbbb.com', 'money': None}
Unfortunatly, there are no distinct parameter in Sum aggregation.
What is wrong with my query? How to fix these numbers getting bigger with every aggregation in annotate?
Raw SQL query investigation showed that the problem comes from multiple LEFT OUTER JOINs. So I ended up with raw SQL:
User.objects.extra(select={
"money": """
SELECT SUM("website_payment"."sum")
FROM "website_payment"
WHERE "website_user"."id" = "website_payment"."user_id"
""",
"guests_number": """
SELECT COUNT("guests_guestaccount"."id")
FROM "guests_guestaccount"
WHERE "website_user"."id" = "guests_guestaccount"."user_id"
""",
}
).values('email', 'money', 'guests_number')
But I need to annotate these fields into queried objects and extra don't do it.