reduce n+1 queries on user profile page

This commit is contained in:
mein Name 2025-03-13 12:43:24 -04:00 committed by Henri Dickson
parent 5667579645
commit cbf6314a62
6 changed files with 216 additions and 10 deletions

View file

@ -62,10 +62,7 @@ def list_marks_on_user_shelf(
target = APIdentity.get_by_handle(handle)
except APIdentity.DoesNotExist:
return ShelfMember.objects.none()
viewer = request.user.identity
if target.is_blocking(viewer) or target.is_blocked_by(viewer):
return ShelfMember.objects.none()
qv = q_owned_piece_visible_to_user(request.user, target)
qv = q_owned_piece_visible_to_user(request.user, target, True)
queryset = (
target.shelf_manager.get_latest_members(
type, ItemCategory(category) if category else None

View file

@ -41,7 +41,12 @@ class VisibilityType(models.IntegerChoices):
Private = 2, _("Mentioned Only") # type:ignore[reportCallIssue]
def q_owned_piece_visible_to_user(viewing_user: User, owner: APIdentity):
def q_owned_piece_visible_to_user(
viewing_user: User | None, owner: APIdentity, check_blocking: bool = False
) -> Q:
"""return a Q object to filter pieces that are visible to the viewing user"""
if check_blocking and owner.restricted:
return Q(pk__in=[])
if not viewing_user or not viewing_user.is_authenticated:
if owner.anonymous_viewable:
return Q(owner=owner, visibility=0)
@ -50,8 +55,8 @@ def q_owned_piece_visible_to_user(viewing_user: User, owner: APIdentity):
viewer = viewing_user.identity
if viewer == owner:
return Q(owner=owner)
# elif viewer.is_blocked_by(owner):
# return Q(pk__in=[])
elif check_blocking and viewer.is_blocked_by(owner):
return Q(pk__in=[])
elif viewer.is_following(owner):
return Q(owner=owner, visibility__in=[0, 1])
else:

View file

@ -7,8 +7,9 @@ from django.db import connection, models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from loguru import logger
from polymorphic.models import PolymorphicManager
from polymorphic.models import ContentType, PolymorphicManager
from catalog.common.models import item_categories
from catalog.models import Item, ItemCategory
from takahe.utils import Takahe
from users.models import APIdentity
@ -743,6 +744,45 @@ class ShelfManager:
def get_manager_for_user(owner: APIdentity):
return ShelfManager(owner)
def get_stats(self, q: models.Q | None = None) -> dict:
from .review import Review
if not q:
q = models.Q(owner=self.owner)
qs = (
ShelfMember.objects.filter(q)
.values("parent__shelf_type", "item__polymorphic_ctype_id")
.annotate(num=models.Count("item"))
)
qs2 = (
Review.objects.filter(q)
.values("item__polymorphic_ctype_id")
.annotate(num=models.Count("item"))
)
stats = {
cat: {t: 0 for t in (ShelfType.values + ["reviewed"])}
for cat in ItemCategory.values
}
for cat, item_classes in item_categories().items():
ct_ids = [
ContentType.objects.get_for_model(item_cls).pk
for item_cls in item_classes
]
for typ in ShelfType.values:
stats[cat][typ] = sum(
[
s["num"]
for s in qs
if s["item__polymorphic_ctype_id"] in ct_ids
and s["parent__shelf_type"] == typ
],
0,
)
stats[cat]["reviewed"] = sum(
[s["num"] for s in qs2 if s["item__polymorphic_ctype_id"] in ct_ids], 0
)
return stats
def get_calendar_data(self, max_visiblity: int):
shelf_id = self.get_shelf(ShelfType.COMPLETE).pk
timezone_offset = timezone.localtime(timezone.now()).strftime("%z")

View file

@ -3,3 +3,4 @@ from .ndjson import *
from .piece import *
from .rating import *
from .search import *
from .shelf import *

162
journal/tests/shelf.py Normal file
View file

@ -0,0 +1,162 @@
from django.test import TestCase
from catalog.common.models import ItemCategory
from catalog.models import Edition, Game, IdType, Movie, TVShow
from journal.models.common import q_owned_piece_visible_to_user
from journal.models.review import Review
from journal.models.shelf import ShelfManager, ShelfMember, ShelfType
from users.models import User
class ShelfManagerTest(TestCase):
databases = "__all__"
def setUp(self):
# Create a user
self.user = User.register(email="user@example.com", username="testuser")
self.identity = self.user.identity
# Create items of different categories
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"],
)
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,
)
self.tvshow = TVShow.objects.create(
localized_title=[{"lang": "en", "text": "Test Show"}],
primary_lookup_id_type=IdType.IMDB,
primary_lookup_id_value="tt9876543",
)
self.game = Game.objects.create(
localized_title=[{"lang": "en", "text": "Test Game"}],
primary_lookup_id_type=IdType.IGDB,
primary_lookup_id_value="12345",
developer=["Test Developer"],
)
# Initialize shelf manager for user
self.shelf_manager = ShelfManager(self.identity)
self._add_items_to_shelves()
def _add_items_to_shelves(self):
"""Helper to add items to different shelves"""
# Add books to shelves
shelves = {
ShelfType.WISHLIST: [self.book],
ShelfType.PROGRESS: [
self.movie,
self.tvshow,
], # Add same book twice to test count
ShelfType.COMPLETE: [self.game],
ShelfType.DROPPED: [],
}
# Create shelf members
for shelf_type, items in shelves.items():
shelf = self.shelf_manager.get_shelf(shelf_type)
for item in items:
ShelfMember.objects.update_or_create(
owner=self.identity,
item=item,
defaults={"visibility": 1, "position": 0, "parent": shelf},
)
# Add reviews for some items
Review.objects.create(
owner=self.identity,
item=self.book,
body="Book review",
title="Book Review",
visibility=0,
)
Review.objects.create(
owner=self.identity,
item=self.movie,
body="Movie review",
title="Movie Review",
visibility=1,
)
# Add two reviews for the game to test counts
Review.objects.create(
owner=self.identity,
item=self.game,
body="Game review 1",
title="Game Review 1",
visibility=2,
)
def test_get_stats(self):
"""Test that ShelfManager.get_stats() returns correct statistics"""
# Get stats
stats = self.shelf_manager.get_stats()
# Verify structure: stats should have keys for each ItemCategory
for category in ItemCategory.values:
self.assertIn(category, stats)
# Verify each category has counts for each shelf type
for category in ItemCategory.values:
for shelf_type in ShelfType.values:
self.assertIn(shelf_type, stats[category])
self.assertIn("reviewed", stats[category])
# Verify expected counts for book category
self.assertEqual(stats[ItemCategory.Book][ShelfType.WISHLIST], 1)
self.assertEqual(stats[ItemCategory.Book][ShelfType.PROGRESS], 0)
self.assertEqual(stats[ItemCategory.Book][ShelfType.COMPLETE], 0)
self.assertEqual(stats[ItemCategory.Book][ShelfType.DROPPED], 0)
self.assertEqual(stats[ItemCategory.Book]["reviewed"], 1)
# Verify expected counts for movie category
self.assertEqual(stats[ItemCategory.Movie][ShelfType.WISHLIST], 0)
self.assertEqual(stats[ItemCategory.Movie][ShelfType.PROGRESS], 1)
self.assertEqual(stats[ItemCategory.Movie][ShelfType.COMPLETE], 0)
self.assertEqual(stats[ItemCategory.Movie][ShelfType.DROPPED], 0)
self.assertEqual(stats[ItemCategory.Movie]["reviewed"], 1)
# Verify expected counts for TV category
self.assertEqual(stats[ItemCategory.TV][ShelfType.WISHLIST], 0)
self.assertEqual(stats[ItemCategory.TV][ShelfType.PROGRESS], 1)
self.assertEqual(stats[ItemCategory.TV][ShelfType.COMPLETE], 0)
self.assertEqual(stats[ItemCategory.TV][ShelfType.DROPPED], 0)
self.assertEqual(stats[ItemCategory.TV]["reviewed"], 0)
# Verify expected counts for game category
self.assertEqual(stats[ItemCategory.Game][ShelfType.WISHLIST], 0)
self.assertEqual(stats[ItemCategory.Game][ShelfType.PROGRESS], 0)
self.assertEqual(stats[ItemCategory.Game][ShelfType.COMPLETE], 1)
self.assertEqual(stats[ItemCategory.Game][ShelfType.DROPPED], 0)
self.assertEqual(stats[ItemCategory.Game]["reviewed"], 1)
def test_get_stats_with_filter(self):
"""Test ShelfManager.get_stats() with a filter"""
q1 = q_owned_piece_visible_to_user(None, self.identity)
stats1 = self.shelf_manager.get_stats(q=q1)
self.assertEqual(stats1[ItemCategory.Book][ShelfType.WISHLIST], 0)
self.assertEqual(stats1[ItemCategory.Book]["reviewed"], 1)
self.assertEqual(stats1[ItemCategory.Movie]["reviewed"], 0)
self.assertEqual(stats1[ItemCategory.Game]["reviewed"], 0)
# Create a second user to make sure filtering works
user2 = User.register(email="user2@example.com", username="testuser2")
user2.identity.follow(self.user.identity, True)
q2 = q_owned_piece_visible_to_user(user2, self.identity)
stats2 = self.shelf_manager.get_stats(q=q2)
self.assertEqual(stats2[ItemCategory.Book][ShelfType.WISHLIST], 1)
self.assertEqual(stats2[ItemCategory.Book]["reviewed"], 1)
self.assertEqual(stats2[ItemCategory.Movie]["reviewed"], 1)
self.assertEqual(stats2[ItemCategory.Game]["reviewed"], 0)

View file

@ -57,6 +57,7 @@ def profile(request: AuthedHttpRequest, user_name):
ItemCategory.Game,
ItemCategory.Performance,
]
stats = target.shelf_manager.get_stats()
for category in visbile_categories:
shelf_list[category] = {}
for shelf_type in ShelfType:
@ -69,7 +70,7 @@ def profile(request: AuthedHttpRequest, user_name):
).filter(qv)
shelf_list[category][shelf_type] = {
"title": label,
"count": members.count(),
"count": stats[category][shelf_type],
"members": members[:10].prefetch_related("item"),
}
reviews = (
@ -79,7 +80,7 @@ def profile(request: AuthedHttpRequest, user_name):
)
shelf_list[category]["reviewed"] = {
"title": target.shelf_manager.get_label("reviewed", category),
"count": reviews.count(),
"count": stats[category].get("reviewed", 0),
"members": reviews[:10].prefetch_related("item"),
}
collections = Collection.objects.filter(qv).order_by("-created_time")