Group by and count foreign key - django

I have a data model where I store certain Events that happen. Each Event is linked to an EventType. The data model roughly looks like this:
class EventType(models.Model):
name = ...
class Event(models.Model):
date = ...
event_type = models.ForeignKey(EventType)
What I would like to know is how often each event time appeared. I tried it like this:
Event.objects.values('event_type', count=Count('event_type'))
But the result looks like this:
<QuerySet [{'count': 1, 'event_type': 71}, {'count': 1, 'event_type': 2}, {'count': 1, 'event_type': 71}, {'count': 1, 'event_type': 71}, ...
So the entries did not get grouped. How can I make it such that they are grouped?

You can use following query
Event.objects.all().values('event_type').annotate(count=Count('event_type'))

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.

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

Django: bulk update from a list of dicts without constructing the whole query set

I have a list which contains dicts. Something like:
[{'id': 0, 'price': 20}, {'id': 1, 'price': 10}] # a few thousands of elements
how can I update corresponding models without constructing the whole QuerySet?
As of django-2.2, you can use .bulk_update(…) [Django-doc]:
data = [{'id': 0, 'price': 20}, {'id': 1, 'price': 10}]
Match.objects.bulk_update([Match(**kv) for kv in data], ['price'])
We here thus construct Match objects that we then pass to the bulk_update to construct an update query.

Django annotate on property field

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

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.