Add rarity field to trade_offer instead of looking up via cards

This commit is contained in:
badblocks 2025-03-16 19:06:36 -07:00
parent ba33139993
commit f7a9b2f823
13 changed files with 87 additions and 50 deletions

View file

@ -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.models
import django.contrib.auth.validators import django.contrib.auth.validators

View file

@ -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 import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View file

@ -6,11 +6,13 @@ register = template.Library()
@register.inclusion_tag("templatetags/card_badge.html") @register.inclusion_tag("templatetags/card_badge.html")
def card_badge(card, quantity=1): 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 { return {
'card': card, 'card': card,
'quantity': quantity, 'quantity': quantity,
'decks': card.decks.all() if card else None, 'decks': decks,
'num_decks': card.decks.count() if card else None, 'num_decks': len(decks),
} }
@register.filter @register.filter
@ -21,7 +23,7 @@ def card_badge_inline(card, quantity=1):
html = render_to_string("templatetags/card_badge.html", { html = render_to_string("templatetags/card_badge.html", {
'card': card, 'card': card,
'quantity': quantity, 'quantity': quantity,
'decks': card.decks.all() if card else None, 'decks': list(card.decks.all()) if card else [],
'num_decks': card.decks.count() if card else None, 'num_decks': len(list(card.decks.all())) if card else 0,
}) })
return mark_safe(html) return mark_safe(html)

View file

@ -47,7 +47,7 @@ INSTALLED_APPS = [
'allauth.socialaccount.providers.google', 'allauth.socialaccount.providers.google',
"crispy_forms", "crispy_forms",
"crispy_tailwind", "crispy_tailwind",
"debug_toolbar", #"debug_toolbar",
"el_pagination", "el_pagination",
"tailwind", "tailwind",
"theme", "theme",
@ -56,8 +56,15 @@ INSTALLED_APPS = [
"cards", "cards",
"home", "home",
"trades.apps.TradesConfig", "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' TAILWIND_APP_NAME = 'theme'
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware # https://docs.djangoproject.com/en/dev/ref/settings/#middleware
@ -66,12 +73,13 @@ MIDDLEWARE = [
"whitenoise.middleware.WhiteNoiseMiddleware", # WhiteNoise "whitenoise.middleware.WhiteNoiseMiddleware", # WhiteNoise
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware", # Django Debug Toolbar #"debug_toolbar.middleware.DebugToolbarMiddleware", # Django Debug Toolbar
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware", # django-allauth "allauth.account.middleware.AccountMiddleware", # django-allauth
'silk.middleware.SilkyMiddleware',
"django_browser_reload.middleware.BrowserReloadMiddleware", "django_browser_reload.middleware.BrowserReloadMiddleware",
#"django_project.middleware.AutoLoginMiddleware", #"django_project.middleware.AutoLoginMiddleware",
] ]
@ -291,6 +299,6 @@ else:
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache", "BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "site_cache", "LOCATION": "django_site_cache",
} }
} }

View file

@ -10,6 +10,7 @@ urlpatterns = [
path('account/', include('accounts.urls')), path('account/', include('accounts.urls')),
path("trades/", include("trades.urls")), path("trades/", include("trades.urls")),
path("__reload__/", include("django_browser_reload.urls")), path("__reload__/", include("django_browser_reload.urls")),
path('silk/', include('silk.urls', namespace='silk')),
] ]
if settings.DEBUG: if settings.DEBUG:

View file

@ -9,10 +9,12 @@ from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from silk.profiling.profiler import silk_profile
class HomePageView(TemplateView): class HomePageView(TemplateView):
template_name = "home/home.html" template_name = "home/home.html"
@silk_profile(name='Home Page')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -29,37 +31,40 @@ class HomePageView(TemplateView):
context["most_offered_cards"] = ( context["most_offered_cards"] = (
Card.objects_no_prefetch.filter(tradeofferhavecard__isnull=False) Card.objects_no_prefetch.filter(tradeofferhavecard__isnull=False)
.annotate(offer_count=Sum("tradeofferhavecard__quantity")) .annotate(offer_count=Sum("tradeofferhavecard__quantity"))
.order_by("-offer_count", "?")[:6] .order_by("-offer_count")[:6]
) )
# Most Wanted Cards # Most Wanted Cards
context["most_wanted_cards"] = ( context["most_wanted_cards"] = (
Card.objects_no_prefetch.filter(tradeofferwantcard__isnull=False) Card.objects_no_prefetch.filter(tradeofferwantcard__isnull=False)
.annotate(offer_count=Sum("tradeofferwantcard__quantity")) .annotate(offer_count=Sum("tradeofferwantcard__quantity"))
.order_by("-offer_count", "?")[:6] .order_by("-offer_count")[:6]
) )
# Least Offered Cards # Least Offered Cards
context["least_offered_cards"] = ( context["least_offered_cards"] = (
Card.objects_no_prefetch.annotate(offer_count=Sum("tradeofferhavecard__quantity")) Card.objects_no_prefetch.annotate(offer_count=Sum("tradeofferhavecard__quantity"))
.order_by("offer_count", "?")[:6] .order_by("offer_count")[:6]
) )
featured = {} featured = {}
# Featured "All" offers
featured["All"] = base_offer_qs.order_by("created_at")[:6] featured["All"] = base_offer_qs.order_by("created_at")[:6]
# Get the normalized ids for rarities with pk<=5.
normalized_ids = list( normalized_ids = list(
Rarity.objects.filter(pk__lte=5).values_list("normalized_id", flat=True).distinct() 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): for norm in sorted(normalized_ids, reverse=True):
offers_qs = base_offer_qs.filter( offers_qs = base_offer_qs.filter(
have_cards__rarity__normalized_id=norm rarity__normalized_id=norm # now using trade_offer.rarity
# 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] ).order_by("created_at").distinct()[:6]
icon_label = rarity_map.get(norm) icon_label = rarity_map.get(norm)
if icon_label: if icon_label:

View file

@ -14,6 +14,7 @@ django-daisy==1.0.13
django-debug-toolbar==4.4.6 django-debug-toolbar==4.4.6
django-el-pagination==4.1.2 django-el-pagination==4.1.2
django-environ==0.12.0 django-environ==0.12.0
django-silk==5.3.1
django-tailwind-4[reload]==0.1.4 django-tailwind-4[reload]==0.1.4
django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
gunicorn==23.0.0 gunicorn==23.0.0

View file

@ -53,7 +53,7 @@
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"> class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
<li><a href="{% url 'home' %}">Home</a></li> <li><a href="{% url 'home' %}">Home</a></li>
<li> <li>
<a>Trade</a> <a>Trades</a>
<ul class="p-2"> <ul class="p-2">
<li><a href="{% url 'trade_offer_list' %}">All Offers</a></li> <li><a href="{% url 'trade_offer_list' %}">All Offers</a></li>
<li><a href="{% url 'trade_offer_my_list' %}">My Trades</a></li> <li><a href="{% url 'trade_offer_my_list' %}">My Trades</a></li>

View file

@ -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 import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -23,6 +23,7 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=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')), ('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( migrations.CreateModel(

View file

@ -7,18 +7,18 @@ from accounts.models import FriendCode
class TradeOfferManager(models.Manager): class TradeOfferManager(models.Manager):
def get_queryset(self): 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( queryset = queryset.prefetch_related(
Prefetch( Prefetch(
"trade_offer_want_cards", "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_quantity=Sum("quantity"),
total_accepted=Sum("qty_accepted") total_accepted=Sum("qty_accepted")
).order_by("total_quantity", "id") ).order_by("total_quantity", "id")
), ),
Prefetch( Prefetch(
"trade_offer_have_cards", "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_quantity=Sum("quantity"),
total_accepted=Sum("qty_accepted") total_accepted=Sum("qty_accepted")
).order_by("total_quantity", "id") ).order_by("total_quantity", "id")
@ -42,7 +42,14 @@ class TradeOffer(models.Model):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='initiated_trade_offers' 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( want_cards = models.ManyToManyField(
"cards.Card", "cards.Card",
related_name='trade_offers_want', related_name='trade_offers_want',

View file

@ -6,22 +6,35 @@ from cards.models import Card
from django.db.models import F from django.db.models import F
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance 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()) 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} rarities = {card.normalized_rarity for card in combined_cards}
if len(rarities) > 1: if len(rarities) > 1:
raise ValidationError("All cards in a trade offer must have the same rarity.") 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) @receiver(m2m_changed, sender=TradeOffer.have_cards.through)
def validate_have_cards_rarity(sender, instance, action, **kwargs): def validate_have_cards_rarity(sender, instance, action, **kwargs):
if action == "post_add": if action == "post_add":
check_trade_offer_rarity(instance) validate_and_set_trade_offer_rarity(instance)
@receiver(m2m_changed, sender=TradeOffer.want_cards.through) @receiver(m2m_changed, sender=TradeOffer.want_cards.through)
def validate_want_cards_rarity(sender, instance, action, **kwargs): def validate_want_cards_rarity(sender, instance, action, **kwargs):
if action == "post_add": if action == "post_add":
check_trade_offer_rarity(instance) validate_and_set_trade_offer_rarity(instance)
ACTIVE_STATES = [ ACTIVE_STATES = [
TradeAcceptance.AcceptanceState.ACCEPTED, TradeAcceptance.AcceptanceState.ACCEPTED,

View file

@ -6,19 +6,19 @@ register = template.Library()
def render_trade_offer(context, offer): def render_trade_offer(context, offer):
""" """
Renders a trade offer including detailed trade acceptance information. 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.
""" """
trade_offer_have_cards = list(offer.trade_offer_have_cards.all())
trade_offer_want_cards = list(offer.trade_offer_want_cards.all())
have_cards_available = [] have_cards_available = [
want_cards_available = [] card for card in trade_offer_have_cards
if card.quantity > card.qty_accepted
for card in offer.trade_offer_have_cards.all(): ]
if (card.quantity > card.qty_accepted): want_cards_available = [
have_cards_available.append(card) card for card in trade_offer_want_cards
if card.quantity > card.qty_accepted
for card in offer.trade_offer_want_cards.all(): ]
if (card.quantity > card.qty_accepted):
want_cards_available.append(card)
return { return {
'offer': offer, 'offer': offer,

View file

@ -16,6 +16,7 @@ from .models import TradeOffer, TradeAcceptance
from .forms import (TradeOfferAcceptForm, from .forms import (TradeOfferAcceptForm,
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm) TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
from cards.models import Card from cards.models import Card
from silk.profiling.profiler import silk_profile
class TradeOfferCreateView(LoginRequiredMixin, CreateView): class TradeOfferCreateView(LoginRequiredMixin, CreateView):
model = TradeOffer model = TradeOffer
@ -67,6 +68,7 @@ class TradeOfferAllListView(ListView):
model = TradeOffer model = TradeOffer
template_name = "trades/trade_offer_all_list.html" 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): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
request = self.request request = self.request
@ -86,6 +88,7 @@ class TradeOfferAllListView(ListView):
context["all_trade_offers_paginated"] = offers_paginator.get_page(offers_page) context["all_trade_offers_paginated"] = offers_paginator.get_page(offers_page)
return context return context
@silk_profile(name="Trade Offer All List- Render to Response")
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
# For AJAX requests, return only the paginated fragment. # For AJAX requests, return only the paginated fragment.
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest": 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")) other_acceptances = involved_acceptances.exclude(pk__in=waiting_acceptances.values("pk"))
return Paginator(other_acceptances, 10).get_page(page_param) 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
request = self.request request = self.request
@ -202,6 +206,7 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView):
return context return context
@silk_profile(name="Trade Offer My List- Render to Response")
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
# For AJAX requests, return only the paginated fragment. # For AJAX requests, return only the paginated fragment.
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest": if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
@ -313,6 +318,7 @@ class TradeOfferSearchView(ListView):
results.append((card_id, qty)) results.append((card_id, qty))
return results return results
@silk_profile(name="Trade Offer Search- Get Queryset")
def get_queryset(self): def get_queryset(self):
from django.db.models import F from django.db.models import F
# For a GET request (initial load), return an empty queryset. # For a GET request (initial load), return an empty queryset.
@ -349,10 +355,12 @@ class TradeOfferSearchView(ListView):
return qs.distinct() return qs.distinct()
@silk_profile(name="Trade Offer Search- Post")
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# For POST, simply process the search through get(). # For POST, simply process the search through get().
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
@silk_profile(name="Trade Offer Search- Get Context Data")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
from cards.models import Card from cards.models import Card
@ -367,6 +375,7 @@ class TradeOfferSearchView(ListView):
context["wanted_cards"] = [] context["wanted_cards"] = []
return context return context
@silk_profile(name="Trade Offer Search- Render to Response")
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
""" """
Render the AJAX fragment if the request is AJAX; otherwise, render the complete page. Render the AJAX fragment if the request is AJAX; otherwise, render the complete page.
@ -386,6 +395,7 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
model = TradeOffer model = TradeOffer
template_name = "trades/trade_offer_detail.html" template_name = "trades/trade_offer_detail.html"
@silk_profile(name="Trade Offer Detail- Get Context Data")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
trade_offer = self.get_object() trade_offer = self.get_object()
@ -446,18 +456,7 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
def get_trade_offer(self): def get_trade_offer(self):
return ( return (
TradeOffer.objects.select_related('initiated_by') TradeOffer.objects.get(pk=self.kwargs['offer_pk'])
.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'])
) )
def get_form_kwargs(self): def get_form_kwargs(self):