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:
parent
4c0db9f842
commit
f3a1366269
16 changed files with 372 additions and 123 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-17 20:39
|
||||
# Generated by Django 5.1.2 on 2025-03-20 00:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from django import template
|
|||
register = template.Library()
|
||||
|
||||
@register.inclusion_tag('templatetags/trade_offer.html', takes_context=True)
|
||||
def render_trade_offer(context, offer):
|
||||
def render_trade_offer(context, offer, screenshot_mode=False):
|
||||
"""
|
||||
Renders a trade offer including detailed trade acceptance information.
|
||||
Freezes the through-model querysets to avoid extra DB hits.
|
||||
|
|
@ -33,6 +33,10 @@ def render_trade_offer(context, offer):
|
|||
'acceptances': acceptances,
|
||||
'have_cards_available': have_cards_available,
|
||||
'want_cards_available': want_cards_available,
|
||||
'screenshot_mode': screenshot_mode,
|
||||
'in_game_name': offer.initiated_by.in_game_name,
|
||||
'friend_code': offer.initiated_by.friend_code,
|
||||
'num_cards_available': len(have_cards_available) + len(want_cards_available),
|
||||
}
|
||||
|
||||
@register.inclusion_tag('templatetags/trade_acceptance.html', takes_context=True)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from .views import (
|
|||
TradeAcceptanceUpdateView,
|
||||
TradeOfferDeleteView,
|
||||
TradeOfferSearchView,
|
||||
TradeOfferPNGView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -17,6 +18,7 @@ urlpatterns = [
|
|||
path("my/", TradeOfferMyListView.as_view(), name="trade_offer_my_list"),
|
||||
path("search/", TradeOfferSearchView.as_view(), name="trade_offer_search"),
|
||||
path("<int:pk>/", TradeOfferDetailView.as_view(), name="trade_offer_detail"),
|
||||
path("<int:pk>.png", TradeOfferPNGView.as_view(), name="trade_offer_png"),
|
||||
path("delete/<int:pk>/", TradeOfferDeleteView.as_view(), name="trade_offer_delete"),
|
||||
path("offer/<int:offer_pk>", TradeAcceptanceCreateView.as_view(), name="trade_acceptance_create"),
|
||||
path("accept/<int:pk>/", TradeAcceptanceUpdateView.as_view(), name="trade_acceptance_update"),
|
||||
|
|
|
|||
179
trades/views.py
179
trades/views.py
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue