734 lines
33 KiB
Python
734 lines
33 KiB
Python
from django.views.generic import DeleteView, CreateView, ListView, DetailView, UpdateView
|
|
from django.views import View
|
|
from django.urls import reverse_lazy
|
|
from django.http import HttpResponseRedirect
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.shortcuts import render
|
|
from django.core.exceptions import PermissionDenied, ValidationError
|
|
from django.core.paginator import Paginator
|
|
from django.contrib import messages
|
|
from meta.views import Meta
|
|
from .models import TradeOffer, TradeAcceptance
|
|
from .forms import (TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
|
|
from django.template.loader import render_to_string
|
|
from trades.templatetags.trade_offer_tags import render_trade_offer_png
|
|
from playwright.sync_api import sync_playwright
|
|
from django.conf import settings
|
|
from .mixins import FriendCodeRequiredMixin
|
|
from common.mixins import ReusablePaginationMixin
|
|
|
|
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(ReusablePaginationMixin, ListView):
|
|
model = TradeOffer
|
|
template_name = "trades/trade_offer_all_list.html"
|
|
|
|
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
|
|
|
|
queryset = TradeOffer.objects.all()
|
|
if show_closed:
|
|
queryset = queryset.filter(is_closed=True)
|
|
else:
|
|
queryset = queryset.filter(is_closed=False)
|
|
|
|
page_number = self.get_page_number()
|
|
self.per_page = 10
|
|
paginated_offers, pagination_context = self.paginate_data(queryset, page_number)
|
|
context["offers"] = paginated_offers
|
|
context["page_obj"] = pagination_context
|
|
# Add the expanded flag to the context based on the URL query parameter.
|
|
context["expanded"] = request.GET.get("expanded", "false").lower() == "true"
|
|
return context
|
|
|
|
def render_to_response(self, context, **response_kwargs):
|
|
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
|
show_closed = self.request.GET.get("show_closed", "false").lower() == "true"
|
|
expanded = self.request.GET.get("expanded", "false").lower() == "true"
|
|
queryset = TradeOffer.objects.all()
|
|
if show_closed:
|
|
queryset = queryset.filter(is_closed=True)
|
|
else:
|
|
queryset = queryset.filter(is_closed=False)
|
|
|
|
page_number = self.get_page_number()
|
|
self.per_page = 10
|
|
paginated_offers, pagination_context = self.paginate_data(queryset, page_number)
|
|
return render(
|
|
self.request,
|
|
"trades/_trade_offer_list.html",
|
|
{"offers": paginated_offers, "page_obj": pagination_context, "expanded": expanded}
|
|
)
|
|
return super().render_to_response(context, **response_kwargs)
|
|
|
|
class TradeOfferDeleteView(LoginRequiredMixin, FriendCodeRequiredMixin, DeleteView):
|
|
model = TradeOffer
|
|
success_url = reverse_lazy("trade_offer_list")
|
|
template_name = "trades/trade_offer_delete.html"
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.object = super().get_object()
|
|
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,
|
|
)
|
|
|
|
if self.request.user.is_authenticated:
|
|
qs = qs.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, FriendCodeRequiredMixin, 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()
|
|
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()
|
|
if (self.trade_offer.initiated_by_id in
|
|
self.request.user.friend_codes.values_list("id", flat=True) or
|
|
self.trade_offer.is_closed):
|
|
raise PermissionDenied("You cannot accept this trade offer.")
|
|
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
|
|
# Set the actioning user before saving
|
|
form.instance._actioning_user = self.request.user
|
|
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_acceptance_update", kwargs={"pk": self.object.pk})
|
|
|
|
class TradeAcceptanceUpdateView(LoginRequiredMixin, FriendCodeRequiredMixin, 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()
|
|
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_acceptance_update", kwargs={"pk": self.object.pk})
|
|
|
|
class TradeOfferPNGView(View):
|
|
"""
|
|
Generate a PNG screenshot of the rendered trade offer detail page using Playwright.
|
|
This view uses PostgreSQL advisory locks to ensure that only one generation process
|
|
runs at a time for a given TradeOffer. The generated PNG is then cached in the
|
|
TradeOffer model's `image` field (assumed to be an ImageField).
|
|
"""
|
|
def get_lock_key(self, trade_offer_id):
|
|
# Use the trade_offer_id as the lock key; adjust if needed.
|
|
return trade_offer_id
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
from django.shortcuts import get_object_or_404
|
|
from django.http import HttpResponse
|
|
from django.core.files.base import ContentFile
|
|
trade_offer = get_object_or_404(TradeOffer, pk=kwargs['pk'])
|
|
|
|
# If the image is already generated and stored, serve it directly.
|
|
if trade_offer.image and not request.GET.get("debug"):
|
|
trade_offer.image.open()
|
|
return HttpResponse(trade_offer.image.read(), content_type="image/png")
|
|
|
|
# Acquire PostgreSQL advisory lock to prevent concurrent generation.
|
|
from django.db import connection
|
|
lock_key = self.get_lock_key(trade_offer.pk)
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("SELECT pg_advisory_lock(%s)", [lock_key])
|
|
try:
|
|
# Double-check if the image was generated while waiting for the lock.
|
|
trade_offer.refresh_from_db()
|
|
if trade_offer.image and not request.GET.get("debug"):
|
|
trade_offer.image.open()
|
|
return HttpResponse(trade_offer.image.read(), content_type="image/png")
|
|
|
|
tag_context = render_trade_offer_png(
|
|
{'request': request}, trade_offer, show_friend_code=True
|
|
)
|
|
image_width = tag_context.get('image_width')
|
|
image_height = tag_context.get('image_height')
|
|
if not image_width or not image_height:
|
|
raise ValueError("Could not determine image dimensions from tag_context")
|
|
html = render_to_string("templatetags/trade_offer_png.html", tag_context)
|
|
|
|
# if query string has "debug=true", render the HTML instead of the PNG
|
|
if request.GET.get("debug") == "html":
|
|
return render(request, "templatetags/trade_offer_png.html", tag_context)
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(
|
|
headless=True,
|
|
args=[
|
|
"--no-sandbox",
|
|
"--disable-setuid-sandbox",
|
|
"--disable-dev-shm-usage",
|
|
"--disable-accelerated-2d-canvas",
|
|
"--disable-gpu",
|
|
"--no-zygote",
|
|
"--disable-audio-output",
|
|
"--disable-webgl",
|
|
"--no-first-run",
|
|
]
|
|
)
|
|
context_browser = browser.new_context(viewport={"width": image_width, "height": image_height})
|
|
page = context_browser.new_page()
|
|
page.on("console", lambda msg: print(f"Console {msg.type}: {msg.text}"))
|
|
page.on("pageerror", lambda err: print(f"Page error: {err}"))
|
|
page.on("requestfailed", lambda req: print(f"Failed to load: {req.url} - {req.failure.error_text}"))
|
|
page.set_content(html, wait_until="domcontentloaded")
|
|
element = page.wait_for_selector(".trade-offer-card-screenshot")
|
|
screenshot_bytes = element.screenshot(type="png", omit_background=True)
|
|
browser.close()
|
|
|
|
# Save the generated PNG to the TradeOffer model (requires an ImageField named `image`).
|
|
filename = f"trade_offer_{trade_offer.pk}.png"
|
|
trade_offer.image.save(filename, ContentFile(screenshot_bytes))
|
|
trade_offer.save(update_fields=["image"])
|
|
return HttpResponse(screenshot_bytes, content_type="image/png")
|
|
finally:
|
|
# Release the advisory lock.
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("SELECT pg_advisory_unlock(%s)", [lock_key])
|
|
|
|
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
|