Fix create trade offer flow and other related bugs

This commit is contained in:
badblocks 2025-03-26 11:38:02 -07:00
parent f3a1366269
commit 65ca344582
40 changed files with 867 additions and 278 deletions

View file

@ -12,7 +12,7 @@ from django.utils.decorators import method_decorator
from django.views.decorators.http import require_http_methods
from django.core.paginator import Paginator
from django.contrib import messages
from django.views.decorators.cache import cache_page
from meta.views import Meta
from .models import TradeOffer, TradeAcceptance
from .forms import (TradeOfferAcceptForm,
@ -27,6 +27,7 @@ from playwright.sync_api import sync_playwright
from django.conf import settings
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
http_method_names = ['get'] # restricts this view to GET only
model = TradeOffer
form_class = TradeOfferCreateForm
template_name = "trades/trade_offer_create.html"
@ -63,13 +64,6 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
context["selected_friend_code"] = selected_friend_code
return context
def form_valid(self, form):
friend_codes = self.request.user.friend_codes.all()
if form.cleaned_data.get("initiated_by") not in friend_codes:
raise PermissionDenied("You cannot initiate trade offers for friend codes that do not belong to you.")
self.object = form.save()
return HttpResponseRedirect(self.get_success_url())
class TradeOfferAllListView(ListView):
model = TradeOffer
template_name = "trades/trade_offer_all_list.html"
@ -326,17 +320,16 @@ class TradeOfferSearchView(ListView):
#@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.
if self.request.method == "GET":
return TradeOffer.objects.none()
# Parse the POST data for offered and wanted selections.
offered_selections = self.parse_selections(self.request.POST.getlist("offered_cards"))
wanted_selections = self.parse_selections(self.request.POST.getlist("wanted_cards"))
have_selections = self.parse_selections(self.request.POST.getlist("have_cards"))
want_selections = self.parse_selections(self.request.POST.getlist("want_cards"))
# If no selections are provided, return an empty queryset.
if not offered_selections and not wanted_selections:
if not have_selections and not want_selections:
return TradeOffer.objects.none()
qs = TradeOffer.objects.filter(
@ -344,16 +337,16 @@ class TradeOfferSearchView(ListView):
).exclude(initiated_by__in=self.request.user.friend_codes.all())
# Chain filters for offered selections (i.e. the user "has" cards).
if offered_selections:
for card_id, qty in offered_selections:
if have_selections:
for card_id, qty in have_selections:
qs = qs.filter(
trade_offer_want_cards__card_id=card_id,
trade_offer_want_cards__quantity__gte=qty,
)
# Chain filters for wanted selections (i.e. the user "wants" cards).
if wanted_selections:
for card_id, qty in wanted_selections:
if want_selections:
for card_id, qty in want_selections:
qs = qs.filter(
trade_offer_have_cards__card_id=card_id,
trade_offer_have_cards__quantity__gte=qty,
@ -373,11 +366,11 @@ class TradeOfferSearchView(ListView):
# Populate available_cards to re-populate the multiselects.
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")
context["have_cards"] = self.request.POST.getlist("have_cards")
context["want_cards"] = self.request.POST.getlist("want_cards")
else:
context["offered_cards"] = []
context["wanted_cards"] = []
context["have_cards"] = []
context["want_cards"] = []
return context
#@silk_profile(name="Trade Offer Search- Render to Response")
@ -405,6 +398,8 @@ class TradeOfferDetailView(DetailView):
context = super().get_context_data(**kwargs)
trade_offer = self.get_object()
screenshot_mode = self.request.GET.get("screenshot_mode")
if screenshot_mode:
context["show_friend_code"] = trade_offer.initiated_by.user.show_friend_code_on_link_previews
context["screenshot_mode"] = screenshot_mode
# Calculate the number of cards in each category.
@ -418,14 +413,14 @@ class TradeOfferDetailView(DetailView):
# Calculate a base height using our previous assumptions:
# - 80px per card row (with rows computed as round(num_cards/2))
# - plus 138px for header/footer.
base_height = (round(num_cards / 2) * 80) + 138
base_height = (round(num_cards / 2) * 56) + 138
# Calculate a base width by assuming two columns of card badges.
# Here we assume each card badge is 80px wide plus the same horizontal offset of 138px.
if (num_wants + num_has) >= 3:
base_width = ((num_wants + num_has) * 144) + 96
else:
if (num_wants + num_has) >= 4:
base_width = (4 * 144) + 96
else:
base_width = (2 * 144) + 128
if base_height > base_width:
# The trade-offer card is taller than wide;
@ -610,6 +605,7 @@ class TradeOfferPNGView(View):
This view loads the trade offer detail page, waits for the trade offer element to render,
simulates a click to expand extra badges, and then screenshots only the trade offer element.
"""
def get(self, request, *args, **kwargs):
# For demonstration purposes, get the first trade offer.
# In production, you might want to identify the offer via a URL parameter.
@ -682,3 +678,174 @@ class TradeOfferPNGView(View):
browser.close()
return HttpResponse(png_bytes, content_type="image/png")
class TradeOfferCreateConfirmView(LoginRequiredMixin, View):
"""
Processes a two-step create for TradeOffer; on confirmation,
commits the offer and shows form errors if any occur.
"""
def post(self, request, *args, **kwargs):
if "confirm" in request.POST:
return self._commit_offer(request)
elif "edit" in request.POST:
return self._redirect_to_edit(request)
elif "preview" in request.POST:
return self._preview_offer(request)
else:
return self._preview_offer(request)
def _commit_offer(self, request):
"""
Commits the offer after confirmation. Any model ValidationError (for example,
due to mismatched card rarities) is caught and added to the form errors so that
it shows up in trade_offer_create.html.
"""
# Instantiate the form with POST data.
form = TradeOfferCreateForm(request.POST)
# Ensure that the 'initiated_by' queryset is limited to the user's friend codes.
form.fields["initiated_by"].queryset = request.user.friend_codes.all()
if form.is_valid():
try:
trade_offer = form.save()
except ValidationError as error:
form.add_error(None, error)
# Update the form's initial data so the template can safely reference have_cards/want_cards.
form.initial = {
"have_cards": request.POST.getlist("have_cards"),
"want_cards": request.POST.getlist("want_cards"),
"initiated_by": request.POST.get("initiated_by"),
}
# Supply additional context required by trade_offer_create.html.
from cards.models import Card
context = {
"form": form,
"friend_codes": request.user.friend_codes.all(),
"selected_friend_code": (
request.user.default_friend_code or request.user.friend_codes.first()
),
"cards": Card.objects.all().order_by("name", "rarity_level"),
}
return render(request, "trades/trade_offer_create.html", context)
messages.success(request, "Trade offer created successfully!")
return HttpResponseRedirect(reverse_lazy("trade_offer_list"))
else:
# When the form is not valid, update its initial data as well:
form.initial = {
"have_cards": request.POST.getlist("have_cards"),
"want_cards": request.POST.getlist("want_cards"),
"initiated_by": request.POST.get("initiated_by"),
}
from cards.models import Card
context = {
"form": form,
"friend_codes": request.user.friend_codes.all(),
"selected_friend_code": (
request.user.default_friend_code or request.user.friend_codes.first()
),
"cards": Card.objects.all().order_by("name", "rarity_level"),
}
return render(request, "trades/trade_offer_create.html", context)
def _redirect_to_edit(self, request):
query_params = request.POST.copy()
query_params.pop("csrfmiddlewaretoken", None)
query_params.pop("edit", None)
query_params.pop("confirm", None)
query_params.pop("preview", None)
from django.urls import reverse
base_url = reverse("trade_offer_create")
url_with_params = f"{base_url}?{query_params.urlencode()}"
return HttpResponseRedirect(url_with_params)
def _preview_offer(self, request):
"""
Processes the preview action (existing logic remains unchanged).
"""
form = TradeOfferCreateForm(request.POST)
form.fields["initiated_by"].queryset = request.user.friend_codes.all()
if not form.is_valid():
# Re-render the creation template with errors.
return render(request, "trades/trade_offer_create.html", {"form": form})
# Parse the card selections for "have" and "want" cards.
have_selections = self._parse_card_selections("have_cards")
want_selections = self._parse_card_selections("want_cards")
from cards.models import Card
have_cards_ids = [card_id for card_id, _ in have_selections]
cards_have_qs = Card.objects.filter(pk__in=have_cards_ids)
cards_have_dict = {card.pk: card for card in cards_have_qs}
# Define a dummy wrapper for a trade offer card entry.
class DummyOfferCard:
def __init__(self, card, quantity):
self.card = card
self.quantity = quantity
self.qty_accepted = 0
have_offer_cards = []
for card_id, quantity in have_selections:
card = cards_have_dict.get(card_id)
if card:
have_offer_cards.append(DummyOfferCard(card, quantity))
want_cards_ids = [card_id for card_id, _ in want_selections]
cards_want_qs = Card.objects.filter(pk__in=want_cards_ids)
cards_want_dict = {card.pk: card for card in cards_want_qs}
want_offer_cards = []
for card_id, quantity in want_selections:
card = cards_want_dict.get(card_id)
if card:
want_offer_cards.append(DummyOfferCard(card, quantity))
# Mimic a related manager's all() method.
class DummyManager:
def __init__(self, items):
self.items = items
def all(self):
return self.items
# Create a dummy TradeOffer object with properties required by the render_trade_offer tag.
class DummyTradeOffer:
pass
dummy_trade_offer = DummyTradeOffer()
dummy_trade_offer.pk = 0 # a placeholder primary key
dummy_trade_offer.hash = "preview"
dummy_trade_offer.rarity_icon = ""
dummy_trade_offer.trade_offer_have_cards = DummyManager(have_offer_cards)
dummy_trade_offer.trade_offer_want_cards = DummyManager(want_offer_cards)
dummy_trade_offer.acceptances = DummyManager([]) # no acceptances in preview
dummy_trade_offer.initiated_by = form.cleaned_data["initiated_by"]
# Pass along the POST data so that hidden inputs can be re-generated.
context = {
"dummy_trade_offer": dummy_trade_offer,
"post_data": request.POST,
}
return render(request, "trades/trade_offer_confirm_create.html", context)
def _parse_card_selections(self, key):
"""
Parses card selections from POST data for a given key (e.g., 'have_cards' or 'want_cards').
Selections are expected in the format 'card_id:quantity', defaulting quantity to 1 if missing.
Returns a list of (card_id, quantity) tuples.
"""
selections = self.request.POST.getlist(key)
results = []
for selection in selections:
parts = selection.split(":")
try:
card_id = int(parts[0])
except (ValueError, IndexError):
continue
quantity = 1
if len(parts) > 1:
try:
quantity = int(parts[1])
except ValueError:
pass
results.append((card_id, quantity))
return results