reduce some duplicated query on rating

This commit is contained in:
mein Name 2025-03-08 12:28:26 -05:00 committed by Henri Dickson
parent 953791c84f
commit 70cf534d12
4 changed files with 246 additions and 13 deletions
catalog/common
journal

View file

@ -771,22 +771,22 @@ class Item(PolymorphicModel):
return not self.is_deleted and self.merged_to_item is None
@cached_property
def rating(self):
def rating_info(self):
from journal.models import Rating
return Rating.get_rating_for_item(self)
return Rating.get_info_for_item(self)
@property
def rating(self):
return self.rating_info.get("average")
@cached_property
def rating_count(self):
from journal.models import Rating
return Rating.get_rating_count_for_item(self)
return self.rating_info.get("count")
@cached_property
def rating_distribution(self):
from journal.models import Rating
return Rating.get_rating_distribution_for_item(self)
return self.rating_info.get("distribution")
@cached_property
def tags(self):

View file

@ -5,14 +5,14 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Avg, Count
from catalog.models import Item
from catalog.models import Item, Performance, TVShow
from takahe.utils import Takahe
from users.models import APIdentity
from .common import Content
MIN_RATING_COUNT = 5
RATING_INCLUDES_CHILD_ITEMS = ["tvshow", "performance"]
RATING_INCLUDES_CHILD_ITEMS = [TVShow, Performance]
class Rating(Content):
@ -73,10 +73,41 @@ class Rating(Content):
p.link_post_id(post.id)
return p
@classmethod
def get_info_for_item(cls, item: Item) -> dict:
stat = Rating.objects.filter(grade__isnull=False)
if item.__class__ in RATING_INCLUDES_CHILD_ITEMS:
stat = stat.filter(item_id__in=item.child_item_ids + [item.pk])
else:
stat = stat.filter(item=item)
stat = stat.values("grade").annotate(count=Count("grade"))
grades = [0] * 11
votes = 0
total = 0
for s in stat:
if s["grade"] and s["grade"] > 0 and s["grade"] < 11:
grades[s["grade"]] = s["count"]
total += s["count"] * s["grade"]
votes += s["count"]
if votes < MIN_RATING_COUNT:
return {"average": None, "count": votes, "distribution": [0] * 5}
else:
return {
"average": round(total / votes, 1),
"count": votes,
"distribution": [
100 * (grades[1] + grades[2]) // votes,
100 * (grades[3] + grades[4]) // votes,
100 * (grades[5] + grades[6]) // votes,
100 * (grades[7] + grades[8]) // votes,
100 * (grades[9] + grades[10]) // votes,
],
}
@staticmethod
def get_rating_for_item(item: Item) -> float | None:
stat = Rating.objects.filter(grade__isnull=False)
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
if item.__class__ in RATING_INCLUDES_CHILD_ITEMS:
stat = stat.filter(item_id__in=item.child_item_ids + [item.pk])
else:
stat = stat.filter(item=item)
@ -86,7 +117,7 @@ class Rating(Content):
@staticmethod
def get_rating_count_for_item(item: Item) -> int:
stat = Rating.objects.filter(grade__isnull=False)
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
if item.__class__ in RATING_INCLUDES_CHILD_ITEMS:
stat = stat.filter(item_id__in=item.child_item_ids + [item.pk])
else:
stat = stat.filter(item=item)
@ -96,7 +127,7 @@ class Rating(Content):
@staticmethod
def get_rating_distribution_for_item(item: Item):
stat = Rating.objects.filter(grade__isnull=False)
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
if item.__class__ in RATING_INCLUDES_CHILD_ITEMS:
stat = stat.filter(item_id__in=item.child_item_ids + [item.pk])
else:
stat = stat.filter(item=item)

View file

@ -1,4 +1,5 @@
from .csv import *
from .ndjson import *
from .piece import *
from .rating import *
from .search import *

201
journal/tests/rating.py Normal file
View file

@ -0,0 +1,201 @@
from django.test import TestCase
from catalog.common.models import Item
from catalog.models import Edition, IdType, Movie, TVEpisode, TVSeason, TVShow
from journal.models.rating import Rating
from users.models import User
class RatingTest(TestCase):
databases = "__all__"
def setUp(self):
# Create 10 users
self.users = []
for i in range(10):
user = User.register(email=f"user{i}@example.com", username=f"user{i}")
self.users.append(user)
# Create a book
self.book = Edition.objects.create(
localized_title=[{"lang": "en", "text": "Test Book"}],
primary_lookup_id_type=IdType.ISBN,
primary_lookup_id_value="9780553283686",
author=["Test Author"],
)
# Create a movie
self.movie = Movie.objects.create(
localized_title=[{"lang": "en", "text": "Test Movie"}],
primary_lookup_id_type=IdType.IMDB,
primary_lookup_id_value="tt1234567",
director=["Test Director"],
year=2020,
)
# Create a TV show with a season and episode
self.tvshow = TVShow.objects.create(
localized_title=[{"lang": "en", "text": "Test Show"}],
primary_lookup_id_type=IdType.IMDB,
primary_lookup_id_value="tt9876543",
)
self.tvseason = TVSeason.objects.create(
localized_title=[{"lang": "en", "text": "Season 1"}],
show=self.tvshow,
season_number=1,
)
self.tvepisode = TVEpisode.objects.create(
localized_title=[{"lang": "en", "text": "Episode 1"}],
season=self.tvseason,
episode_number=1,
)
def test_rating_basic(self):
"""Test basic rating functionality for a single item."""
# Add ratings for the book from all users
ratings = [7, 8, 9, 10, 8, 7, 6, 9, 10, 8]
for i, user in enumerate(self.users):
Rating.update_item_rating(
self.book, user.identity, ratings[i], visibility=1
)
# Get rating info for the book
rating_info = Rating.get_info_for_item(self.book)
# Check rating count
self.assertEqual(rating_info["count"], 10)
# Check average rating - should be 8.2
expected_avg = sum(ratings) / len(ratings)
self.assertEqual(rating_info["average"], round(expected_avg, 1))
# Check distribution
# [1-2, 3-4, 5-6, 7-8, 9-10] buckets represented as percentages
expected_distribution = [0, 0, 10, 50, 40] # Based on our ratings
self.assertEqual(rating_info["distribution"], expected_distribution)
# Test individual user rating
user_rating = Rating.get_item_rating(self.book, self.users[0].identity)
self.assertEqual(user_rating, 7)
book = Item.objects.get(pk=self.book.pk)
self.assertEqual(book.rating, round(expected_avg, 1))
self.assertEqual(book.rating_count, 10)
self.assertEqual(book.rating_distribution, expected_distribution)
def test_rating_multiple_items(self):
"""Test ratings across multiple items."""
# Rate the movie with varying scores
movie_ratings = [3, 4, 5, 6, 7, 8, 9, 10, 2, 1]
for i, user in enumerate(self.users):
Rating.update_item_rating(
self.movie, user.identity, movie_ratings[i], visibility=1
)
# Rate the TV show
tvshow_ratings = [10, 9, 8, 9, 10, 9, 8, 10, 9, 8]
for i, user in enumerate(self.users):
Rating.update_item_rating(
self.tvshow, user.identity, tvshow_ratings[i], visibility=1
)
# Get rating info for both items
movie_info = Rating.get_info_for_item(self.movie)
tvshow_info = Rating.get_info_for_item(self.tvshow)
# Check counts
self.assertEqual(movie_info["count"], 10)
self.assertEqual(tvshow_info["count"], 10)
# Check averages
expected_movie_avg = sum(movie_ratings) / len(movie_ratings)
expected_tvshow_avg = sum(tvshow_ratings) / len(tvshow_ratings)
self.assertEqual(movie_info["average"], round(expected_movie_avg, 1))
self.assertEqual(tvshow_info["average"], round(expected_tvshow_avg, 1))
# Check distribution for movie
# [1-2, 3-4, 5-6, 7-8, 9-10] buckets
expected_movie_distribution = [
20,
20,
20,
20,
20,
] # Evenly distributed across buckets
self.assertEqual(movie_info["distribution"], expected_movie_distribution)
# Check distribution for TV show
# [1-2, 3-4, 5-6, 7-8, 9-10] buckets
expected_tvshow_distribution = [0, 0, 0, 30, 70] # High ratings only
self.assertEqual(tvshow_info["distribution"], expected_tvshow_distribution)
def test_rating_update_and_delete(self):
"""Test updating and deleting ratings."""
# Add initial ratings
for user in self.users[:5]:
Rating.update_item_rating(self.tvepisode, user.identity, 8, visibility=1)
# Check initial count
self.assertEqual(Rating.get_rating_count_for_item(self.tvepisode), 5)
# Update a rating
Rating.update_item_rating(
self.tvepisode, self.users[0].identity, 10, visibility=1
)
# Check that rating was updated
updated_rating = Rating.get_item_rating(self.tvepisode, self.users[0].identity)
self.assertEqual(updated_rating, 10)
# Delete a rating by setting it to None
Rating.update_item_rating(
self.tvepisode, self.users[1].identity, None, visibility=1
)
# Check that rating count decreased
self.assertEqual(Rating.get_rating_count_for_item(self.tvepisode), 4)
# Check that the rating was deleted
deleted_rating = Rating.get_item_rating(self.tvepisode, self.users[1].identity)
self.assertIsNone(deleted_rating)
def test_rating_minimum_count(self):
"""Test the minimum rating count threshold."""
# Add only 4 ratings to the book (below MIN_RATING_COUNT of 5)
for user in self.users[:4]:
Rating.update_item_rating(self.book, user.identity, 10, visibility=1)
# Check that get_rating_for_item returns None due to insufficient ratings
rating = Rating.get_rating_for_item(self.book)
self.assertIsNone(rating)
# Add one more rating to reach the threshold
Rating.update_item_rating(self.book, self.users[4].identity, 10, visibility=1)
# Now we should get a valid rating
rating = Rating.get_rating_for_item(self.book)
self.assertEqual(rating, 10.0)
def test_tvshow_rating_includes_children(self):
"""Test that TV show ratings include ratings from child items."""
# Rate the TV show directly
Rating.update_item_rating(self.tvshow, self.users[0].identity, 6, visibility=1)
# Rate the episode (which is a child of the TV show)
for i in range(1, 6): # Users 1-5
Rating.update_item_rating(
self.tvseason, self.users[i].identity, 10, visibility=1
)
# Get info for TV show - should include ratings from episode
tvshow_info = Rating.get_info_for_item(self.tvshow)
# Check count (1 for show + 5 for episode = 6)
self.assertEqual(tvshow_info["count"], 6)
# The average should consider all ratings (6 + 5*10 = 56, divided by 6 = 9.3)
self.assertEqual(tvshow_info["average"], 9.3)