Refactor database models to majorly increase queries needed and decrease load times of home from 30 secs to 5 sec (we will be caching the rest to decrease even further via background tasks)

This commit is contained in:
badblocks 2025-03-17 14:08:01 -07:00
parent f7a9b2f823
commit 86c7eba10a
25 changed files with 1941 additions and 1560 deletions

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-03-16 18:18
# Generated by Django 5.1.2 on 2025-03-17 20:39
import django.db.models.deletion
from django.db import migrations, models
@ -20,10 +20,11 @@ class Migration(migrations.Migration):
('id', models.AutoField(primary_key=True, serialize=False)),
('is_closed', models.BooleanField(db_index=True, default=False)),
('hash', models.CharField(editable=False, max_length=9)),
('rarity_icon', models.CharField(max_length=8, null=True)),
('rarity_level', models.IntegerField(null=True)),
('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(

View file

@ -7,30 +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", "rarity")
queryset = queryset.prefetch_related(
Prefetch(
"trade_offer_want_cards",
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").prefetch_related('card__decks').annotate(
total_quantity=Sum("quantity"),
total_accepted=Sum("qty_accepted")
).order_by("total_quantity", "id")
),
Prefetch(
"acceptances",
queryset=TradeAcceptance.objects.select_related("accepted_by", "accepted_by__user", "requested_card", "offered_card")
),
queryset = super().get_queryset().select_related(
"initiated_by__user",
).prefetch_related(
"trade_offer_have_cards__card",
"trade_offer_want_cards__card",
"acceptances",
"acceptances__requested_card",
"acceptances__offered_card",
"acceptances__accepted_by__user",
).order_by("-updated_at")
return queryset
class TradeOffer(models.Model):
objects = TradeOfferManager()
@ -42,14 +30,8 @@ class TradeOffer(models.Model):
on_delete=models.PROTECT,
related_name='initiated_trade_offers'
)
rarity = models.ForeignKey(
"cards.Rarity",
on_delete=models.PROTECT,
null=True,
blank=True,
editable=False,
db_index=True
)
rarity_icon = models.CharField(max_length=8, null=True)
rarity_level = models.IntegerField(null=True)
want_cards = models.ManyToManyField(
"cards.Card",
related_name='trade_offers_want',

View file

@ -5,36 +5,40 @@ from .models import TradeOffer
from cards.models import Card
from django.db.models import F
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance
from django.db import transaction
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.
Ensures all cards on both sides share the same rarity and sets the TradeOffer's
rarity_level and rarity_icon if they haven't been set already.
"""
# Combine cards from both sides.
combined_cards = list(instance.have_cards.all()) + list(instance.want_cards.all())
if not combined_cards:
return
# Gather the Rarity instances from the cards.
rarities = {card.normalized_rarity for card in combined_cards}
rarities = {card.rarity_level 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"])
updated_fields = []
if instance.rarity_level is None:
instance.rarity_level = combined_cards[0].rarity_level
updated_fields.append("rarity_level")
if instance.rarity_icon is None:
instance.rarity_icon = combined_cards[0].rarity_icon
updated_fields.append("rarity_icon")
if updated_fields:
instance.save(update_fields=updated_fields)
@receiver(m2m_changed, sender=TradeOffer.have_cards.through)
def validate_have_cards_rarity(sender, instance, action, **kwargs):
if action == "post_add":
validate_and_set_trade_offer_rarity(instance)
transaction.on_commit(lambda: 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":
validate_and_set_trade_offer_rarity(instance)
transaction.on_commit(lambda: validate_and_set_trade_offer_rarity(instance))
ACTIVE_STATES = [
TradeAcceptance.AcceptanceState.ACCEPTED,

View file

@ -20,8 +20,17 @@ def render_trade_offer(context, offer):
if card.quantity > card.qty_accepted
]
acceptances = [acceptance for acceptance in list(offer.acceptances.all())
if acceptance.is_active
]
return {
'offer': offer,
'offer_pk': offer.pk,
'offer_hash': offer.hash,
'rarity_icon': offer.rarity_icon,
'initiated_by_email': offer.initiated_by.user.email,
'initiated_by_username': offer.initiated_by.user.username,
'acceptances': acceptances,
'have_cards_available': have_cards_available,
'want_cards_available': want_cards_available,
}

View file

@ -16,7 +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
#from silk.profiling.profiler import silk_profile
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
model = TradeOffer
@ -42,7 +42,7 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
context = super().get_context_data(**kwargs)
from cards.models import Card
# Ensure available_cards is a proper QuerySet
context["cards"] = Card.objects.all().order_by("name", "rarity__pk") \
context["cards"] = Card.objects.all().order_by("name", "rarity_level") \
.select_related("rarity", "cardset") \
.prefetch_related("decks")
friend_codes = self.request.user.friend_codes.all()
@ -68,7 +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")
#@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
@ -88,14 +88,14 @@ 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")
#@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":
page = self.request.GET.get("page")
show_closed = self.request.GET.get("show_closed", "false").lower() == "true"
queryset = TradeOffer.objects.all()
queryset = TradeOffer.objects
if show_closed:
queryset = queryset.filter(is_closed=True)
else:
@ -185,7 +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")
#@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
@ -206,7 +206,7 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView):
return context
@silk_profile(name="Trade Offer My List- Render to Response")
#@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":
@ -318,7 +318,7 @@ class TradeOfferSearchView(ListView):
results.append((card_id, qty))
return results
@silk_profile(name="Trade Offer Search- Get Queryset")
#@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.
@ -355,18 +355,17 @@ class TradeOfferSearchView(ListView):
return qs.distinct()
@silk_profile(name="Trade Offer Search- Post")
#@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")
#@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
# Populate available_cards to re-populate the multiselects.
context["cards"] = Card.objects.all().order_by("name", "rarity__pk") \
.select_related("rarity", "cardset")
context["cards"] = Card.objects.all().order_by("name")
if self.request.method == "POST":
context["offered_cards"] = self.request.POST.getlist("offered_cards")
context["wanted_cards"] = self.request.POST.getlist("wanted_cards")
@ -375,7 +374,7 @@ class TradeOfferSearchView(ListView):
context["wanted_cards"] = []
return context
@silk_profile(name="Trade Offer Search- Render to Response")
#@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.
@ -395,7 +394,7 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
model = TradeOffer
template_name = "trades/trade_offer_detail.html"
@silk_profile(name="Trade Offer Detail- Get Context Data")
#@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()