Selecting distinct nested relation in Django - django

To describe the system quickly, I have a list of Orders. Each Order can have 1 to n Items associated with it. Each Item has a list of ItemSizes. Given the following models, which have been abbreviated in terms of fields for this question, my goal is to get a distinct list of ItemSize objects for a given Order object.
class ItemSize(models.Model):
name = models.CharField(max_length=10, choices=SIZE_CHOICES)
class Item(models.Model):
name = models.CharField(max_length=100)
sizes = models.ManyToManyField(ItemSize)
class OrderItem(models.Model):
order = models.ForeignKey(Order)
item = models.ForeignKey(Item)
class Order(models.Model):
some_field = models.CharField(max_length=100, unique=True)
So... if I have:
o = Order.objects.get(id=1)
#how do I use the ORM to do this complex query?
#i need o.orderitem_set.items.sizes (pseudo-code)

In your current set up, the answer by #radious is correct. However, OrderItems really shouldn't exist. Orders should have a direct M2M relationship with Items. An intermediary table will be created much like OrderItems to achieve the relationship, but with an M2M you get much simpler and more logical relations
class Order(models.Model):
some_field = models.CharField(max_length=100, unique=True)
items = models.ManyToManyField(Items, related_name='orders')
You can then do: Order.items.all() and Item.orders.all(). The query you need for this issue would be simplified to:
ItemSize.objects.filter(item__orders=some_order)
If you need additional data on the Order-Item relationship, you can keep OrderItem, but use it as a through table like:
class Order(models.Model):
some_field = models.CharField(max_length=100, unique=True)
items = models.ManyToManyField(Items, related_name='orders', through=OrderItem)
And you still get your simpler relationships.

ItemSize.objects.filter(items__orderitems__order=some_order)
Assuming you have reverse keys like:
ItemSize.items - reverse fk for all items with such size
Item.orderitems - reverse for all orderitems connected to item
Item.orders - you can guess ;)
(AFAIR that names would be choose by default, but I'm not sure, you have to test it)
More informations about reverse key queries are available in documentation.

Related

Filter an object that has multiple related objects in Django

Let's say I have two models that have one-to-many relationships as the code below.
I'd like to only get order objects that have more than one shipment object.
The only way I can think of is getting it through a list comprehension [order for order in Order.objects.all() if order.shipments.count() > 1] but it seems too inefficient.
Is there a better way of doing this query in Django?
class Order(models.Model):
name = models.CharField(max_length=20)
store = models.CharField(max_length=20)
class Shipment(models.Model):
order = models.ForeignKey(Order, related_name='shipments')
this should do it:
you can access shipments via the related name:
Order.objects.annotate(num_shipments=Count('shipments')).filter(num_shipments__gt=1)

Any way to update a ManyToMany field via a queryset?

Suppose I have an object:
survey = Survey.objects.all().first()
and I want to create a relationship between it and a group of objects:
respondents = Respondent.objects.all()
for r in respondents:
r.eligible_for.add(survey)
where eligible_for is an M2M field in the Respondent model.
Is there any way to do it in one pass, or do I need to loop over the queryset?
models.py
class Questionnaire(models.Model):
label = models.CharField(max_length=48)
organization = models.ForeignKey('Home.Organization', on_delete=models.PROTECT, null=True)
class Respondent(models.Model):
user = models.OneToOneField('Home.ListenUser', on_delete=models.CASCADE)
organization = models.ForeignKey('Home.Organization', on_delete=models.PROTECT)
eligible_for = models.ManyToManyField('Questionnaire', related_name='eligible_users', blank=True)
.add(…) [Django-doc] can take a variable number of items, and will usually add these in bulk.
You can thus add all the rs with:
survey.eligible_users.add(*respondents)
We here thus add the respondents to the relation in reverse. Notice the asterisk (*) in front of respondents that will thus perform iterable unpacking.
https://docs.djangoproject.com/en/2.2/ref/models/relations/
survey.eligible_users.set(respondents) is another way to do this without having to unpack the list.

How to filter same column on each intermediate table in Django join

I'd like to join many tables and filter each one on the same column. My example is for checking for soft deletion, but I could imagine other applications, like filtering by a specific date.
For example, if my models are:
from django.db import models
class Members(models.Model):
name = models.CharField(max_length=255)
deleted = models.BooleanField(default=False)
class Purchases(models.Model):
member = models.ForeignKey(Members, on_delete=models.CASCADE)
item = models.ForeignKey('Items', on_delete=models.CASCADE)
deleted = models.BooleanField(default=False)
class Items(models.Model):
company = models.ForeignKey('Companies', on_delete=models.CASCADE)
deleted = models.BooleanField(default=False)
class Companies(models.Model):
deleted = models.BooleanField(default=False)
Members make purchases, purchases contain items, items are made by companies. Say I want to count the number of unique companies a member has purchased from, but I want to check each table to see if it's deleted. I can do this:
Members.objects.filter(
deleted=True,
purchases__deleted=True,
purchases__item__deleted=True,
purchases__item__companies__deleted=True
).annotate(num_companies=Count('purchases__item__companies', distinct=True))
But it seems silly to repeat the __deleted==True for every column. Is there a more efficient way filter the same column in the same way on each intermediate table in a large join?

Query which spans and follows back several tables

My question is about creating a query which filter objects that are related through several intermediate tables. My relational database looks like this:
Any number of products can be uploaded by one user (one to many relationship). However, users also can rank products. A ranking can be completed by several users and a user can have several rankings (Many to many relationship). The same applies between Product and Ranking. I use explicit intermediate tables (Rank and Belong) which defines the M2M relationships by the through parameter, because they have additional information which describes the relationship.
The models code is something like this (I omitted irrelevant fields for simplicity):
class Product(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
belong= models.ManyToManyField(Ranking, through="Belong")
#...
#The M2M table which relates Product and Ranking
class Belong(models.Model):
ranking = models.ForeignKey(Ranking, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
#...
class Meta:
unique_together = (("ranking", "product"))
class Ranking(models.Model):
user= models.ManyToManyField(settings.AUTH_USER_MODEL, through="Rank")
created = models.DateTimeField(auto_now_add=True)
#...
#The M2M table which relates User and Ranking
class Rank(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
ranking = models.ForeignKey(Ranking, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
#...
class Meta:
unique_together = (("user", "ranking"))
#The AUTH_USER_MODEL, which is not included here
My question is: How can I create a query which filters products which has been ranked by a given user? This implies “following back” relations between Belong, Ranking and Rank tables. I tried this following the Django tutorial, but it didn´t work:
Product.objects.filter(belong__ranking__rank__user=”username”)
You're a bit confused between your M2M relationships, and their through models.
For example, I don't understand why your M2M from Product to Ranking is called "belong". It should be called "rankings". Your M2M from Ranking to User at least has the right basic name, but it points to many users so should be "users".
Nevertheless, the point is that when you follow the M2Ms, you don't need to take the through tables into account. And the other issue is that "user" is itself a model, so to compare with a username you would need to continue to follow to that field. So:
Product.objects.filter(belong__user__username="username")

How to retrieve a set of objects, filtered and ordered by fields of other objects, for which the desired object is a foreign key?

To rephrase the title to the context of my problem: How to retrieve a set of foods, filtered and ordered by fields of other objects, for which the food object is a foreign key?
I have the following models:
class Food(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
description = models.CharField(max_length=200, blank=True)
class DayOfFood(models.Model):
user = models.ForeignKey(User)
date = models.DateField()
unique_together = ("user", "date")
class FoodEaten(models.Model):
day = models.ForeignKey(DayOfFood, on_delete=models.CASCADE)
food = models.ForeignKey(Food, on_delete=models.CASCADE)
servings = models.FloatField(default=1)
I want to be able to retrieve the foods that a given user ate most recently. This collection of foods will be passed to a template so it must be a QuerySet to allow the template to loop over the food objects.
This is how far I got
days = DayOfFood.objects.filter(user=request.user)
foodeatens = FoodEaten.objects.filter(day__in=days)
foodeatens = foodeatens.order_by('day__date')
Now it feels like I am almost there, all the foods I want are contained in the FoodEaten objects in the resulting QuerySet. I do not know how to use "for ... in:" to get the food objects and still have them stored in a QuerySet. Is there a way to perform foreach or map to a QuerySet?
I do not want to rewrite the template to accept FoodEaten objects instead because the template is used by other views which do simply pass food objects to the template.
The solution
The answer from Shang Wang helped me write code that solves my problem:
days = DayOfFood.objects.filter(user=request.user)
foods = Food.objects.filter(
foodeaten__day__in=days,
foodeaten__day__user=request.user) \
.order_by('-foodeaten__day__date')
That could be done using chain of relations:
Food.objects.filter(foodeaten__day__in=days,
foodeaten__day__user=request.user) \
.order_by('foodeaten__day__date')
By the way, I'm not sure why do you have user on multiple models Food and DayOfFood. If you really need user relations on both of them, maybe make the field name more explicit with the user's role in each model, otherwise you will get confused very quickly.