From 9ce5d525b35cbdd9765c533f100da4b6d2521d11 Mon Sep 17 00:00:00 2001 From: badbl0cks <4161747+badbl0cks@users.noreply.github.com> Date: Sat, 15 Mar 2025 15:23:00 -0700 Subject: [PATCH] Some optimizations to trade_offers to reduce loading times --- cards/migrations/0001_initial.py | 3 +- cards/models.py | 35 ++++-- home/views.py | 102 ++++------------ seed/0001_Rarity.json | 10 ++ theme/templates/base.html | 3 +- .../templates/trades/_friend_code_select.html | 2 +- .../templates/trades/trade_offer_detail.html | 53 ++++++-- .../templates/trades/trade_offer_my_list.html | 3 +- trades/forms.py | 19 ++- trades/migrations/0001_initial.py | 2 +- trades/models.py | 30 ++++- trades/signals.py | 100 ++++++++++----- trades/views.py | 115 ++++++------------ 13 files changed, 255 insertions(+), 222 deletions(-) diff --git a/cards/migrations/0001_initial.py b/cards/migrations/0001_initial.py index 35fdeaf..76f7364 100644 --- a/cards/migrations/0001_initial.py +++ b/cards/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.2 on 2025-03-14 05:35 +# Generated by Django 5.1.2 on 2025-03-15 22:05 import django.db.models.deletion from django.db import migrations, models @@ -35,6 +35,7 @@ class Migration(migrations.Migration): name='Rarity', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), + ('normalized_id', models.IntegerField()), ('name', models.CharField(max_length=64)), ('icons', models.CharField(max_length=64)), ('created_at', models.DateTimeField(auto_now_add=True)), diff --git a/cards/models.py b/cards/models.py index 363fb59..157f9f2 100644 --- a/cards/models.py +++ b/cards/models.py @@ -1,4 +1,6 @@ from django.db import models +from django.db.models import Prefetch +from django.apps import apps class DeckNameTranslation(models.Model): id = models.AutoField(primary_key=True) @@ -55,6 +57,7 @@ class Deck(models.Model): class Rarity(models.Model): id = models.AutoField(primary_key=True) + normalized_id = models.IntegerField(null=False) name = models.CharField(max_length=64) icons = models.CharField(max_length=64) created_at = models.DateTimeField(auto_now_add=True) @@ -63,14 +66,26 @@ class Rarity(models.Model): def __str__(self): return self.name - @property - def normalized_id(self): - """ - For trading equivalence: treat Special Art Rare (pk 7) and Super Rare (pk 6) as the same. - """ - if self.pk in (6, 7): - return 6 - return self.pk +# Custom Manager for Card model +class CardPrefetchManager(models.Manager): + def get_queryset(self): + return ( + super() + .get_queryset() + .select_related("cardset", "rarity") + .prefetch_related( + "decks", + "decks__cardset", + ) + ) + +class CardManager(models.Manager): + def get_queryset(self): + return ( + super() + .get_queryset() + .select_related("cardset", "rarity") + ) class Card(models.Model): id = models.AutoField(primary_key=True) @@ -82,6 +97,10 @@ class Card(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + # Use the custom manager to ensure optimized querysets everywhere. + objects = CardPrefetchManager() + objects_no_prefetch = CardManager() + def __str__(self): # For display, we show the original rarity icons. return f"{self.name} {self.rarity.icons} {self.cardset.name}" diff --git a/home/views.py b/home/views.py index 5b9b0a7..7c47e08 100644 --- a/home/views.py +++ b/home/views.py @@ -14,106 +14,58 @@ from django.http import HttpResponseRedirect class HomePageView(TemplateView): template_name = "home/home.html" - def get_base_trade_offer_queryset(self): - """ - Returns a queryset for TradeOffer that includes prefetches and denormalized aggregates. - """ - active_states = [ - TradeAcceptance.AcceptanceState.ACCEPTED, - TradeAcceptance.AcceptanceState.SENT, - TradeAcceptance.AcceptanceState.RECEIVED, - TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR, - TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR, - TradeAcceptance.AcceptanceState.THANKED_BY_BOTH, - ] - - have_cards_prefetch = Prefetch( - 'have_cards', - queryset=Card.objects.annotate( - trade_offer_count=Count("trade_offers_have") - ).order_by("trade_offer_count", "id") - ) - want_cards_prefetch = Prefetch( - 'want_cards', - queryset=Card.objects.annotate( - trade_offer_count=Count("trade_offers_want") - ).order_by("trade_offer_count", "id") - ) - - qs = ( - TradeOffer.objects.all() - .prefetch_related( - have_cards_prefetch, - "have_cards__decks", - "have_cards__rarity", - "have_cards__cardset", - want_cards_prefetch, - "want_cards__decks", - "want_cards__rarity", - "want_cards__cardset", - "acceptances", - - ) - .select_related("initiated_by__user") - ) - return qs - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["cards"] = Card.objects.all() \ - .order_by("name", "rarity__pk") \ - .select_related("rarity", "cardset") \ - .prefetch_related("decks") + context["cards"] = Card.objects.all().order_by("name", "rarity__pk") # Reuse base trade offer queryset for market stats - base_offer_qs = self.get_base_trade_offer_queryset().filter(is_closed=False) + base_offer_qs = TradeOffer.objects.filter(is_closed=False) # Recent Offers - recent_offers_qs = base_offer_qs.order_by("-created_at")[:10] - context["recent_offers"] = list(recent_offers_qs)[:5] + recent_offers_qs = base_offer_qs.order_by("-created_at") + context["recent_offers"] = recent_offers_qs[:6] # Most Offered Cards context["most_offered_cards"] = ( - Card.objects.filter(tradeofferhavecard__isnull=False) + Card.objects_no_prefetch.filter(tradeofferhavecard__isnull=False) .annotate(offer_count=Sum("tradeofferhavecard__quantity")) - .order_by("-offer_count") - .select_related("rarity", "cardset") - .prefetch_related("decks")[:5] + .order_by("-offer_count", "?")[:6] ) # Most Wanted Cards context["most_wanted_cards"] = ( - Card.objects.filter(tradeofferwantcard__isnull=False) + Card.objects_no_prefetch.filter(tradeofferwantcard__isnull=False) .annotate(offer_count=Sum("tradeofferwantcard__quantity")) - .order_by("-offer_count") - .select_related("rarity", "cardset") - .prefetch_related("decks")[:5] + .order_by("-offer_count", "?")[:6] ) # Least Offered Cards context["least_offered_cards"] = ( - Card.objects.annotate(offer_count=Sum("tradeofferhavecard__quantity")) - .order_by("offer_count", "?")[:5] + Card.objects_no_prefetch.annotate(offer_count=Sum("tradeofferhavecard__quantity")) + .order_by("offer_count", "?")[:6] ) - # Featured Offers grouped by rarity - all_offers = base_offer_qs.order_by("created_at") featured = {} - featured["All"] = all_offers[:5] - grouped = defaultdict(list) - for offer in all_offers: - normalized_ids = {card.rarity.normalized_id for card in offer.have_cards.all() if card.rarity} - for norm in normalized_ids: - grouped[norm].append(offer) - norm_ids_available = list(grouped.keys()) - rareness_qs = Rarity.objects.filter(pk__in=[6] + [nid for nid in norm_ids_available if nid != 6]) - rarity_map = {rarity.pk: rarity.icons for rarity in rareness_qs} - for norm in sorted(grouped.keys(), reverse=True): - offers = grouped[norm] + featured["All"] = base_offer_qs.order_by("created_at")[:6] + + normalized_ids = list( + Rarity.objects.filter(pk__lte=5).values_list("normalized_id", flat=True).distinct() + ) + + rarity_map = {rarity.normalized_id: rarity.icons for rarity in Rarity.objects.filter(pk__lte=5)} + + # For each normalized id (sorted descending), filter base offers that have a related card with that rarity. + for norm in sorted(normalized_ids, reverse=True): + offers_qs = base_offer_qs.filter( + have_cards__rarity__normalized_id=norm + # or want cards, but all offers share the same rarity so checking have_cards is enough + # TODO: attach rarity to offer so we don't need to do this + ).order_by("created_at").distinct()[:6] icon_label = rarity_map.get(norm) if icon_label: - featured[icon_label] = offers[:5] + featured[icon_label] = offers_qs + context["featured_offers"] = featured return context \ No newline at end of file diff --git a/seed/0001_Rarity.json b/seed/0001_Rarity.json index c4eb922..39c4cfe 100644 --- a/seed/0001_Rarity.json +++ b/seed/0001_Rarity.json @@ -4,6 +4,7 @@ "pk": 1, "fields": { "icons": "🔷", + "normalized_id": 1, "name": "Common", "created_at": "2025-02-16T06:54:40.993Z", "updated_at": "2025-02-16T06:54:40.993Z" @@ -14,6 +15,7 @@ "pk": 2, "fields": { "icons": "🔷🔷", + "normalized_id": 2, "name": "Uncommon", "created_at": "2025-02-16T06:54:44.213Z", "updated_at": "2025-02-16T06:54:44.213Z" @@ -24,6 +26,7 @@ "pk": 3, "fields": { "icons": "🔷🔷🔷", + "normalized_id": 3, "name": "Rare", "created_at": "2025-02-16T06:54:47.297Z", "updated_at": "2025-02-16T06:54:47.297Z" @@ -34,6 +37,7 @@ "pk": 4, "fields": { "icons": "🔷🔷🔷🔷", + "normalized_id": 4, "name": "Double Rare", "created_at": "2025-02-16T06:54:50.363Z", "updated_at": "2025-02-16T06:54:50.363Z" @@ -44,6 +48,7 @@ "pk": 5, "fields": { "icons": "⭐️", + "normalized_id": 5, "name": "Full Art Rare", "created_at": "2025-02-16T06:54:59.888Z", "updated_at": "2025-02-16T06:54:59.888Z" @@ -54,6 +59,7 @@ "pk": 6, "fields": { "icons": "⭐️⭐️", + "normalized_id": 6, "name": "Super Rare", "created_at": "2025-02-16T06:55:02.853Z", "updated_at": "2025-02-16T06:55:02.853Z" @@ -64,6 +70,7 @@ "pk": 7, "fields": { "icons": "🌟🌟", + "normalized_id": 6, "name": "Special Art Rare", "created_at": "2025-02-16T06:55:02.853Z", "updated_at": "2025-02-16T06:55:02.853Z" @@ -74,6 +81,7 @@ "pk": 8, "fields": { "icons": "⭐️⭐️⭐️", + "normalized_id": 7, "name": "Immersive Rare", "created_at": "2025-02-16T06:55:05.728Z", "updated_at": "2025-02-16T06:55:05.728Z" @@ -84,6 +92,7 @@ "pk": 9, "fields": { "icons": "👑", + "normalized_id": 8, "name": "Crown Rare", "created_at": "2025-02-16T06:55:13.907Z", "updated_at": "2025-02-16T06:55:13.907Z" @@ -94,6 +103,7 @@ "pk": 10, "fields": { "icons": "🅿️", + "normalized_id": 9, "name": "Promo", "created_at": "2025-02-16T06:55:13.907Z", "updated_at": "2025-02-16T06:55:13.907Z" diff --git a/theme/templates/base.html b/theme/templates/base.html index abbd77e..a2a5fc3 100644 --- a/theme/templates/base.html +++ b/theme/templates/base.html @@ -40,7 +40,7 @@ {% block javascript_head %}{% endblock %} -
+{{ error }}
+ {% endfor %} +{{ error }}
+ {% endfor %} +{{ error }}
+ {% endfor %} +