From f7a9b2f8237606c5d3f220bd728cf6030d9c02a4 Mon Sep 17 00:00:00 2001
From: badbl0cks <4161747+badbl0cks@users.noreply.github.com>
Date: Sun, 16 Mar 2025 19:06:36 -0700
Subject: [PATCH] Add rarity field to trade_offer instead of looking up via
cards
---
accounts/migrations/0001_initial.py | 2 +-
cards/migrations/0001_initial.py | 2 +-
cards/templatetags/card_badge.py | 10 ++++++----
django_project/settings.py | 14 +++++++++++---
django_project/urls.py | 1 +
home/views.py | 21 +++++++++++++--------
requirements.txt | 1 +
theme/templates/base.html | 2 +-
trades/migrations/0001_initial.py | 3 ++-
trades/models.py | 15 +++++++++++----
trades/signals.py | 21 +++++++++++++++++----
trades/templatetags/trade_offer_tags.py | 22 +++++++++++-----------
trades/views.py | 23 +++++++++++------------
13 files changed, 87 insertions(+), 50 deletions(-)
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
index db8f4b6..5add355 100644
--- a/accounts/migrations/0001_initial.py
+++ b/accounts/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.2 on 2025-03-16 04:58
+# Generated by Django 5.1.2 on 2025-03-16 18:18
import django.contrib.auth.models
import django.contrib.auth.validators
diff --git a/cards/migrations/0001_initial.py b/cards/migrations/0001_initial.py
index b13630e..52471c3 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-16 04:58
+# Generated by Django 5.1.2 on 2025-03-16 18:18
import django.db.models.deletion
from django.db import migrations, models
diff --git a/cards/templatetags/card_badge.py b/cards/templatetags/card_badge.py
index 1dc125b..9842704 100644
--- a/cards/templatetags/card_badge.py
+++ b/cards/templatetags/card_badge.py
@@ -6,11 +6,13 @@ register = template.Library()
@register.inclusion_tag("templatetags/card_badge.html")
def card_badge(card, quantity=1):
+ # Freeze the decks queryset once so that both the iteration and count use the same data
+ decks = list(card.decks.all()) if card else []
return {
'card': card,
'quantity': quantity,
- 'decks': card.decks.all() if card else None,
- 'num_decks': card.decks.count() if card else None,
+ 'decks': decks,
+ 'num_decks': len(decks),
}
@register.filter
@@ -21,7 +23,7 @@ def card_badge_inline(card, quantity=1):
html = render_to_string("templatetags/card_badge.html", {
'card': card,
'quantity': quantity,
- 'decks': card.decks.all() if card else None,
- 'num_decks': card.decks.count() if card else None,
+ 'decks': list(card.decks.all()) if card else [],
+ 'num_decks': len(list(card.decks.all())) if card else 0,
})
return mark_safe(html)
\ No newline at end of file
diff --git a/django_project/settings.py b/django_project/settings.py
index 81fdf59..46e34fe 100644
--- a/django_project/settings.py
+++ b/django_project/settings.py
@@ -47,7 +47,7 @@ INSTALLED_APPS = [
'allauth.socialaccount.providers.google',
"crispy_forms",
"crispy_tailwind",
- "debug_toolbar",
+ #"debug_toolbar",
"el_pagination",
"tailwind",
"theme",
@@ -56,8 +56,15 @@ INSTALLED_APPS = [
"cards",
"home",
"trades.apps.TradesConfig",
+ "silk",
]
+SILKY_PYTHON_PROFILER = True
+SILKY_AUTHENTICATION = True
+SILKY_AUTHORISATION = True
+SILKY_PERMISSIONS = lambda user: user.is_superuser
+SILKY_META = True
+
TAILWIND_APP_NAME = 'theme'
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
@@ -66,12 +73,13 @@ MIDDLEWARE = [
"whitenoise.middleware.WhiteNoiseMiddleware", # WhiteNoise
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
- "debug_toolbar.middleware.DebugToolbarMiddleware", # Django Debug Toolbar
+ #"debug_toolbar.middleware.DebugToolbarMiddleware", # Django Debug Toolbar
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware", # django-allauth
+ 'silk.middleware.SilkyMiddleware',
"django_browser_reload.middleware.BrowserReloadMiddleware",
#"django_project.middleware.AutoLoginMiddleware",
]
@@ -291,6 +299,6 @@ else:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
- "LOCATION": "site_cache",
+ "LOCATION": "django_site_cache",
}
}
\ No newline at end of file
diff --git a/django_project/urls.py b/django_project/urls.py
index 505ad30..c159e94 100644
--- a/django_project/urls.py
+++ b/django_project/urls.py
@@ -10,6 +10,7 @@ urlpatterns = [
path('account/', include('accounts.urls')),
path("trades/", include("trades.urls")),
path("__reload__/", include("django_browser_reload.urls")),
+ path('silk/', include('silk.urls', namespace='silk')),
]
if settings.DEBUG:
diff --git a/home/views.py b/home/views.py
index 176ed9f..a728f5d 100644
--- a/home/views.py
+++ b/home/views.py
@@ -9,10 +9,12 @@ from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.template.response import TemplateResponse
from django.http import HttpResponseRedirect
+from silk.profiling.profiler import silk_profile
class HomePageView(TemplateView):
template_name = "home/home.html"
+ @silk_profile(name='Home Page')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -29,37 +31,40 @@ class HomePageView(TemplateView):
context["most_offered_cards"] = (
Card.objects_no_prefetch.filter(tradeofferhavecard__isnull=False)
.annotate(offer_count=Sum("tradeofferhavecard__quantity"))
- .order_by("-offer_count", "?")[:6]
+ .order_by("-offer_count")[:6]
)
# Most Wanted Cards
context["most_wanted_cards"] = (
Card.objects_no_prefetch.filter(tradeofferwantcard__isnull=False)
.annotate(offer_count=Sum("tradeofferwantcard__quantity"))
- .order_by("-offer_count", "?")[:6]
+ .order_by("-offer_count")[:6]
)
# Least Offered Cards
context["least_offered_cards"] = (
Card.objects_no_prefetch.annotate(offer_count=Sum("tradeofferhavecard__quantity"))
- .order_by("offer_count", "?")[:6]
+ .order_by("offer_count")[:6]
)
featured = {}
+ # Featured "All" offers
featured["All"] = base_offer_qs.order_by("created_at")[:6]
+ # Get the normalized ids for rarities with pk<=5.
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)}
+ 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 each normalized id (sorted descending), filter base offers that have the matching trade offer 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
+ rarity__normalized_id=norm # now using trade_offer.rarity
).order_by("created_at").distinct()[:6]
icon_label = rarity_map.get(norm)
if icon_label:
diff --git a/requirements.txt b/requirements.txt
index 0781862..759a4bf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,6 +14,7 @@ django-daisy==1.0.13
django-debug-toolbar==4.4.6
django-el-pagination==4.1.2
django-environ==0.12.0
+django-silk==5.3.1
django-tailwind-4[reload]==0.1.4
django-widget-tweaks==1.5.0
gunicorn==23.0.0
diff --git a/theme/templates/base.html b/theme/templates/base.html
index a2a5fc3..68b1a9c 100644
--- a/theme/templates/base.html
+++ b/theme/templates/base.html
@@ -53,7 +53,7 @@
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
Home
- Trade
+ Trades
- All Offers
- My Trades
diff --git a/trades/migrations/0001_initial.py b/trades/migrations/0001_initial.py
index b19de94..0f2f879 100644
--- a/trades/migrations/0001_initial.py
+++ b/trades/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.2 on 2025-03-16 04:58
+# Generated by Django 5.1.2 on 2025-03-16 18:18
import django.db.models.deletion
from django.db import migrations, models
@@ -23,6 +23,7 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='initiated_trade_offers', to='accounts.friendcode')),
+ ('rarity', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to='cards.rarity')),
],
),
migrations.CreateModel(
diff --git a/trades/models.py b/trades/models.py
index 1d27a20..f5c5a5d 100644
--- a/trades/models.py
+++ b/trades/models.py
@@ -7,18 +7,18 @@ from accounts.models import FriendCode
class TradeOfferManager(models.Manager):
def get_queryset(self):
- queryset = super().get_queryset().select_related("initiated_by", "initiated_by__user")
+ queryset = super().get_queryset().select_related("initiated_by", "initiated_by__user", "rarity")
queryset = queryset.prefetch_related(
Prefetch(
"trade_offer_want_cards",
- queryset=TradeOfferWantCard.objects.select_related("card").annotate(
+ queryset=TradeOfferWantCard.objects.select_related("card").prefetch_related('card__decks').annotate(
total_quantity=Sum("quantity"),
total_accepted=Sum("qty_accepted")
).order_by("total_quantity", "id")
),
Prefetch(
"trade_offer_have_cards",
- queryset=TradeOfferHaveCard.objects.select_related("card").annotate(
+ queryset=TradeOfferHaveCard.objects.select_related("card").prefetch_related('card__decks').annotate(
total_quantity=Sum("quantity"),
total_accepted=Sum("qty_accepted")
).order_by("total_quantity", "id")
@@ -42,7 +42,14 @@ class TradeOffer(models.Model):
on_delete=models.PROTECT,
related_name='initiated_trade_offers'
)
- # Use custom through models to support multiples.
+ rarity = models.ForeignKey(
+ "cards.Rarity",
+ on_delete=models.PROTECT,
+ null=True,
+ blank=True,
+ editable=False,
+ db_index=True
+ )
want_cards = models.ManyToManyField(
"cards.Card",
related_name='trade_offers_want',
diff --git a/trades/signals.py b/trades/signals.py
index 8995f12..89e3ab7 100644
--- a/trades/signals.py
+++ b/trades/signals.py
@@ -6,22 +6,35 @@ from cards.models import Card
from django.db.models import F
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance
-def check_trade_offer_rarity(instance):
+def validate_and_set_trade_offer_rarity(instance):
+ """
+ Ensures all cards on both sides share the same rarity and sets the TradeOffer.rarity
+ if it hasn't been set already.
+ """
+ # Combine cards from both sides.
combined_cards = list(instance.have_cards.all()) + list(instance.want_cards.all())
- # Use the normalized rarity from each card
+ if not combined_cards:
+ return
+
+ # Gather the Rarity instances from the cards.
rarities = {card.normalized_rarity for card in combined_cards}
if len(rarities) > 1:
raise ValidationError("All cards in a trade offer must have the same rarity.")
+ # If trade offer's rarity isn't set yet, update it.
+ if instance.rarity is None:
+ instance.rarity = combined_cards[0].normalized_rarity
+ instance.save(update_fields=["rarity"])
+
@receiver(m2m_changed, sender=TradeOffer.have_cards.through)
def validate_have_cards_rarity(sender, instance, action, **kwargs):
if action == "post_add":
- check_trade_offer_rarity(instance)
+ validate_and_set_trade_offer_rarity(instance)
@receiver(m2m_changed, sender=TradeOffer.want_cards.through)
def validate_want_cards_rarity(sender, instance, action, **kwargs):
if action == "post_add":
- check_trade_offer_rarity(instance)
+ validate_and_set_trade_offer_rarity(instance)
ACTIVE_STATES = [
TradeAcceptance.AcceptanceState.ACCEPTED,
diff --git a/trades/templatetags/trade_offer_tags.py b/trades/templatetags/trade_offer_tags.py
index a02762f..e99664c 100644
--- a/trades/templatetags/trade_offer_tags.py
+++ b/trades/templatetags/trade_offer_tags.py
@@ -6,19 +6,19 @@ register = template.Library()
def render_trade_offer(context, offer):
"""
Renders a trade offer including detailed trade acceptance information.
- Groups acceptances for each card on both the have and want sides.
+ Freezes the through-model querysets to avoid extra DB hits.
"""
-
- have_cards_available = []
- want_cards_available = []
+ trade_offer_have_cards = list(offer.trade_offer_have_cards.all())
+ trade_offer_want_cards = list(offer.trade_offer_want_cards.all())
- for card in offer.trade_offer_have_cards.all():
- if (card.quantity > card.qty_accepted):
- have_cards_available.append(card)
-
- for card in offer.trade_offer_want_cards.all():
- if (card.quantity > card.qty_accepted):
- want_cards_available.append(card)
+ have_cards_available = [
+ card for card in trade_offer_have_cards
+ if card.quantity > card.qty_accepted
+ ]
+ want_cards_available = [
+ card for card in trade_offer_want_cards
+ if card.quantity > card.qty_accepted
+ ]
return {
'offer': offer,
diff --git a/trades/views.py b/trades/views.py
index 99e2a8d..4cde149 100644
--- a/trades/views.py
+++ b/trades/views.py
@@ -16,6 +16,7 @@ from .models import TradeOffer, TradeAcceptance
from .forms import (TradeOfferAcceptForm,
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
from cards.models import Card
+from silk.profiling.profiler import silk_profile
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
model = TradeOffer
@@ -67,6 +68,7 @@ class TradeOfferAllListView(ListView):
model = TradeOffer
template_name = "trades/trade_offer_all_list.html"
+ @silk_profile(name="Trade Offer All List- Get Context Data")
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs)
request = self.request
@@ -86,6 +88,7 @@ class TradeOfferAllListView(ListView):
context["all_trade_offers_paginated"] = offers_paginator.get_page(offers_page)
return context
+ @silk_profile(name="Trade Offer All List- Render to Response")
def render_to_response(self, context, **response_kwargs):
# For AJAX requests, return only the paginated fragment.
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
@@ -182,6 +185,7 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView):
other_acceptances = involved_acceptances.exclude(pk__in=waiting_acceptances.values("pk"))
return Paginator(other_acceptances, 10).get_page(page_param)
+ @silk_profile(name="Trade Offer My List- Get Context Data")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
request = self.request
@@ -202,6 +206,7 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView):
return context
+ @silk_profile(name="Trade Offer My List- Render to Response")
def render_to_response(self, context, **response_kwargs):
# For AJAX requests, return only the paginated fragment.
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
@@ -313,6 +318,7 @@ class TradeOfferSearchView(ListView):
results.append((card_id, qty))
return results
+ @silk_profile(name="Trade Offer Search- Get Queryset")
def get_queryset(self):
from django.db.models import F
# For a GET request (initial load), return an empty queryset.
@@ -349,10 +355,12 @@ class TradeOfferSearchView(ListView):
return qs.distinct()
+ @silk_profile(name="Trade Offer Search- Post")
def post(self, request, *args, **kwargs):
# For POST, simply process the search through get().
return self.get(request, *args, **kwargs)
+ @silk_profile(name="Trade Offer Search- Get Context Data")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
from cards.models import Card
@@ -367,6 +375,7 @@ class TradeOfferSearchView(ListView):
context["wanted_cards"] = []
return context
+ @silk_profile(name="Trade Offer Search- Render to Response")
def render_to_response(self, context, **response_kwargs):
"""
Render the AJAX fragment if the request is AJAX; otherwise, render the complete page.
@@ -386,6 +395,7 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
model = TradeOffer
template_name = "trades/trade_offer_detail.html"
+ @silk_profile(name="Trade Offer Detail- Get Context Data")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
trade_offer = self.get_object()
@@ -446,18 +456,7 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
def get_trade_offer(self):
return (
- TradeOffer.objects.select_related('initiated_by')
- .prefetch_related(
- 'trade_offer_want_cards__card',
- 'trade_offer_have_cards__card',
- Prefetch(
- 'acceptances',
- queryset=TradeAcceptance.objects.select_related(
- 'accepted_by', 'requested_card', 'offered_card'
- )
- )
- )
- .get(pk=self.kwargs['offer_pk'])
+ TradeOffer.objects.get(pk=self.kwargs['offer_pk'])
)
def get_form_kwargs(self):