Django group up by foreign keys - python

So i have two models:
class Product(models.Model):
name = models.CharField(_('Name'), max_length=200)
class Like(models.Model):
product = models.ForeignKey(Product, related_name='likes', on_delete=models.DO_NOTHING)
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='likes', on_delete=models.DO_NOTHING)
What I want is to get the total number of base products (<5 likes), standard products (<10 likes) and premium products.
I don't know how I should count FK of each product and then group them; and preferably in 1 query.
python 3.6, django 2.1, posgtresql

I don't understand how that should be acomplished in a single query, but you could try this:
from django.db.models import Count
qs = Product.objects.annotate(num_likes=Count('like'))
base_products = qs.filter(num_likes__lt=5)
standard_products = qs.filter(num_likes__gte=5, num_likes__lt=10)
premium_products = qs.filter(num_likes__gte=10)
This code gives you 3 QuerySet objects with the data you asked for.
If you want a single database query (it still includes both tables in the query) but don't mind loading the data in python lists instead of querysets (needs more memory and maybe even more time), then you could try:
from django.db.models import Count
qs = Product.objects.annotate(num_likes=Count('like'))
base_products = []
standard_products = []
premium_products = []
for p in qs:
if p.num_likes < 5:
base_products.append(p)
elif p.num_likes < 5:
standard_products.append(p)
else:
premium_products.append(p)

Related

How to get data from both sides of a many to many join in django

Let's say I have the following models:
class Well(TimeStampMixin, models.Model):
plate = models.ForeignKey(Plate, on_delete=models.CASCADE, related_name="wells")
row = models.TextField(null=False)
column = models.TextField(null=False)
class Meta:
unique_together = [["plate", "row", "column"]]
class Antibiotic(TimeStampMixin, models.Model):
name = models.TextField(null=True, default=None)
class WellConditionAntibiotic(TimeStampMixin, models.Model):
wells = models.ManyToManyField(Well, related_name="well_condition_antibiotics")
volume = models.IntegerField(null=True, default=None)
stock_concentration = models.IntegerField(null=True, default=None)
dosage = models.FloatField(null=True, default=None)
antibiotic = models.ForeignKey(
Antibiotic, on_delete=models.RESTRICT, related_name="antibiotics"
)
In plain english, there are a set of wells and each well can have multiple and many different types of antibiotics.
I'm trying to fetch the data of a given well and all of the antibiotics contained inside it.
I've tried WellConditionAntibiotic.objects.filter(wells__id=1).select_related('antibiotic')
which gives me this query:
SELECT
"kingdom_wellconditionantibiotic"."id",
"kingdom_wellconditionantibiotic"."created_at",
"kingdom_wellconditionantibiotic"."updated_at",
"kingdom_wellconditionantibiotic"."volume",
"kingdom_wellconditionantibiotic"."stock_concentration",
"kingdom_wellconditionantibiotic"."dosage",
"kingdom_wellconditionantibiotic"."antibiotic_id",
"kingdom_antibiotic"."id",
"kingdom_antibiotic"."created_at",
"kingdom_antibiotic"."updated_at",
"kingdom_antibiotic"."name"
FROM
"kingdom_wellconditionantibiotic"
INNER JOIN "kingdom_wellconditionantibiotic_wells" ON (
"kingdom_wellconditionantibiotic"."id" = "kingdom_wellconditionantibiotic_wells"."wellconditionantibiotic_id"
)
INNER JOIN "kingdom_antibiotic" ON (
"kingdom_wellconditionantibiotic"."antibiotic_id" = "kingdom_antibiotic"."id"
)
WHERE
"kingdom_wellconditionantibiotic_wells"."well_id" = 1
This gives me all of the antibiotic data, but none of the well data. So I tried
Well.objects.filter(pk=1).select_related(['well_condition_antibiotics', 'antibiotic']).query which errored.
How can I generate a django query to include all well data and all well antibiotic data?
Building up on your second attempt using Well, you will have to prefetch WellConditionAntibiotic and also select the related antibiotic like this:
from django.db.models import Prefetch
well = Well.objects.filter(pk=1).prefetch_related(
Prefetch(
"well_condition_antibiotics",
queryset=WellConditionAntibiotic.objects.select_related("antibiotic"),
)
)
Then you can just iterate through the related WellConditionAntibiotic entries with the corresponding antibiotic:
for well_condition_antiobiotic in well.well_condition_antibiotics.all():
print(well_condition_antiobiotic.antibiotic.name)
You can find more information about prefetch_related and Prefetch here..[Django-doc]

How to write Django query to grab all objects in descending order according to two number fields?

I'm using Django. i'm trying to write query according to the top rated products. i have product table. as you can see below.
class Product(models.Model):
user = models.ForeignKey(User, verbose_name=_("Owner"), on_delete=models.CASCADE)
name = models.CharField(_("Name"), max_length=150,null=True)
average_rating =models.DecimalField(_("average rating"), max_digits=10, decimal_places=2,null=True,blank=True)
total_reviews = models.IntegerField(_("total reviews "),default=0,null=True,blank=True)
is_remove = models.BooleanField(_("Remove"), default=False)
create_time = models.DateTimeField(_("Create time"), default=timezone.now)
Now i want to get all objects which have highest average rating and total count.
I have tried many things below. but none of them worked.
1 -
def get_all_top_rated_products(self):
query = self.filter(is_remove=False).order_by("total_reviews","average_rating")
print(query)
return query
2
def get_all_top_rated_products(self):
query = self.filter(is_remove=False).aggregate(Max('average_rating'),Max('total_reviews'))
print(query)
return query
You should order in descending order, you can do this by prefixing the fieldname with a minus (-):
def get_all_top_rated_products(self):
return self.filter(is_remove=False).order_by(
'-average_rating', '-total_reviews'
)

Django - how to decrease DB queries when iterating

I have a model ProductFilter in the project. An object of this model is a set of lookups defining a queryset. It's like a group of products - but just as a filter, not connected with a ForeignKey (ManyToManyField).
The product can be filtered in multiple ProductFilters and the first is taken to account.
It works like this:
User define their filters. Then assign to each filter some pricing (and other) rules.
For example filter with the name "Products under 10 USD" - rule - increase the price by 10%.
The problem is that when I render products or export them. I have to check for every product if they belong to any filter which means a lot of DB queries. There can be 100k products.
Do you have any ideas how to make it faster?
class Product(...):
...
#property
def price(self):
for filter in self.user.filters():
if filter.get_queryset().filter(pk=self.pk).exists():
return filter.rule.apply_rule(self.price)
class ProductFilter(BaseTimeStampedModel):
project = models.ForeignKey('projects.UserEshop', related_name='product_filters', on_delete=models.CASCADE)
order = models.IntegerField(default=0)
name = models.CharField(max_length=64)
categories = models.ManyToManyField('FeedCategory', blank=True)
brands = models.ManyToManyField('Brand', blank=True)
manufacturers = models.ManyToManyField('Manufacturer', blank=True)
import_price_no_vat_max = models.DecimalField(decimal_places=2, max_digits=64, null=True, blank=True)
import_price_no_vat_min = models.DecimalField(decimal_places=2, max_digits=64, null=True, blank=True)
name_icontains = models.CharField(max_length=128, null=True, blank=True)
def get_queryset(self):
# products for this project
qs = Product.objects.filter(source__user_eshop=self.project)
if self.categories.exists():
qs = qs.filter(category__in=self.categories.all().get_descendants(include_self=True))
if self.brands.exists():
qs = qs.filter(brand__in=self.brands.all())
if self.manufacturers.exists():
qs = qs.filter(manufacturer__in=self.manufacturers.all())
if self.import_price_no_vat_max is not None:
qs = qs.filter(import_price_no_vat__lte=self.import_price_no_vat_max)
if self.import_price_no_vat_min is not None:
qs = qs.filter(import_price_no_vat__gte=self.import_price_no_vat_min)
if self.name_icontains:
qs = qs.filter(name__icontains=self.name_icontains)
return qs
EDIT
My best idea is to create a ManyToManyField on ProductFilter to Product and everytime somethings changes I'll assign products to every filter. I'm just not sure if ManyToManyField is ok to hold thousands or hundreds of thousands products.

Django queryset with prefetch or not?

I have such models
class Category(models.Model):
name = models.CharField()
…
class Product(models.Model):
category = models.ForeignKey(Category, verbose_name=‘cat’)
name = models.CharField()
price = models.IntegerField()
I need to create queryset which will select all products with price >= 100 grouped by categories. Afterwards I need to get count of products in each category.
I did
categories = Category.objects.filter(product__price__gte = 100)
This queryset gave me all categories which contain product with price >= 100. But how can I get count of products in it and products themseves? Maybe I need to use a prefetch_related, but I don't know how.
You can achieve this usingCount with conditional aggregation from django.
categories = Category.objects.filter(product__price__gte = 100).\
annotate(count_of_products=Count('products', filter=Q(product__price__gte = 100)))

How can I access a specific field of a queryset in Django?

I have these models in my Django app:
class Book(models.Model):
title = models.CharField(max_length=50, unique=True)
owner = models.CharField(max_length=30)
price = models.DecimalField(max_digits=5, decimal_places=2, null=True)
book_rating = models.ForeignKey('Rating', null=True)
RATE_CHOICES = zip(range(1,6), range(1,6))
class Rating(models.Model):
user = models.ForeignKey(User)
this_book = models.ForeignKey(Book)
rate = models.DecimalField(max_digits=2, decimal_places=1, choices=RATE_CHOICES)
comment = models.TextField(max_length=4000, null=True)
I am trying to access the Ratings of each instance of the Book model. Here is what I've tried so far in the shell:
from django.contrib.contenttypes.models import ContentType
>>> ctype = ContentType.objects.get_for_model(Rating)
>>> ctype
<ContentType: rating>
>>> book_titles = ctype.model_class().objects.filter(this_book__title='My Test Book')
>>> book_titles
<QuerySet [<Rating: My Test Book - parrot987 - 3.0>, <Rating: My Test Book - 123#gmail.com - 5.0>]>
How can I access the two rating values of each object (5.0 and 3.0) without all of this other data?
Can this be done in such a way that I am able to average the numbers and return the final value?
For 1. you can use (relevant documentation):
Rating.objects.filter(this_book__title='My Test Book').values('rate')
If you just want a flat list you can use values_list('rate', flat=True) instead of values('rate').
For 2 (relevant documentation):
from django.db.models import Avg
Rating.objects.filter(this_book__title='My Test Book').aggregate(Avg('rate'))
This will return a dictionary where the key is rate__avg and the value is the average of the ratings.
Please see the following for Many to One fields django - Get the set of objects from Many To One relationship
To access the rating, you can use a for loop and access the individual values e.g.
total = 0
for rating in book_titles.book_set.all()
total += rating.rate
Good luck!

Categories