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

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

Related

Django: To combine and retrieve rows from different models

1.We have two similar models, and
I would like to be able to retrieve these at the same time and sort them by posting date and time, etc.
Is it possible?
2.Or should both redundant fields be combined into one model?
# 1.
class Model1(models.Model):
title = ...
thumbnail = ...
description = ...
...
class Model2(models.Model):
title = ...
image_url = ...
product_id = ...
reivew = ...
# 2. Combine into one model
class Model(models.Model)
title = ...
thumbnail = ...
description = ...
image_url = ...
product_id = ...
reivew = ...
You can union() two models with no shared parents, and after that order_by() some
column.
'Consider this answer from a beginner'
If you just want to fetch someones model1 and model2 objects with just one query statement maybe is good to inherit two models from a base model like this:
in your models.py:
class Base(models.Model):
title = ...
created_at = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(user)
class Meta:
ordering = ['-created_at']
# inherits fields of Base model
class Model1(Base):
field = ...
# fields
class Model2(Base):
# fields
using this method, remember fill needed fields of `Base' model like this:
>>> m1 = Model1(field="value", title=..., owner=UserObject,...).save()
# for get objects do normal:
>>> objects = Base.objects.filter(owner=user)
# the result is list of users `Model1' or 'Model2` created objects
Also you can use django-model-utils and get Base objects by child type. It would be like this:
from model_utils.managers import InheritanceManager
class Base(models.Model):
# like previous version
# just remember modify objects
objects = InheritanceManager()
# inherits fields of Base model
class Model1(Base):
class Model2(Base):
get objects:
>>> Base.objects.all().select_related('Model1', 'Model2')
please read also this answer and others.

How to chain model managers?

I have two abstract models:
class SoftDeleteModel(models.Model):
objects = SoftDeletableManager()
class Meta:
abstract = True
class BookAwareModel(models.Model):
book = models.ForeignKey(Book)
class Meta:
abstract = True
I use often use these models together for DRY purposes, e.g.:
class MyNewModel(SoftDeleteModel, BookAwareModel):
The SoftDeleteModel has a custom manager SoftDeletableManager():
class SoftDeletableManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_removed=False)
If I want to extend the BookAware abstract model to add a queryset filter on Books whilst still preserving the SoftDeletableManager() how would I go about this?
E.g. I can't add objects = BookManager() to BookAwareModel because it will overwrite the SoftDeletableManager.
Having played with your code a bit I came up with three possible solutions which seem to work (according to my tests):
Option 1:
Create a combined manager which is used when defining your concrete MyNewModel and use it for that model:
class CombiManager(SoftDeletableManager, BookAwareManager):
def get_queryset(self):
qs1 = SoftDeletableManager.get_queryset(self)
qs2 = BookAwareManager.get_queryset(self)
return qs1.intersection(qs2)
and then
class MyNewModel(SoftDeleteModel, BookAwareModel):
objects = CombiManager()
Option 2:
Create a Manager for the BookAware model as a subclass of the SoftDeleteableManager
class BookAwareManager(SoftDeletableManager):
def get_queryset(self):
return super().get_queryset().filter(your_filter)
and then add it to your BookAware model with a different name than 'objects':
class BookAwareModel(models.Model):
book = models.ForeignKey(Book)
book_objects = BookAwareManager()
class Meta:
abstract = True
allowing you to get the filtered queryset like
MyNewModel.book_objects.all()
Option 3
Put the BookAwareManager as in Option two as manager into your concrete MyNewModel. Then you can leave the managers name as the default 'objects'

Aggregation in 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.

Combine queries from 2 inherited Django models

I would like to perform a prefetch. The point is: I have a baseModel and inherited models that are liked to other models. But my baseModel isn't linked with them.
Here is the speudo code:
class Author(models.Model):
name = models.CharField()
class Movie(PolymorphicModel):
title = models.CharField()
author = models.ForeignKey('Author')
class EnglishMovie(Movie):
pass
class FrenchMovie(Movie):
pass
class Subtitle(models.Model):
movie = models.ForeignKey('FrenchMovie', related_name='subtitle')
text = models.CharField()
As you can see, FrenchMovie is linked to subtitles, but EnglishMovie isn't.
I would like to prefetch all my movie, and prefetch the subtitles as well.
I've tried several methods:
def get_queryset(self):
return Movie.objects.prefetch_related('author', 'subtitle').all()
I get: ValueError: Cannot query "EnglishMovie object (1)": Must be "FrenchMovie" instance.
def get_queryset(self):
q1 = Movie.objects.prefetch_related('author').all()
q2 = FrenchMovie.objects.prefetch_related('subtitle').all()
return q1 | q2
I get: AssertionError: Cannot combine queries on two different base models.
Any tips ? Thanks a lot.

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.