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 %} - +
{% else %}
Login diff --git a/theme/templates/trades/_friend_code_select.html b/theme/templates/trades/_friend_code_select.html index 844c570..043bc1a 100644 --- a/theme/templates/trades/_friend_code_select.html +++ b/theme/templates/trades/_friend_code_select.html @@ -25,7 +25,7 @@ Expected variables:
{% else %} - + {% endif %} {% endwith %} {% endwith %} \ No newline at end of file diff --git a/theme/templates/trades/trade_offer_detail.html b/theme/templates/trades/trade_offer_detail.html index bf69c91..73de7e8 100644 --- a/theme/templates/trades/trade_offer_detail.html +++ b/theme/templates/trades/trade_offer_detail.html @@ -6,25 +6,54 @@ {% block content %}

Trade Offer Details

-
+
{% render_trade_offer object %}
{% if acceptance_form %} -

Accept This Offer

-
+
+

Accept This Offer

{% csrf_token %} - {{ acceptance_form.as_p }} - -
-
- {% endif %} + -
- {% if is_initiator %} - Delete/Close Trade Offer +
+ + {{ acceptance_form.offered_card }} + {% for error in acceptance_form.offered_card.errors %} +

{{ error }}

+ {% endfor %} +
+ +
+ + {{ acceptance_form.requested_card }} + {% for error in acceptance_form.requested_card.errors %} +

{{ error }}

+ {% endfor %} +
+ +
+ {% if not acceptance_form.accepted_by.field.widget.is_hidden %} + + {% endif %} + {{ acceptance_form.accepted_by }} + {% for error in acceptance_form.accepted_by.errors %} +

{{ error }}

+ {% endfor %} +
+
{% endif %} - Back to Trade Offers + +
+ Back to Trade Offers + {% if is_initiator %} + Delete/Close Trade Offer + {% else %} + + {% endif %} +
{% endblock content %} \ No newline at end of file diff --git a/theme/templates/trades/trade_offer_my_list.html b/theme/templates/trades/trade_offer_my_list.html index 9c322e3..6f3a05c 100644 --- a/theme/templates/trades/trade_offer_my_list.html +++ b/theme/templates/trades/trade_offer_my_list.html @@ -57,8 +57,7 @@
+ x-init="init()"> {% include "trades/_trade_offer_list_paginated.html" with offers=my_trade_offers_paginated %}
diff --git a/trades/forms.py b/trades/forms.py index 38ac33e..f6ddb8f 100644 --- a/trades/forms.py +++ b/trades/forms.py @@ -29,31 +29,38 @@ class TradeAcceptanceCreateForm(forms.ModelForm): Expects the caller to pass: - trade_offer: the instance of TradeOffer this acceptance is for. - friend_codes: a queryset of FriendCode objects for the current user. + - default_friend_code (optional): the user's default FriendCode. It filters available requested and offered cards based on what's still available. """ class Meta: model = TradeAcceptance fields = ["accepted_by", "requested_card", "offered_card"] - def __init__(self, *args, trade_offer=None, friend_codes=None, **kwargs): + def __init__(self, *args, trade_offer=None, friend_codes=None, default_friend_code=None, **kwargs): if trade_offer is None: raise ValueError("trade_offer must be provided to filter choices.") super().__init__(*args, **kwargs) self.trade_offer = trade_offer - # Filter accepted_by to those friend codes that belong to the current user. if friend_codes is None: raise ValueError("friend_codes must be provided") + # Set the accepted_by queryset to the user's friend codes. self.fields["accepted_by"].queryset = friend_codes - # Update active_states to include only states that mean the acceptance is still "open". + # If the user only has one friend code, preset the field and use a HiddenInput. + if friend_codes.count() == 1: + self.initial["accepted_by"] = friend_codes.first().pk + self.fields["accepted_by"].widget = forms.HiddenInput() + # Otherwise, if a default friend code is provided and it is in the queryset, preselect it. + elif default_friend_code and friend_codes.filter(pk=default_friend_code.pk).exists(): + self.initial["accepted_by"] = default_friend_code.pk + + # Update available requested_card choices from the TradeOffer's "have" side. active_states = [ TradeAcceptance.AcceptanceState.ACCEPTED, TradeAcceptance.AcceptanceState.SENT, TradeAcceptance.AcceptanceState.RECEIVED, ] - - # Build available requested_card choices from the TradeOffer's "have" side. available_requested_ids = [] for through_obj in trade_offer.trade_offer_have_cards.all(): active_count = trade_offer.acceptances.filter( @@ -64,7 +71,7 @@ class TradeAcceptanceCreateForm(forms.ModelForm): available_requested_ids.append(through_obj.card.id) self.fields["requested_card"].queryset = Card.objects.filter(id__in=available_requested_ids) - # Similarly, build available offered_card choices from the TradeOffer's "want" side. + # Update available offered_card choices from the TradeOffer's "want" side. available_offered_ids = [] for through_obj in trade_offer.trade_offer_want_cards.all(): active_count = trade_offer.acceptances.filter( diff --git a/trades/migrations/0001_initial.py b/trades/migrations/0001_initial.py index 9e749e8..2f5f330 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-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 diff --git a/trades/models.py b/trades/models.py index b7ec4ac..1d27a20 100644 --- a/trades/models.py +++ b/trades/models.py @@ -1,11 +1,39 @@ from django.db import models from django.core.exceptions import ValidationError -from django.db.models import Q +from django.db.models import Q, Count, Prefetch, F, Sum import hashlib from cards.models import Card 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 = queryset.prefetch_related( + Prefetch( + "trade_offer_want_cards", + queryset=TradeOfferWantCard.objects.select_related("card").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( + 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") + ), + ).order_by("-updated_at") + return queryset + class TradeOffer(models.Model): + objects = TradeOfferManager() + id = models.AutoField(primary_key=True) is_closed = models.BooleanField(default=False, db_index=True) hash = models.CharField(max_length=9, editable=False) diff --git a/trades/signals.py b/trades/signals.py index 908328a..8995f12 100644 --- a/trades/signals.py +++ b/trades/signals.py @@ -1,8 +1,9 @@ from django.core.exceptions import ValidationError -from django.db.models.signals import m2m_changed, post_save, post_delete +from django.db.models.signals import m2m_changed, post_save, post_delete, pre_save from django.dispatch import receiver from .models import TradeOffer from cards.models import Card +from django.db.models import F from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance def check_trade_offer_rarity(instance): @@ -31,46 +32,79 @@ ACTIVE_STATES = [ TradeAcceptance.AcceptanceState.THANKED_BY_BOTH, ] -def update_qty_for_trade_offer(trade_offer, card, side): +def adjust_qty_for_trade_offer(trade_offer, card, side, delta): + """ + Increment (or decrement) qty_accepted by delta for the given card on the specified side. + """ if side == 'have': - count = TradeAcceptance.objects.filter( - trade_offer=trade_offer, - requested_card=card, - state__in=ACTIVE_STATES - ).count() TradeOfferHaveCard.objects.filter( - trade_offer=trade_offer, - card=card - ).update(qty_accepted=count) - - if count >= TradeOfferHaveCard.objects.filter(trade_offer=trade_offer, card=card).first().quantity: - trade_offer.is_closed = True - trade_offer.save(update_fields=["is_closed"]) - elif side == 'want': - count = TradeAcceptance.objects.filter( trade_offer=trade_offer, - offered_card=card, - state__in=ACTIVE_STATES - ).count() - TradeOfferWantCard.objects.filter( - trade_offer=trade_offer, card=card - ).update(qty_accepted=count) - - if count >= TradeOfferWantCard.objects.filter(trade_offer=trade_offer, card=card).first().quantity: - trade_offer.is_closed = True - trade_offer.save(update_fields=["is_closed"]) + ).update(qty_accepted=F('qty_accepted') + delta) + elif side == 'want': + TradeOfferWantCard.objects.filter( + trade_offer=trade_offer, + card=card + ).update(qty_accepted=F('qty_accepted') + delta) +def update_trade_offer_closed_status(trade_offer): + """ + Check if both sides of the trade offer meet the quantity requirement. + Mark the trade_offer as closed if all cards have qty_accepted + greater than or equal to quantity; otherwise, mark it as open. + """ + have_complete = not TradeOfferHaveCard.objects.filter( + trade_offer=trade_offer, + qty_accepted__lt=F('quantity') + ).exists() + want_complete = not TradeOfferWantCard.objects.filter( + trade_offer=trade_offer, + qty_accepted__lt=F('quantity') + ).exists() + closed = have_complete or want_complete + if trade_offer.is_closed != closed: + trade_offer.is_closed = closed + trade_offer.save(update_fields=["is_closed"]) -def update_all_qty(instance): - trade_offer = instance.trade_offer - update_qty_for_trade_offer(trade_offer, instance.requested_card, 'have') - update_qty_for_trade_offer(trade_offer, instance.offered_card, 'want') +# Pre-save signal to capture the original state before any changes. +@receiver(pre_save, sender=TradeAcceptance) +def trade_acceptance_pre_save(sender, instance, **kwargs): + if instance.pk: + old_instance = TradeAcceptance.objects.get(pk=instance.pk) + instance._old_state = old_instance.state +# Post-save signal to adjust qty_accepted incrementally. @receiver(post_save, sender=TradeAcceptance) -def trade_acceptance_post_save(sender, instance, **kwargs): - update_all_qty(instance) +def trade_acceptance_post_save(sender, instance, created, **kwargs): + delta = 0 + if created: + # For a new acceptance, increment only if the state is active. + if instance.state in ACTIVE_STATES: + delta = 1 + else: + old_state = getattr(instance, '_old_state', None) + if old_state is not None: + # Transition from active to non-active (e.g. a rejection) + if old_state in ACTIVE_STATES and instance.state not in ACTIVE_STATES: + delta = -1 + # Transition from non-active to active + elif old_state not in ACTIVE_STATES and instance.state in ACTIVE_STATES: + delta = 1 + if delta != 0: + trade_offer = instance.trade_offer + # Update the "have" side using the requested_card. + adjust_qty_for_trade_offer(trade_offer, instance.requested_card, side='have', delta=delta) + # Update the "want" side using the offered_card. + adjust_qty_for_trade_offer(trade_offer, instance.offered_card, side='want', delta=delta) + update_trade_offer_closed_status(trade_offer) + +# Post-delete signal to decrement qty_accepted if the deleted acceptance was active. @receiver(post_delete, sender=TradeAcceptance) def trade_acceptance_post_delete(sender, instance, **kwargs): - update_all_qty(instance) \ No newline at end of file + if instance.state in ACTIVE_STATES: + delta = -1 + trade_offer = instance.trade_offer + adjust_qty_for_trade_offer(trade_offer, instance.requested_card, side='have', delta=delta) + adjust_qty_for_trade_offer(trade_offer, instance.offered_card, side='want', delta=delta) + update_trade_offer_closed_status(trade_offer) \ No newline at end of file diff --git a/trades/views.py b/trades/views.py index d9ad5dc..99e2a8d 100644 --- a/trades/views.py +++ b/trades/views.py @@ -74,31 +74,7 @@ class TradeOfferAllListView(ListView): context["show_closed"] = show_closed # Build the queryset with our related objects. - queryset = ( - TradeOffer.objects.select_related('initiated_by') - .prefetch_related( - 'trade_offer_have_cards__card', - 'trade_offer_want_cards__card', - Prefetch( - 'acceptances', - queryset=TradeAcceptance.objects.select_related('accepted_by', 'requested_card', 'offered_card').prefetch_related( - 'requested_card__decks', - 'offered_card__decks', - 'requested_card__rarity', - 'offered_card__rarity', - 'requested_card__cardset', - 'offered_card__cardset', - ) - ), - 'trade_offer_have_cards__card__decks', - 'trade_offer_want_cards__card__decks', - 'trade_offer_have_cards__card__rarity', - 'trade_offer_want_cards__card__rarity', - 'trade_offer_have_cards__card__cardset', - 'trade_offer_want_cards__card__cardset' - ) - .order_by("-updated_at") - ) + queryset = TradeOffer.objects if show_closed: queryset = queryset.filter(is_closed=True) else: @@ -116,31 +92,7 @@ class TradeOfferAllListView(ListView): page = self.request.GET.get("page") show_closed = self.request.GET.get("show_closed", "false").lower() == "true" - queryset = ( - TradeOffer.objects.select_related('initiated_by') - .prefetch_related( - 'trade_offer_have_cards__card', - 'trade_offer_want_cards__card', - Prefetch( - 'acceptances', - queryset=TradeAcceptance.objects.select_related('accepted_by', 'requested_card', 'offered_card').prefetch_related( - 'requested_card__decks', - 'offered_card__decks', - 'requested_card__rarity', - 'offered_card__rarity', - 'requested_card__cardset', - 'offered_card__cardset', - ) - ), - 'trade_offer_have_cards__card__decks', - 'trade_offer_want_cards__card__decks', - 'trade_offer_have_cards__card__rarity', - 'trade_offer_want_cards__card__rarity', - 'trade_offer_have_cards__card__cardset', - 'trade_offer_want_cards__card__cardset' - ) - .order_by("-updated_at") - ) + queryset = TradeOffer.objects.all() if show_closed: queryset = queryset.filter(is_closed=True) else: @@ -162,20 +114,6 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView): raise PermissionDenied("No friend codes available for your account.") return super().dispatch(request, *args, **kwargs) - def get_queryset(self): - return ( - TradeOffer.objects.select_related('initiated_by') - .prefetch_related( - 'trade_offer_have_cards__card', - 'trade_offer_want_cards__card', - Prefetch( - 'acceptances', - queryset=TradeAcceptance.objects.select_related('accepted_by', 'requested_card', 'offered_card') - ) - ) - .order_by("-updated_at") - ) - def get_selected_friend_code(self): friend_codes = self.request.user.friend_codes.all() friend_code_param = self.request.GET.get("friend_code") @@ -448,21 +386,6 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView): model = TradeOffer template_name = "trades/trade_offer_detail.html" - def get_queryset(self): - return ( - TradeOffer.objects.select_related('initiated_by') - .prefetch_related( - 'trade_offer_have_cards__card', - 'trade_offer_want_cards__card', - Prefetch( - 'acceptances', - queryset=TradeAcceptance.objects.select_related( - 'accepted_by', 'requested_card', 'offered_card' - ) - ) - ) - ) - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) trade_offer = self.get_object() @@ -492,10 +415,15 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView): else: context["is_initiator"] = False + # Determine the user's default friend code (or fallback as needed). + default_friend_code = self.request.user.default_friend_code or user_friend_codes.first() + # If the current user is not the initiator and the offer is open, allow a new acceptance. if trade_offer.initiated_by not in user_friend_codes and not trade_offer.is_closed: context["acceptance_form"] = TradeAcceptanceCreateForm( - trade_offer=trade_offer, friend_codes=user_friend_codes + trade_offer=trade_offer, + friend_codes=user_friend_codes, + default_friend_code=default_friend_code ) return context @@ -538,11 +466,38 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView): kwargs['friend_codes'] = self.request.user.friend_codes.all() return kwargs + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["trade_offer"] = self.trade_offer + return context + def form_valid(self, form): form.instance.trade_offer = self.trade_offer self.object = form.save() return HttpResponseRedirect(self.get_success_url()) + def form_invalid(self, form): + """ + If the form submission includes a 'next' URL (sent as a hidden field from the detail page), + render the trade offer detail template for a better UX. Otherwise, fall back to the default + CreateView behavior. + """ + next_url = self.request.POST.get("next") + if next_url: + friend_codes = self.request.user.friend_codes.all() + is_initiator = self.trade_offer.initiated_by in friend_codes + context = { + "object": self.trade_offer, + "trade_offer": self.trade_offer, + "acceptance_form": form, + "friend_codes": friend_codes, + "is_initiator": is_initiator, + "delete_close_url": reverse_lazy("trade_offer_delete", kwargs={"pk": self.trade_offer.pk}) if is_initiator else None, + } + # Render the detail page with the form errors + return render(self.request, "trades/trade_offer_detail.html", context) + return super().form_invalid(form) + def get_success_url(self): return reverse_lazy("trade_offer_detail", kwargs={"pk": self.trade_offer.pk})