Django annotate on property field - django

I'm using Django 2.0 and Django REST Framework
I have a model like below.
class Contact(models.Model):
first_name = models.CharField(max_length=100)
class AmountGiven(models.Model):
contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
amount = models.FloatField(help_text='Amount given to the contact')
#property
def total_payable(self):
return self.amount
#property
def amount_due(self):
returned_amount = 0
for returned in self.amountreturned_set.all():
returned_amount += returned.amount
return self.total_payable - returned_amount
class AmountReturned(models.Model):
amount_given = models.ForeignKey(AmountGiven, on_delete=models.CASCADE)
amount = models.FloadField()
I have to get the top 10 contacts of the amount given and due respectively.
In my view, I'm filtering data like
#api_view(http_method_names=['GET'])
def top_ten(request):
filter_type = request.query_params.get('type', None)
if filter_type == 'due':
# query for due type
elif filter_type == 'given':
qs = Contact.objects.filter(
user=request.user
).values('id').annotate(
amount_given=Sum('amountgiven__amount')
).order_by(
'-amount_given'
)[:10]
graph_data = []
for q in qs:
d = {}
contact = Contact.objects.get(pk=q['id'])
d['contact'] = contact.full_name if contact else 'Unknown'
d['value'] = q['amount_given']
graph_data.append(d)
return Response(graph_data)
else:
raise NotFound('No data found for given filter type')
the type query can be due or given.
The code for given type is working fine as all fields are in the database. But how can I filter based on the virtual field for due type?
What I have to do is to annotate Sum of amount_due property group by contact.

You cannot filter based on #property.
As far as I understand your problem correctly you can aggregate sum of related AmountGiven and sum of AmountReturned, then calculate due field which keep result of subtracting letter and former.
The query:
from django.db.models import Sum, Value
from django.db.models.functions import Coalesce
Contact.objects.filter(
amountgiven__amount__gt=0
).annotate(
due=Sum('amountgiven__amount') - Coalesce(Sum('amountgiven__amountreturned__amount'), Value(0))
).order_by('-due').values_list('due', 'id')
will return:
<QuerySet [{'id': 3, 'due': 2500.0}, {'id': 1, 'due': 2450.0}, {'id': 2, 'due': 1500.0}]>
However with this solution you cannot distinct between many AmountGiven across one Contact. You get big picture like results.
If you want split due value per AmountGiven instance the just annotate like so:
AmountGiven.objects.annotate(
due=Sum('amount') - Coalesce(Sum('amountreturned__amount'), Value(0))
).order_by('-due').values_list('due', 'contact__id', 'id')
which returns
<QuerySet [
{'contact__id': 3, 'id': 3, 'due': 2500.0},
{'contact__id': 1, 'id': 1, 'due': 1750.0},
{'contact__id': 2, 'id': 2, 'due': 1500.0},
{'contact__id': 1, 'id': 4, 'due': 350.0},
{'contact__id': 1, 'id': 5, 'due': 350.0}
]>
References
Coalesce

Related

Convert raw sql query to django orm

I written this query in PostgreSQL and I'm confused of conversion of this query to django orm
SELECT count(*),
concat(date_part('month', issue_date), '/', date_part('year', issue_date) ) as date
FROM affiliates_issuelog
WHERE tenant_id = '{tenant_id}'
GROUP BY date_part('month', issue_date),
date_part('year', issue_date)
ORDER BY date_part('year', issue_date) desc,
date_part('month', issue_date) desc
I have this model that records the insertion of new affiliates by date and by institution (tenant), only I need to receive from the query the total amount of records inserted per month in the year, and I was using the listview to make my pages until then but I don't know how to filter this data using orm.
class IssueLog():
tenant = models.ForeignKey("tenants.Federation", on_delete=models.CASCADE)
issue_date = models.DateField(
default=date.today, verbose_name=_("date of issue")
)
class Meta:
verbose_name = _("Entrada de emissão")
verbose_name_plural = _("Entradas de emissão")
def __str__(self):
return f"{self.institution}, {self.tenant}"
My pages that return a list of data I did as the example below, is it possible to pass the data as I want through get_queryset()?, I already managed to solve my problem using the raw query, but the project is being done only with orm so I wanted to keep that pattern for the sake of the team. Ex:
class AffiliateExpiredListView(HasRoleMixin, AffiliateFilterMxin, ListView):
allowed_roles = "federation"
model = Affiliate
ordering = "-created_at"
template_name_suffix = "issued_list_expired"
paginate_by = 20
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["renew_form"] = AffiliateRenewForm()
tenant_t = self.request.user.tenant
context["cancel_presets"] = tenant_t.cancelationreason_set.all()
return context
def get_queryset(self):
return super().get_queryset().filter(is_expired=True).order_by('full_name')
You can query with:
from django.db.models import Count
from django.db.models.functions import ExtractMonth, ExtractYear
IssueLog.objects.values(
year=ExtractYear('issue_date'),
month=ExtractMonth('issue_date')
).annotate(
total=Count('pk')
).order_by('-year', '-month')
This will make a queryset with dictionaries that look like:
<QuerySet [
{'year': 2022, 'month': 2, 'total': 14},
{'year': 2022, 'month': 1, 'total': 25},
{'year': 2021, 'month': 12, 'total': 13}
]>
I would not do string formatting in the database query, but just do this in the template, etc.
But the model can not be abstract = True [Django-doc]: that means that there is no table, and that it is only used for inheritance purposes to implement logic and reuse it somewhere else.

Accessing one model from within another in Django many-to-one using ForeignKey

Lets imagine we have two models (many-to-one model).
Code below shows that a reporter can have multiple articles
class Reporter(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField()
def __str__(self):
return "%s %s" % (self.first_name, self.last_name)
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField(null=True)
reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE, null=True)
def __str__(self):
return self.headline
Let's see what I have in my database on this model.
# Reporter.objects.all().values()
# <QuerySet [
# {'id': 1, 'first_name': 'John', 'last_name': 'Smith', 'email': 'john#example.com'},
# {'id': 2, 'first_name': 'Paul', 'last_name': 'Jones', 'email': 'paul#example.com'}
# ]>
# Article.objects.all().values()
# <QuerySet [
# {'id': 5, 'headline': "1st headline", 'pub_date': datetime.date(2005, 7, 29),
# 'reporter_id': 1},
# {'id': 6, 'headline': "2nd headline", 'pub_date': datetime.date(2006, 1, 17),
# 'reporter_id': 2},
# {'id': 7, 'headline': '3rd headline', 'pub_date': datetime.date(2005, 7, 27),
# 'reporter_id': 1}
# ]>
The first reporter has two publications and second has the only.
I need to get the list of all articles for each reporter.
I tried this way (according to django docs):
Article.objects.filter(reporter__first_name='John')
It's okay. It works. I also tried to instantiate the first reporter as 'r1' and then do this:
r1.article_set.all()
And this piece of code works too.
But as I'm new to django, I think that instantiating the first reporter as 'r1' and then making a query is a bit slow. It is because django makes me run r1.save() and then r1.article_set.all(). It looks like django makes 2 query into database (first query - to save an instance, the second query to run r1.article_set.all)
Is my point of view correct? And how to query all the reporter's articles as fast as Article.objects.filter(reporter__first_name='John') but using the Reporter object?
Thanks
I also tried to instantiate the first reporter as 'r1' and then do this:
r1.article_set.all()
And this piece of code works too. But as I'm new to django, I think that instantiating the first reporter as 'r1' and then making a query is a bit slow.
Yes, but Django can load the related articles all with a second query in bulk. We do this with .prefetch_related(…) [Django-doc]:
reporters = Reporter.objects.prefetch_related('article_set')
for reporter in reporters:
print(reporter.first_name)
print(reporter.article_set.all())
Instead of the N+1 queries that your implementations make (one query to fetch all reporters, and one query per reporter to fetch the related articles), this will make two queries: one to fetch all the reporters, and one to fetch all the articles related to one of these reporters. Django will then do some joining such that the articles related to the first reporter r1 end up in r1.article_set.all()

Is there a way to get the columns from a joined table in the model instance dict object?

t = PurchaseHeader.objects.first()
t.__dict__
{
'_state': <django.db.models.base.ModelState object at 0x7f4b34aa7fa0>,
'id': 3,
'ref': 'jhkh',
'goods': Decimal('-100.00'),
'discount': Decimal('0.00'),
'vat': Decimal('-20.00'),
'total': Decimal('-120.00'),
'paid': Decimal('-120.00'),
'due': Decimal('0.00'),
'date': datetime.date(2020, 11, 7),
'due_date': datetime.date(2020, 11, 14),
'period': '202007',
'status': 'c',
'created': datetime.datetime(2020, 11, 7, 15, 46, 48, 191772, tzinfo=<UTC>),
'cash_book_id': None,
'supplier_id': 1128,
'type': 'pc'
}
When I joined the supplier table I was disappointed to find that the columns are not included in the dict. Below, t.__dict__ is the same as above. I noticed that the Supplier model instance is cached inside of t._state so I guess I could create my own method which all models inherit from which does what i want - all the columns from all tables inside a dict. But I wondered if anybody knew a way of doing this sort of thing out of the box?
t = PurchaseHeader.objects.select_related("supplier").first()
t.__dict__
select_related's goal is actually to prefetch data so that it doesn't need to be fetched in a second query when accessing "supplier". Instead it already fetched this data using a join in the original query.
If you want to obtain a dict based of your model that also contains the data of a relation in it, your best bet is using ModelSerializer with a nested serializer. Assuming that your supplier model is called Supplier it would look something like this:
class SupplierSerializer(serializers.ModelSerializer):
class Meta:
model = Supplier
fields = ['name', 'other_field'] # Add more Supplier fields
class PurchaseHeaderSerializer(serializers.ModelSerializer):
supplier = SupplierSerializer(read_only=True)
class Meta:
model = PurchaseHeader
fields = ['supplied', 'vat', 'total'] # Add more PurchaseHeader fields
You can then use the PurchaseHeaderSerializer like this:
purchase_header = PurchaseHeader.objects.select_related("supplier").first()
the_dict_you_want = PurchaseHeaderSerializer(instance=purchase_header).data

How to get number of customers for each hour using django rest framework

I would like get number of customers for each hour of the day.
This my model:
class Client(models.Model):
id_track=models.IntegerField(default=0)
temps_attente=models.CharField(max_length=50)
date_entree = models.DateTimeField(auto_now_add=True)
camera = models.ForeignKey("Camera", on_delete=models.CASCADE)
class Meta:
verbose_name = "Client"
ordering = ['camera']
def __str__(self):
return self.temps_attente
I want something like this :
[
{date:02/09/2020 , hour:"09" ,nbrCustumer:30}
{date:02/09/2020 , hour:"10" ,nbrCustumer:22 }
{date:02/09/2020 , hour:"11" ,nbrCustumer:10}
{date:02/09/2020 , hour:"12h ,nbrCustumer:12}
]
You can use GROUP BY expression as
from django.db.models import Count
from django.db.models.functions import ExtractHour
from django.utils import timezone
result = Client.objects.filter(
date_entree__date=timezone.now().date()
).annotate(
hour=ExtractHour('date_entree')
).values('hour').annotate(count=Count('hour'))
The result will be as,
[{'hour': 2, 'count': 1}, {'hour': 18, 'count': 1}]
References
date lookup
ExtractHour
Django Group By
Count()

Django - annotate with multiple Count

I have a model called Post which has two fields upvotes and downvotes. Now, upvotes, downvotes are ManyToManyField to a Profile. This is the model:
class Post(models.Model):
profile = models.ForeignKey(Profile, on_delete=models.CASCADE)
title = models.CharField(max_length=300)
content = models.CharField(max_length=1000)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
subreddit = models.ForeignKey(Subreddit, on_delete=models.CASCADE)
upvotes = models.ManyToManyField(Profile, blank=True, related_name='upvoted_posts')
downvotes = models.ManyToManyField(Profile, blank=True, related_name='downvoted_posts')
So, I want to fetch all the posts such that they are in the order of
total(upvotes) - total(downvotes)
So I have used this query:
Post.objects.annotate(
total_votes=Count('upvotes')-Count('downvotes')
).order_by('total_votes')
The problem with this query is the total_votes is always turning out to be zero.
The below queries will explain the situation:
In [5]: Post.objects.annotate(up=Count('upvotes')).values('up')
Out[5]: <QuerySet [{'up': 1}, {'up': 3}, {'up': 2}]>
In [6]: Post.objects.annotate(down=Count('downvotes')).values('down')
Out[6]: <QuerySet [{'down': 1}, {'down': 1}, {'down': 1}]>
In [10]: Post.objects.annotate(up=Count('upvotes'), down=Count('downvotes'), total=Count('upvotes')-Count('downvotes')).values('up', 'down', 'total')
Out[10]: <QuerySet [{'up': 1, 'down': 1, 'total': 0}, {'up': 3, 'down': 3, 'total': 0}, {'up': 2, 'down': 2, 'total': 0}]>
Seems like both up and down are having the same value(which is actually the value of up). How can I solve this?
I have tried this:
In [9]: Post.objects.annotate(up=Count('upvotes')).annotate(down=Count('downvotes')).values('up', 'down')
Out[9]: <QuerySet [{'up': 1, 'down': 1}, {'up': 3, 'down': 3}, {'up': 2, 'down': 2}]>
but even this gives the same output.
Try to use dictinct argument:
Post.objects.annotate(
total_votes=Count('upvotes', distinct=True)-Count('downvotes', distinct=True)
).order_by('total_votes')
From the docs:
Combining multiple aggregations with annotate() will yield the wrong
results because joins are used instead of subqueries. For most
aggregates, there is no way to avoid this problem, however, the Count
aggregate has a distinct parameter that may help.
(I'm aware that this isn't exactly an answer, but code can't be embedded in a comment.)
A better data model would be
class Post:
# ...
class Vote:
voter = models.ForeignKey(Profile, on_delete=models.PROTECT)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
score = models.IntegerField() # either -1 or +1; validate accordingly
class Meta:
unique_together = [('voter', 'post'),]
This way you could count the current total score for a post simply with
Vote.objects.filter(post=post).aggregate(score=Sum('score'))
However, you should be well aware of the performance implications of doing this (or your original version for that matter) every time. It would be better to add a
score = models.IntegerField(editable=False)
field to the Post, that gets updated with the aggregate score every time a vote is created, modified or deleted.