pkmntrade.club/trades/views.py

857 lines
38 KiB
Python

from django.views.generic import TemplateView, DeleteView, CreateView, ListView, DetailView, UpdateView, FormView
from django.views import View
from django.urls import reverse_lazy
from django.http import HttpResponseRedirect, JsonResponse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404, render
from django.core.exceptions import PermissionDenied, ValidationError
from django.views.generic.edit import FormMixin
from django.utils import timezone
from django.db.models import Q, Case, When, Value, BooleanField, Prefetch, F
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,
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
from cards.models import Card
import imgkit
from django.http import HttpResponse, Http404
from django.template.loader import render_to_string
from trades.templatetags.trade_offer_tags import render_trade_offer
from django.template import RequestContext
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"
success_url = reverse_lazy("trade_offer_list")
def get_form(self, form_class=None):
form = super().get_form(form_class)
# Restrict the 'initiated_by' choices to friend codes owned by the logged-in user.
form.fields["initiated_by"].queryset = self.request.user.friend_codes.all()
return form
def get_initial(self):
initial = super().get_initial()
initial["have_cards"] = self.request.GET.getlist("have_cards")
initial["want_cards"] = self.request.GET.getlist("want_cards")
if self.request.user.friend_codes.count() == 1:
initial["initiated_by"] = self.request.user.friend_codes.first().pk
return initial
def get_context_data(self, **kwargs):
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_level")
friend_codes = self.request.user.friend_codes.all()
if "initiated_by" in self.request.GET:
try:
selected_friend_code = friend_codes.get(pk=self.request.GET.get("initiated_by"))
except friend_codes.model.DoesNotExist:
selected_friend_code = self.request.user.default_friend_code or friend_codes.first()
else:
selected_friend_code = self.request.user.default_friend_code or friend_codes.first()
context["friend_codes"] = friend_codes
context["selected_friend_code"] = selected_friend_code
return context
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
show_closed = request.GET.get("show_closed", "false").lower() == "true"
context["show_closed"] = show_closed
# Build the queryset with our related objects.
queryset = TradeOffer.objects
if show_closed:
queryset = queryset.filter(is_closed=True)
else:
queryset = queryset.filter(is_closed=False)
# On initial load, use the 'offers_page' parameter.
offers_page = request.GET.get("offers_page")
offers_paginator = Paginator(queryset, 10)
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":
page = self.request.GET.get("page")
show_closed = self.request.GET.get("show_closed", "false").lower() == "true"
queryset = TradeOffer.objects
if show_closed:
queryset = queryset.filter(is_closed=True)
else:
queryset = queryset.filter(is_closed=False)
paginated_offers = Paginator(queryset, 10).get_page(page)
return render(
self.request,
"trades/_trade_offer_list_paginated.html",
{"offers": paginated_offers}
)
return super().render_to_response(context, **response_kwargs)
class TradeOfferMyListView(LoginRequiredMixin, ListView):
model = TradeOffer # Fallback model; our context data holds separate filtered querysets.
template_name = "trades/trade_offer_my_list.html"
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated and not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
return super().dispatch(request, *args, **kwargs)
def get_selected_friend_code(self):
friend_codes = self.request.user.friend_codes.all()
friend_code_param = self.request.GET.get("friend_code")
if friend_code_param:
try:
selected_friend_code = friend_codes.get(pk=friend_code_param)
except friend_codes.model.DoesNotExist:
selected_friend_code = self.request.user.default_friend_code or friend_codes.first()
else:
selected_friend_code = self.request.user.default_friend_code or friend_codes.first()
if not selected_friend_code:
raise PermissionDenied("You do not have an active friend code associated with your account.")
return selected_friend_code
def get_show_closed(self):
return self.request.GET.get("show_closed", "false").lower() == "true"
def get_my_trade_offers_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
queryset = self.get_queryset().filter(initiated_by=selected_friend_code)
if self.get_show_closed():
queryset = queryset.filter(is_closed=True)
else:
queryset = queryset.filter(is_closed=False)
return Paginator(queryset, 10).get_page(page_param)
def get_involved_acceptances(self, selected_friend_code):
terminal_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
involved_acceptances_qs = TradeAcceptance.objects.filter(
Q(trade_offer__initiated_by=selected_friend_code) | Q(accepted_by=selected_friend_code)
).order_by("-updated_at")
if self.get_show_closed():
return involved_acceptances_qs.filter(state__in=terminal_states)
return involved_acceptances_qs.exclude(state__in=terminal_states)
def get_trade_acceptances_waiting_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
involved_acceptances = self.get_involved_acceptances(selected_friend_code)
waiting_acceptances = involved_acceptances.filter(
Q(trade_offer__initiated_by=selected_friend_code, state__in=[
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.RECEIVED,
]) |
Q(accepted_by=selected_friend_code, state__in=[TradeAcceptance.AcceptanceState.SENT])
)
return Paginator(waiting_acceptances, 10).get_page(page_param)
def get_other_party_trade_acceptances_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
involved_acceptances = self.get_involved_acceptances(selected_friend_code)
waiting_acceptances = involved_acceptances.filter(
Q(trade_offer__initiated_by=selected_friend_code, state__in=[
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.RECEIVED,
]) |
Q(accepted_by=selected_friend_code, state__in=[TradeAcceptance.AcceptanceState.SENT])
)
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
show_closed = self.get_show_closed()
context["show_closed"] = show_closed
selected_friend_code = self.get_selected_friend_code()
context["selected_friend_code"] = selected_friend_code
context["friend_codes"] = request.user.friend_codes.all()
# Use request params for initial full page load (could be None)
offers_page = request.GET.get("offers_page")
waiting_page = request.GET.get("waiting_page")
other_page = request.GET.get("other_page")
context["my_trade_offers_paginated"] = self.get_my_trade_offers_paginated(offers_page)
context["trade_acceptances_waiting_paginated"] = self.get_trade_acceptances_waiting_paginated(waiting_page)
context["other_party_trade_acceptances_paginated"] = self.get_other_party_trade_acceptances_paginated(other_page)
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":
ajax_section = self.request.GET.get("ajax_section")
page = self.request.GET.get("page")
if ajax_section == "my_trade_offers":
offers = self.get_my_trade_offers_paginated(page)
elif ajax_section == "waiting_acceptances":
offers = self.get_trade_acceptances_waiting_paginated(page)
elif ajax_section == "other_party_acceptances":
offers = self.get_other_party_trade_acceptances_paginated(page)
else:
# Fallback to my trade offers.
offers = self.get_my_trade_offers_paginated(page)
return render(self.request, "trades/_trade_offer_list_paginated.html", {"offers": offers})
return super().render_to_response(context, **response_kwargs)
class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
model = TradeOffer
success_url = reverse_lazy("trade_offer_list")
template_name = "trades/trade_offer_delete.html"
def dispatch(self, request, *args, **kwargs):
# Retrieve the object normally
self.object = super().get_object()
# Perform the permission check here
if self.object.initiated_by_id not in request.user.friend_codes.values_list("id", flat=True):
raise PermissionDenied("You are not authorized to delete or close this trade offer.")
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
trade_offer = self.object
terminal_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
active_acceptances = trade_offer.acceptances.exclude(state__in=terminal_states)
if trade_offer.acceptances.count() == 0:
context["action"] = "delete"
elif trade_offer.acceptances.count() > 0 and not active_acceptances.exists():
context["action"] = "close"
else:
context["action"] = None
return context
def post(self, request, *args, **kwargs):
trade_offer = self.object
terminal_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
active_acceptances = trade_offer.acceptances.exclude(state__in=terminal_states)
if active_acceptances.exists():
messages.error(
request,
"Cannot close this trade offer while there are active acceptances. Please reject all acceptances before closing, or finish the trades."
)
context = self.get_context_data()
return self.render_to_response(context)
else:
if trade_offer.acceptances.count() > 0:
trade_offer.is_closed = True
trade_offer.save(update_fields=["is_closed"])
messages.success(request, "Trade offer has been marked as closed.")
return HttpResponseRedirect(self.get_success_url())
else:
messages.success(request, "Trade offer has been deleted.")
return super().delete(request, *args, **kwargs)
class TradeOfferSearchView(ListView):
"""
Reworked trade offer search view using POST.
This view allows users to search active trade offers based on the cards they have and/or want.
The POST parameters (offered_cards and wanted_cards) are expected to be in the format 'card_id:quantity'.
If both types of selections are provided, the resultant queryset must satisfy both conditions.
Offers initiated by any of the user's friend codes are excluded.
When the request is AJAX (via X-Requested-With header), only the search results fragment
(_search_results.html) is rendered. On GET (initial page load), the search results queryset
is empty.
"""
model = TradeOffer
context_object_name = "search_results"
template_name = "trades/trade_offer_search.html"
paginate_by = 10
http_method_names = ["get", "post"]
def parse_selections(self, selection_list):
"""
Parse a list of selections (each formatted as 'card_id:quantity') into a list of tuples.
Defaults the quantity to 1 if missing.
"""
results = []
for item in selection_list:
parts = item.split(":")
try:
card_id = int(parts[0])
except ValueError:
continue # Skip invalid values.
qty = 1
if len(parts) > 1:
try:
qty = int(parts[1])
except ValueError:
qty = 1
results.append((card_id, qty))
return results
#@silk_profile(name="Trade Offer Search- Get Queryset")
def get_queryset(self):
# 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.
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 have_selections and not want_selections:
return TradeOffer.objects.none()
qs = TradeOffer.objects.filter(
is_closed=False,
).exclude(initiated_by__in=self.request.user.friend_codes.all())
# Chain filters for offered selections (i.e. the user "has" cards).
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 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,
)
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
# Populate available_cards to re-populate the multiselects.
context["cards"] = Card.objects.all().order_by("name")
if self.request.method == "POST":
context["have_cards"] = self.request.POST.getlist("have_cards")
context["want_cards"] = self.request.POST.getlist("want_cards")
else:
context["have_cards"] = []
context["want_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.
"""
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
from django.shortcuts import render
return render(self.request, "trades/_search_results.html", context)
else:
return super().render_to_response(context, **response_kwargs)
class TradeOfferDetailView(DetailView):
"""
Displays the details of a TradeOffer along with its active acceptances.
If the offer is still open and the current user is not its initiator,
an acceptance form is provided to create a new acceptance.
"""
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()
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.
num_has = trade_offer.trade_offer_have_cards.count()
num_wants = trade_offer.trade_offer_want_cards.count()
num_cards = max(num_has, num_wants)
# Define the aspect ratio.
aspect_ratio = 1.91
# 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) * 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) >= 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;
# compute the width from the height.
image_height = base_height
image_width = int(round(image_height * aspect_ratio)) + 1
else:
# The trade-offer card is wider than tall;
# compute the height from the width.
image_width = base_width
image_height = int(round(image_width / aspect_ratio))
# Build the meta tags with the computed dimensions.
title = f'Trade Offer from {trade_offer.initiated_by.in_game_name} ({trade_offer.initiated_by.friend_code})'
context["meta"] = Meta(
title=title,
description=f'Has: {", ".join([card.card.name for card in trade_offer.trade_offer_have_cards.all()])}\nWants: {", ".join([card.card.name for card in trade_offer.trade_offer_want_cards.all()])}',
image_object={
"url": f'http://localhost:8000{reverse_lazy("trade_offer_png", kwargs={"pk": trade_offer.pk})}',
"type": "image/png",
"width": image_width,
"height": image_height,
},
twitter_type="summary_large_image",
use_og=True,
use_twitter=True,
use_facebook=True,
use_schemaorg=True,
)
# Define terminal (closed) acceptance states based on our new system:
terminal_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
# For example, if you want to separate active from terminal acceptances:
context["acceptances"] = trade_offer.acceptances.all()
# Option 1: Filter active acceptances using the queryset lookup.
context["active_acceptances"] = trade_offer.acceptances.exclude(state__in=terminal_states)
if self.request.user.is_authenticated:
user_friend_codes = self.request.user.friend_codes.all()
# Add context flag and deletion URL if the current user is the initiator
if trade_offer.initiated_by in user_friend_codes:
context["is_initiator"] = True
context["delete_close_url"] = reverse_lazy("trade_offer_delete", kwargs={"pk": trade_offer.pk})
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,
default_friend_code=default_friend_code
)
else:
context["is_initiator"] = False
context["delete_close_url"] = None
context["acceptance_form"] = None
return context
class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
"""
View to create a new TradeAcceptance.
The URL should provide 'offer_pk' so that the proper TradeOffer can be identified.
"""
model = TradeAcceptance
form_class = TradeAcceptanceCreateForm
template_name = "trades/trade_acceptance_create.html"
def dispatch(self, request, *args, **kwargs):
self.trade_offer = self.get_trade_offer()
if self.trade_offer.initiated_by_id in request.user.friend_codes.values_list("id", flat=True) or self.trade_offer.is_closed:
raise PermissionDenied("You cannot accept this trade offer.")
if not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
return super().dispatch(request, *args, **kwargs)
def get_trade_offer(self):
return (
TradeOffer.objects.get(pk=self.kwargs['offer_pk'])
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['trade_offer'] = self.trade_offer
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})
class TradeAcceptanceUpdateView(LoginRequiredMixin, UpdateView):
"""
View to update the state of an existing TradeAcceptance.
The allowed state transitions are provided via the form.
"""
model = TradeAcceptance
form_class = TradeAcceptanceTransitionForm
template_name = "trades/trade_acceptance_update.html"
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
friend_codes = request.user.friend_codes.values_list("id", flat=True)
if self.object.accepted_by_id not in friend_codes and self.object.trade_offer.initiated_by_id not in friend_codes:
raise PermissionDenied("You are not authorized to update this acceptance.")
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
# Pass the current instance to the form so it can set proper allowed transitions.
kwargs["instance"] = self.object
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form):
new_state = form.cleaned_data["state"]
try:
# Try to cast new_state to the enum member
valid_state = TradeAcceptance.AcceptanceState(new_state)
except ValueError:
form.add_error("state", "Invalid state transition.")
return self.form_invalid(form)
try:
form.instance.update_state(valid_state, self.request.user)
except ValueError as e:
form.add_error("state", str(e))
return self.form_invalid(form)
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse_lazy("trade_offer_detail", kwargs={"pk": self.object.trade_offer.pk})
class TradeOfferPNGView(View):
"""
Generate a PNG screenshot of the rendered trade offer detail page using Playwright.
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.
trade_offer = TradeOffer.objects.get(pk=kwargs['pk'])
if not trade_offer:
raise Http404("Trade offer not found")
# Get the URL for the trade offer detail view.
detail_url = request.build_absolute_uri(
reverse_lazy("trade_offer_detail", kwargs={"pk": trade_offer.pk})+"?screenshot_mode=true"
)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1280, "height": 800},
)
# If the request contains a Django session cookie,
# add it to the browser context to bypass the login screen.
session_cookie = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
if session_cookie:
cookie = {
"name": settings.SESSION_COOKIE_NAME,
"value": session_cookie,
"domain": request.get_host().split(':')[0],
"path": "/",
"httpOnly": True,
"secure": not settings.DEBUG,
}
context.add_cookies([cookie])
# Open a new page and navigate to the detail view.
page = context.new_page()
page.goto(detail_url, wait_until="networkidle")
# Inject CSS to force transparency.
page.add_style_tag(content="""
html, body, .bg-base-200 {
background-color: rgba(255, 255, 255, 0) !important;
}
""")
trade_offer_selector = ".trade-offer-card-screenshot"
page.wait_for_selector(trade_offer_selector)
# Simulate a click on the toggle button within the trade offer element
# to force the extra details (e.g., extra badges) to expand.
# We use a selector that targets the first svg with a "cursor-pointer" class inside the trade offer card.
toggle_selector = f"{trade_offer_selector} svg.cursor-pointer"
try:
toggle = page.query_selector(toggle_selector)
if toggle:
toggle.click()
# Wait for the CSS animation to complete (600ms as in your template)
page.wait_for_timeout(600)
except Exception:
# If the toggle is not found or clicking fails, proceed without expansion.
pass
# Locate the element containing the trade offer and capture its screenshot.
element = page.query_selector(trade_offer_selector)
if not element:
browser.close()
raise Http404("Trade offer element not found on page")
png_bytes = element.screenshot(type="png", omit_background=True)
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