Fix gravatar hovercards, and add trade_offer image generation with playwright, for use with opengraph tags on trade_offer_detal.html

This commit is contained in:
badblocks 2025-03-20 23:59:22 -07:00
parent 4c0db9f842
commit f3a1366269
16 changed files with 372 additions and 123 deletions

View file

@ -1,4 +1,5 @@
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
@ -12,11 +13,18 @@ from django.views.decorators.http import require_http_methods
from django.core.paginator import Paginator
from django.contrib import messages
from meta.views import Meta
from .models import TradeOffer, TradeAcceptance
from .forms import (TradeOfferAcceptForm,
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
from cards.models import Card
#from silk.profiling.profiler import silk_profile
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):
model = TradeOffer
@ -383,7 +391,7 @@ class TradeOfferSearchView(ListView):
else:
return super().render_to_response(context, **response_kwargs)
class TradeOfferDetailView(LoginRequiredMixin, DetailView):
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,
@ -396,6 +404,57 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
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")
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) * 80) + 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:
base_width = (4 * 144) + 96
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 = [
@ -412,26 +471,31 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
# Option 1: Filter active acceptances using the queryset lookup.
context["active_acceptances"] = trade_offer.acceptances.exclude(state__in=terminal_states)
user_friend_codes = self.request.user.friend_codes.all()
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})
# 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
# 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
)
return context
class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
@ -539,3 +603,82 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, UpdateView):
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")