Aggregation in Django - django

I have the following Model in Django:
class A(models.Model):
nr = models.IntegerField()
class B(models.Model):
points = models.IntegerField()
class C(models.Model):
a = models.ForeignKey(A, on_delete=models.CASCADE)
b = models.ForeignKey(B, on_delete=models.CASCADE)
So for every A there are many entries in C, and for every B there are also many entries in C. But for every entry in C there is exactly one entry in A and one in B.
I would like to sum up the B.points for a given A, but I have to go over C.
How can I do that in Django? I would know how to do that in SQL if that helps?

You can .annotate(..) [Django-doc] the As, like:
from django.db.models import Sum
A.objects.annotate(
total_points=Sum('c__b__points')
)
If you for example always want to annotate your A objects, you can define a manager for that:
class WithPointsManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
total_points=Sum('c__b__points')
)
and then define this manager on the A class, like:
class A(models.Model):
nr = models.IntegerField()
objects = WithPointsManager()
So now if you perform an A.objects.all(), the As will have a total_points attribute. Note that of course this will have a certain cost at the database side.
Then all A objects that arise from this QuerySet, will contain an extra .total_points attribute that contains the sum of all Bs that are related to a C that is related to that A.
Or for a given A object, you can .aggregate(..) [Django-doc], like:
from django.db.models import Sum
some_a.c_set.aggregate(
total_points=Sum('b__points')
)['total_points']
This will return the sum of the points of related B objects of the set of Cs that are related to some_a.

Related

Django: get related objects of related objects and pass to template

In a Django app I have three models:
class A(models.Model):
aId = models.AutoField(primary_key=True)
class B(models.Model):
bId = models.AutoField(primary_key=True)
aId = models.ForeignKey(A)
class C(models.Model):
cId = models.AutoField(primary_key=True)
bId = models.ForeignKey(B)
There is a on-to-many relation between A and B, as there is between B and C.
And there is a View class with context_data. In the template I need to show and filter Bs, with their Cs.
How can I pass All Bs related to an A and All Cs related to those Bs to my template (context)?
I tried to get Bs and Cs separately in two arrays, but it seems not to be a good idea because I can not categorize Cs by Bs.
Say you have an instance of A called a.
bs = a.b_set.all()
for b in bs:
cs = b.c_set.all()
The iteration over the elements might be done in the template itself.
In order to avoid multiple queries you can prefetch related objects.
So this would be the code for your view. I am not sure from which object(s) are given in the args/kwargs.
from django.views.generic import TemplateView
class YourView(TemplateView):
template_name = 'yourtemplate.html'
def get_context_data(self, **kwargs):
a = kwargs.get('a')
b = kwargs.get('b')
ctx = super().get_context_data(**kwargs)
ctx['all b related to a'] = a.b_set.all()
ctx['all c related to b'] = b.c_set.all()
return ctx
If you have to combine querysets, say multiple querysets of cs for each b as #s_puria suggested, you can use the UNION operator https://docs.djangoproject.com/en/1.11/ref/models/querysets/#union

Filter by custom QuerySet of a related model in Django

Let's say I have two models: Book and Author
class Author(models.Model):
name = models.CharField()
country = models.CharField()
approved = models.BooleanField()
class Book(models.Model):
title = models.CharField()
approved = models.BooleanField()
author = models.ForeignKey(Author)
Each of the two models has an approved attribute, which shows or hides the object from the website. If the Book is not approved, it is hidden. If the Author is not approved, all his books are hidden.
In order to define these criteria in a DRY manner, making a custom QuerySet looks like a perfect solution:
class AuthorQuerySet(models.query.QuerySet):
def for_site():
return self.filter(approved=True)
class BookQuerySet(models.query.QuerySet):
def for_site():
reuturn self.filter(approved=True).filter(author__approved=True)
After hooking up these QuerysSets to the corresponding models, they can be queried like this: Book.objects.for_site(), without the need to hardcode all the filtering every time.
Nevertheless, this solution is still not perfect. Later I can decide to add another filter to authors:
class AuthorQuerySet(models.query.QuerySet):
def for_site():
return self.filter(approved=True).exclude(country='Problematic Country')
but this new filter will only work in Author.objects.for_site(), but not in Book.objects.for_site(), since there it is hardcoded.
So my questions is: is it possible to apply a custom queryset of a related model when filtering on a different model, so that it looks similar to this:
class BookQuerySet(models.query.QuerySet):
def for_site():
reuturn self.filter(approved=True).filter(author__for_site=True)
where for_site is a custom QuerySet of the Author model.
I think, I've come up with a solution based on Q objects, which are described in the official documentation. This is definitely not the most elegant solution one can invent, but it works. See the code below.
from django.db import models
from django.db.models import Q
######## Custom querysets
class QuerySetRelated(models.query.QuerySet):
"""Queryset that can be applied in filters on related models"""
#classmethod
def _qq(cls, q, related_name):
"""Returns a Q object or a QuerySet filtered with the Q object, prepending fields with the related_name if specified"""
if not related_name:
# Returning Q object without changes
return q
# Recursively updating keywords in this and nested Q objects
for i_child in range(len(q.children)):
child = q.children[i_child]
if isinstance(child, Q):
q.children[i_child] = cls._qq(child, related_name)
else:
q.children[i_child] = ('__'.join([related_name, child[0]]), child[1])
return q
class AuthorQuerySet(QuerySetRelated):
#classmethod
def for_site_q(cls, q_prefix=None):
q = Q(approved=True)
q = q & ~Q(country='Problematic Country')
return cls._qq(q, q_prefix)
def for_site(self):
return self.filter(self.for_site_q())
class BookQuerySet(QuerySetRelated):
#classmethod
def for_site_q(cls, q_prefix=None):
q = Q(approved=True) & AuthorQuerySet.for_site_q('author')
return cls._qq(q, q_prefix)
def for_site(self):
return self.filter(self.for_site_q())
######## Models
class Author(models.Model):
name = models.CharField(max_length=255)
country = models.CharField(max_length=255)
approved = models.BooleanField()
objects = AuthorQuerySet.as_manager()
class Book(models.Model):
title = models.CharField(max_length=255)
approved = models.BooleanField()
author = models.ForeignKey(Author)
objects = BookQuerySet.as_manager()
This way, whenever the AuthorQuerySet.for_site_q() method is changed, it will be automatically reflected in the BookQuerySet.for_site() method.
Here the custom QuerySet classes perform selection at the class level by combining different Q objects, instead of using filter() or exclude() methods at the object level. Having a Q object allows 3 different ways of using it:
put it inside a filter() call, to filter a queryset in place;
combine it with other Q objects using & (AND) or | (OR) operators;
dynamically change names of keywords used in the Q objects by accessing its children attribute, which is defined in the superclass django.utils.tree.Node
The _qq() method defined in every custom QuerySet class takes care of prepending the specified related_name to all filter keys.
If we have a q = Q(approved=True) object, then we can have the following outputs:
self._qq(q) – is equivalent to self.filter(approved=True);
self._qq(q, 'author') – is equivalent to self.filter(author__approved=True)
This solution still has serious drawbacks:
one has to import and call custom QuerySet class of the related model explicitly;
for each filter method one has to define two methods filter_q (class method) and filter (instance method);
UPDATE: The drawback 2. can be partially reduced by creating filter methods dynamically:
# in class QuerySetRelated
#classmethod
def add_filters(cls, names):
for name in names:
method_q = getattr(cls, '{0:s}_q'.format(name))
def function(self, *args, **kwargs):
return self.filter(method_q(*args, **kwargs))
setattr(cls, name, function)
AuthorQuerySet.add_filters(['for_site'])
BookQuerySet.add_filters(['for_site'])
Therefore, if someone comes up with a more elegant solution, please suggest it. It would be very appreciated.

Django model A can has only one instance of model B

I have two models:
class ModelA(models.Model):
name = models.CharField(max_length=256)
class ModelB(models.Model):
user = models.ForeignKey(MyUser)
model_a = models.ForeignKey(ModelA)
points = models.IntegerField(default=0)
How can I prevent creating the same object? For example:
I have A, B, C instances of ModelA, and two users. In ModelB I need relationships like this:
User1 can has only one 'link' to A, one to B, etc.
User2 the same. He can only one 'link' to each ModelA instance.
Each User can has one record in ModelB associated with ModelA.
E.g. (User1, A, 100), (User2, A, 50)
But if I will try to do something like this
...create(user=User1, model_a=A, points=50)
I need to get from db records with user1, and A, and ad points, not creating another similiar model.
So you want all pairs of user and model_a to be unique. You can specify this in the metadata of the model using unique_together.
unique_together = (("driver", "restaurant"),)
This is a tuple of tuples that must be unique when
considered together. It’s used in the Django admin and is enforced at
the database level (i.e., the appropriate UNIQUE statements are
included in the CREATE TABLE statement).
Django documentation - unique_together
Therefore modify your model in following way:
class ModelB(models.Model):
user = models.ForeignKey(MyUser)
model_a = models.ForeignKey(ModelA)
points = models.IntegerField(default=0)
class Meta:
unique_together = (('user', 'model_a'),)

Filling model with 2 foreign keys on Django

Good evening fellows and happy workers day. I have this task, that is, filling an intermediate model with two foreign keys from two other models.
Class A:
string_attr = models.CharField()
string_attr_2 = models.CharField()
Class B:
string_attr_x = models.CharField()
string_attr_y = models.CharField()
Class C:
date_attr = models.DateField()
a = models.ForeignKey(A)
b = models.ForeignKey(B)
The task is given a object from Model A, create as many objects that the table from class B has on model C. That is, given that class B has 4 objects and with the object X from class A, create 4 objects on table C.
Class A is a Pacients Model and class B is a Vital Signs and class C is PacientVitalSign model, for any given Pacient, I want to show a Formset or anything like it, with every vital sign to set on the PacientVitalSign model.
I don't know if the point is clear enough, English is not my mother language. I've look django-extra-views to achieve this task but I haven't hit the right key.
Any ideas?
Thanks

Django, sum of fields in intermediary model with given queryset

I have 2 models, one of them has many to many relation with itself through other table like this.
class a(models.Model):
# fields
class b(models.Model):
from_a = models.ForeignKey(a)
to_a = models.ForeignKey(a)
count = models.PositiveIntegerField()
Now, what I wonder is, what is the best way of calculating sum of counts in b's where from_a is "something". This one seems trivial, but I can't figure it out.
from django.db.models import Sum
b.objects.filter(from_a__whatever='something').aggregate(Sum('count'))