Django models - Able to dynamically calculate the value of a field - python

Say for example I have a rating which is attached to a product model like so..
class Product(models.Model):
...
rating = models.IntegerField(...)
I want the product rating to change as new Reviews (that include a star rating of the product) change or updated/deleted.
class Review(models.Model):
...
related_product = models.ForeignKey(Product, on_delete=models.CASCADE, ...)
rating = models.IntegerField(...)
Initially, I used a method on the product class to calculate the value of rating by counting the rating value from each review and then dividing by the number of reviews to get an average.
class Product(models.Model):
...
def rating(self):
total_rating = ...
reviews_count = ...
average = total_rating / reviews_count
However, this doesn't allow me to use order_by('rating') when querying the objects and sending back the objects by order of their rating since 'rating' has to be defined in the database (i.e. as a field instead of a method).
Is there any way that I can calculate the value for rating which is then stored in the database?

You can annotate your queryset to calculate the average using your database
from django.db.models import Avg
Product.objects.all().annotate(
rating=Avg('review_set__rating')
).order_by('rating')
Now each Product object in the queryset will have an additional attribute rating
If you must store this rating because it becomes expensive to calculate then #niklas' comment is the correct solution

Related

Django: Get average rating per user

Given the following, how can I make a query which returns a list of the average rating per user in friendlist?
models.py
class Beer(models.Model):
id = models.BigIntegerField(primary_key=True)
name = models.CharField(max_length=150)
...
class Checkin(models.Model):
id = models.IntegerField(primary_key=True)
rating = models.FloatField(blank=True, null=True)
user = models.ForeignKey(User, on_delete=CASCADE)
...
class FriendList(models.Model):
user = models.OneToOneField(User, on_delete=CASCADE, primary_key=True)
friend = models.ManyToManyField(User, related_name="friends")
database (postgresql)
user
beer
rating
1
1
4.2
1
1
3.5
1
1
4.0
2
1
4.1
2
1
3.7
My current query to get all friends checkins:
Checkin.objects.filter(beer=1, user__in=friends.friend.all())
Which gives me something like:
[{user: 1, rating: 4.2}, {user: 1, rating: 3.5},...,{user: 2, rating: 4.1}...]
What I want is:
[{user: 1, avg_rating: 4.1}, {user: 2, avg_rating: 3.8}]
It makes more sense to .annotate(…) [Django-doc] the User objects, so:
from django.db.models import Avg
friends.friend.filter(
checkin__beer_id=1
).annotate(
rating=Avg('checkin__rating')
)
Where checkin__ is the related_query_name=… [Django-doc] for the user from Checkin to the User model. If you did not specify a related_query_name=…, then it will use the value for the related_name=… [Django-doc], and if that one is not specified either, it will use the name of the source model in lowercase, so checkin.
The User objects that arise from this queryset will have an extra attribute .rating that contains the average rating over the Checkins for that beer_id.
You can determine the average of these averages with an .aggregate(…) call [Django-doc]:
from django.db.models import Avg
friends.friend.filter(
checkin__beer_id=1
).annotate(
rating=Avg('checkin__rating')
).aggregate(
all_users_avg_rating=Avg('rating'),
number_of_users=Count('pk')
)
This will return a dictionary with two elements: all_users_avg_rating will map to the average of these averages, and number_of_users will return the number of distinct users.
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

Query to fetch highest rated movie with mimimum 5 people rated

I want to fetch name of movie with maximum rated movie with minimum 5 people rated in django.
My code :
model.py
class Movie(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
vote_count = models.IntegerField()
class Watchlist(models.Model):
userid = models.IntegerField()
movie_id = models.ForeignKey(Movie, on_delete=models.CASCADE)
rating = models.IntegerField()
what will be query to get movie with highest rating with minimum 5 people ?
I propose that you make some changes to your model. Normally ForeignKeys do not end with an id suffix, since Django will add a "twin field" with an _id suffix that stores the value of the target field. Furthermore you probably better make a ForeignKey to the user model. If you do not specify a primary key yourself, Django will automatically add an field named id that is an AutoField, hendce there is no need to add that manually. Finally you do not need to store the vote_count in a field of the Movie, you can retrieve that by counting the number of related Rating objects:
from django.conf import settings
class Movie(models.Model):
title = models.CharField(max_length=100)
class Rating(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete.models.CASCADE)
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
rating = models.IntegerField()
Then we can retrieve the highest rated movie with:
from django.db.models import Avg, Count
higest_rated = Movie.objects.annotate(
rating=Avg('rating__rating'),
votes=Count('rating')
).filter(votes__gte=5).order_by('-rating').first()
Here the votes__gte=5 will filter such that it will only obtain Movies with five or more votes, and we order by rating in descending order.
I'd modify the model, moving out Rating entity related fields from Watchlist and Movie.
Add the "Rate" class, and then filter by two conditions:
Count(Rate for the exact Movie) > minimum threshold(e.g. 5)
AVG(rating score for the exact Movie) > minimum threshold(e.g. 5)
or, if you need top-rated movies, use Order by as it described in that answer
In your case, you could use Count and Average with Watchlist.Rating field

how to remain the same data after changed or deleted the primary key (an instance)from model

i want to remain the same data as i saved before , after changing , or deleted the model which provide the foreign key ,
i tried to
on_delete=models.SET_NULL
but still removes the instance which i've saved before
class Product(models.Model):
product_name = models.CharField(unique=True, max_length=50 ,
blank=False,null=False)
price = models.PositiveIntegerField()
active = models.BooleanField(default=True)
def __str__(self):
return self.product_name
class Order(models.Model):
id = models.AutoField(primary_key = True)
products = models.ManyToManyField(Product ,through='ProductOrder')
date_time = models.DateTimeField(default=datetime.now())
#property
def total(self):
return self.productorder_set.aggregate(
price_sum=Sum(F('quantity') * F('product__price'),
output_field=IntegerField()) )['price_sum']
class ProductOrder(models.Model):
product = models.ForeignKey(Product, on_delete=models.SET_NULL , null=True)
ordering = models.ForeignKey(Order, on_delete=models.SET_NULL,blank=True,null=True ,default='')
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
return f"{self.product} : {self.quantity}"
and how save the total price in variable , if the the price in Product updated , it remains as the first time ?
and how to prevent losing data from Order model after removing a product from Product model thanks for advice
There are multiple ways to solve this, but it generally has little to do with Django itself, because these are business decisions:
Don't link to the Product in the Order (and ProductOrder) models, but save the product information directly in the ProductOrder model. You could for example use a JSONField (if you're on PostgreSQL) and keep all the product details there, including the price.
Don't change a Product once it's been ordered once. i.e. you can make it impossible to change/delete a Product (on_delete=models.PROTECT). Give a Product a unique SKU and version number, create new versions of your Product when the price changes. That way all orders will use a reference to the Product that was actually ordered. When fetching Products that can be purchased, always fetch the latest version of the Product. You could create a new ProductVersion table for this and link to that instead of the Product.
Don't allow deleting a Product (on_delete=PROTECT), but allow changing the price. Note that if other characteristics of the Product change (such as color), you should always create a new Product. Keep a copy of the paid price in the ProductOrder itself (the line item of the order). This is the most common approach. You just copy the price in the line items, so that you always have actual price paid by the customer.
The solution you choose depends a bit on the business requirements of your company, i.e. how products are managed, what needs to be kept, etc... It also depends on what you want your customers to be able to see when looking up their historical orders.

Django - How to update a field for a model on another model's creation?

I have two models, a Product model and a Rating model. What I want to accomplish is, every time a "Rating" is created, via an API POST to an endpoint created using DRF, I want to compute and update the average_rating field in the associated Product.
class Product(models.Model):
name = models.CharField(max_length=100)
...
average_rating = models.DecimalField(max_digits=3, decimal_places=2)
def __str__(self):
return self.name
class Rating(models.Model):
rating = models.IntegerField()
product = models.ForeignKey('Product', related_name='ratings')
def __str__(self):
return "{}".format(self.rating)
What is the best way to do this? Do I use a post_save (Post create?) signal?
What is the best way to do this? Do I use a post_save (Post create?) signal?
The problem is not that much here how to do this technically I think, but more how you make this robust. After all it is not only creating new ratings that is important: if people change their rating, or remove a rating, then the average rating needs to be updated as well. It is even possible that if you define a ForeignKey with a cascade, then deleting something related to a Rating can result in removing several ratings, and thus updating several Products. So getting the average in sync can become quite hard. Especially if you would allow other programs to manipulate the database.
It might therefore be better to calculate the average rating. For example with an aggregate:
from django.db.models import Avg
class Product(models.Model):
name = models.CharField(max_length=100)
#property
def average_rating(self):
return self.ratings.aggregate(average_rating=Avg('rating'))['average_rating']
def __str__(self):
return self.name
Or if you want to load multiple Products in a QuerySet, you can do an .annotate(..) to calculate the average rating in bulk:
Product.objects.annotate(
average_rating=Avg('rating__rating')
)
Here the Products will have an attribute average_rating that is the average rating of the related ratings.
In case the number of ratings can be huge, it can take considerable time to calculate the average. In that case I propose to add a field, and use a periodic task to update the rating. For example:
from django.db.models import Avg, OuterRef, Subquery
class Product(models.Model):
name = models.CharField(max_length=100)
avg_rating=models.DecimalField(
max_digits=3,
decimal_places=2,
null=True,
default=None
)
#property
def average_rating(self):
return self.avg_rating or self.ratings.aggregate(average_rating=Avg('rating'))['average_rating']
#classmethod
def update_averages(cls):
subq = cls.objects.filter(
id=OuterRef('id')
).annotate(
avg=Avg('rating__rating')
).values('avg')[:1]
cls.objects.update(
avg_rating=Subquery(subq)
)
def __str__(self):
return self.name
You can then periodically call Product.update_averages() to update the average ratings of all products. In case you create, update, or remove a rating, then you can aim to set the avg_rating field of the related product(s) to None to force recalculation, for example with a post_save, etc. But note that signals can be circumveted (for example with the .update(..) of a queryset, or by bulk_create(..)), and thus that it is still a good idea to periodically synchronize the average ratings.

SQLite Django Model for Inventory of Seeds

I'm trying to build an Inventory Model for a Django App that handles the sale of seeds. Seeds are stored and sold in packs of 3, 5, or 10 seeds of a single variety (for example: 3 pack of mustard seeds).
I want to add x amount of products to inventory with a price for each entry, and sell that product at that price for as long as that entry has items left(quantity field > 0) even if later entries have been made for the same product and presentation but at a different price, so i have the following model:
class Product(models.Model):
name = models.CharField(max_length=100)
class Presentation(models.Model):
seed_qty = models.IntegerField()
class Stock(models.Model):
date = models.DateField(auto_now=True)
quantity = models.IntegerField()
product = models.ForeignKey(Product, on_delete=models.CASCADE)
presentation = models.ForeignKey(Presentation, on_delete=models.CASCADE)
cost = models.FloatField(null=True, blank=True)
sell_price = models.FloatField(null=True, blank=True)
I'm wondering if I should actually relate Product and Stock with a ManyToMany field through a GeneralEntry intermediate model in which I'd store date_added, presentation and cost/price.
My issue is that when I add multiple Stock entries for the same product and presentation, I can't seem to query the earliest prices for each available (quantity>0) stock entry for each product.
What I've tried so far has been:
stock = Stock.objects.filter(quantity__gt=0).order_by('-date')
stock = stock.annotate(min_date=Min('date')).filter(date=min_date)
But that returns that max_date isn't defined.
Any ideas on how to query or rearrange this model ?
Thanks!
*** UPDATE : I wasn't using F() function from django.db.models.
Doing it like this works:
stock = Stock.objects.filter(quantity__gt=0).order_by('-date')
stock = stock.annotate(min_date=Min('date')).filter(date=F('min_date'))
Turns out I wasn't using F() function from django.db.models.
Doing it like this works:
stock = Stock.objects.filter(quantity__gt=0).order_by('-date')
stock = stock.annotate(min_date=Min('date')).filter(date=F('min_date'))

Categories