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
|
|
@ -23,7 +23,9 @@ COPY .env.production /code/.env
|
||||||
ENV HOME=/code
|
ENV HOME=/code
|
||||||
|
|
||||||
# Install NPM & node.js
|
# Install NPM & node.js
|
||||||
RUN apt-get update && apt-get install -y nodejs npm
|
RUN apt-get update && apt-get install -y nodejs npm xvfb libnss3 libnspr4 libasound2t64 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 libcairo-gobject2 libdbus-glib-1-2 libfontconfig1 libfreetype6 libgdk-pixbuf-2.0-0 libgtk-3-0 libharfbuzz0b libpangocairo-1.0-0 libx11-xcb1 libxcb-shm0 libxcursor1 libxi6 libxrender1 libxtst6 libsoup-3.0-0 gstreamer1.0-libav gstreamer1.0-plugins-base gstreamer1.0-plugins-good libegl1 libenchant-2-2 libepoxy0 libevdev2 libgles2 libglx0 libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgtk-4-1 libgudev-1.0-0 libharfbuzz-icu0 libhyphen0 libicu72 libjpeg62-turbo liblcms2-2 libmanette-0.2-0 libnotify4 libopengl0 libopenjp2-7 libopus0 libpng16-16 libproxy1v5 libsecret-1-0 libwayland-client0 libwayland-egl1 libwayland-server0 libwebp7 libwebpdemux2 libwoff1 libxml2 libxslt1.1 libatomic1 libevent-2.1-7 libavif16 xvfb fonts-noto-color-emoji fonts-unifont xfonts-scalable fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf
|
||||||
|
|
||||||
|
RUN playwright install-deps && playwright install
|
||||||
|
|
||||||
# Expose port 8000
|
# Expose port 8000
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
@ -32,5 +34,7 @@ EXPOSE 8000
|
||||||
|
|
||||||
RUN python manage.py collectstatic --noinput
|
RUN python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
#RUN python manage.py createcachetable django_cache
|
||||||
|
|
||||||
# Use gunicorn on port 8000
|
# Use gunicorn on port 8000
|
||||||
CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "django_project.wsgi"]
|
CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "django_project.wsgi"]
|
||||||
|
|
|
||||||
|
|
@ -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.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ def gravatar_url(email, size=20):
|
||||||
Returns the Gravatar URL for a given email. The URL includes parameters
|
Returns the Gravatar URL for a given email. The URL includes parameters
|
||||||
for the default image and the size.
|
for the default image and the size.
|
||||||
"""
|
"""
|
||||||
default = "wavatar"
|
default = "retro"
|
||||||
email_hash = gravatar_hash(email)
|
email_hash = gravatar_hash(email)
|
||||||
params = urlencode({'d': default, 's': str(size)})
|
params = urlencode({'d': default, 's': str(size)})
|
||||||
return f"https://www.gravatar.com/avatar/{email_hash}?{params}"
|
return f"https://www.gravatar.com/avatar/{email_hash}?{params}"
|
||||||
|
|
|
||||||
|
|
@ -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
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ INSTALLED_APPS = [
|
||||||
"cards",
|
"cards",
|
||||||
"home",
|
"home",
|
||||||
"trades.apps.TradesConfig",
|
"trades.apps.TradesConfig",
|
||||||
|
"meta",
|
||||||
#"silk",
|
#"silk",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -67,6 +68,11 @@ SILKY_META = True
|
||||||
|
|
||||||
TAILWIND_APP_NAME = 'theme'
|
TAILWIND_APP_NAME = 'theme'
|
||||||
|
|
||||||
|
META_SITE_NAME = 'PKMN Trade Club'
|
||||||
|
META_SITE_PROTOCOL = 'https'
|
||||||
|
META_USE_SITES = True
|
||||||
|
META_IMAGE_URL = 'https://pkmntrade.club/'
|
||||||
|
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
|
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
|
@ -299,6 +305,6 @@ else:
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
||||||
"LOCATION": "site-cache",
|
"LOCATION": "django_cache",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
services:
|
services:
|
||||||
# web:
|
# web:
|
||||||
# build: .
|
# build: .
|
||||||
# command: python /code/manage.py runserver 0.0.0.0:8000
|
# command: python /code/manage.py runserver 0.0.0.0:8000
|
||||||
# volumes:
|
# volumes:
|
||||||
# - .:/code:z
|
# - .:/code:z
|
||||||
# ports:
|
# ports:
|
||||||
# - 8000:8000
|
# - 8000:8000
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# - db
|
# - db
|
||||||
db:
|
db:
|
||||||
image: postgres:16
|
image: postgres:16
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,13 @@ django-daisy==1.0.13
|
||||||
django-debug-toolbar==4.4.6
|
django-debug-toolbar==4.4.6
|
||||||
django-el-pagination==4.1.2
|
django-el-pagination==4.1.2
|
||||||
django-environ==0.12.0
|
django-environ==0.12.0
|
||||||
|
django-meta==2.4.2
|
||||||
django-silk==5.3.1
|
django-silk==5.3.1
|
||||||
django-tailwind-4[reload]==0.1.4
|
django-tailwind-4[reload]==0.1.4
|
||||||
django-widget-tweaks==1.5.0
|
django-widget-tweaks==1.5.0
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
idna==3.4
|
idna==3.4
|
||||||
|
imgkit==1.2.3
|
||||||
oauthlib==3.2.2
|
oauthlib==3.2.2
|
||||||
packaging==23.1
|
packaging==23.1
|
||||||
psycopg==3.2.3
|
psycopg==3.2.3
|
||||||
|
|
@ -26,6 +28,7 @@ psycopg-binary==3.2.3
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
PyJWT==2.6.0
|
PyJWT==2.6.0
|
||||||
python3-openid==3.2.0
|
python3-openid==3.2.0
|
||||||
|
playwright==1.51.0
|
||||||
requests==2.28.2
|
requests==2.28.2
|
||||||
requests-oauthlib==1.3.1
|
requests-oauthlib==1.3.1
|
||||||
sqlparse==0.4.3
|
sqlparse==0.4.3
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,19 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mx-auto mb-4">{{ user.email|gravatar:100 }}</div>
|
<div class="mx-auto mb-4">{{ user.email|gravatar:100 }}</div>
|
||||||
<p class="text-center">All profile information is managed through Gravatar.</p>
|
<p class="text-center">All profile information is managed through Gravatar.</p>
|
||||||
<p class="text-center mt-4"><a href="https://gravatar.com/profile/" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">Edit Profile on Gravatar</a></p>
|
<p class="text-center mt-4">
|
||||||
|
<a href="https://gravatar.com/profile/" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
||||||
|
Edit Profile on Gravatar
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||||
|
</svg>
|
||||||
|
</a></p>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h2 class="text-lg font-semibold pt-0">What is Gravatar?</h2>
|
<h2 class="text-lg font-semibold pt-0">What is Gravatar?</h2>
|
||||||
<p class="mb-4">Gravatar (Globally Recognized Avatar) is a free service that links your email address to a profile picture and, optionally, a profile. Many websites, including this one, use Gravatar to display your avatar and profile automatically.</p>
|
<p class="mb-4">Gravatar (Globally Recognized Avatar) is a free service that links your email address to a profile picture and, optionally, a profile. Many websites, including this one, use Gravatar to display your avatar and profile automatically.</p>
|
||||||
|
|
||||||
<h2 class="text-lg font-semibold">How does it work?</h2>
|
<h2 class="text-lg font-semibold">How does it work?</h2>
|
||||||
<p class="mb-4">If you’ve set up a Gravatar, your profile picture will appear here whenever you use your email on supported sites. When someone hovers over or clicks on your avatar, your Gravatar profile will appear if you have one. If you don’t have a Gravatar yet, you’ll see a default image instead.</p>
|
<p class="mb-4">If you've set up a Gravatar, your profile picture will appear here whenever you use your email on supported sites. When someone hovers over or clicks on your avatar, your Gravatar profile will also appear if you have one. If you don't have a Gravatar yet, you'll see a default image instead.</p>
|
||||||
|
|
||||||
<h2 class="text-lg font-semibold">Want to update or add a Gravatar?</h2>
|
<h2 class="text-lg font-semibold">Want to update or add a Gravatar?</h2>
|
||||||
<p class="mb-4">Go to Gravatar.com to set up or change your avatar or profile. Your updates will appear here once saved!</p>
|
<p class="mb-4">Go to Gravatar.com to set up or change your avatar or profile. Your updates will appear here once saved!</p>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{% include 'meta/meta.html' %}
|
||||||
|
|
||||||
<!-- Inline script to set the theme before rendering -->
|
<!-- Inline script to set the theme before rendering -->
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -25,6 +26,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<title>[PᴋMɴ Trade Club] {% block title %}{% endblock title %}</title>
|
<title>[PᴋMɴ Trade Club] {% block title %}{% endblock title %}</title>
|
||||||
|
|
||||||
<link rel="shortcut icon" href="{% static 'images/favicon.ico' %}">
|
<link rel="shortcut icon" href="{% static 'images/favicon.ico' %}">
|
||||||
<!-- Choices.js -->
|
<!-- Choices.js -->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js@11.0.6/public/assets/styles/choices.min.css" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js@11.0.6/public/assets/styles/choices.min.css" />
|
||||||
|
|
@ -113,12 +115,7 @@
|
||||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-32 p-2 shadow">
|
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-32 p-2 shadow">
|
||||||
<li>
|
<li>
|
||||||
<a class="flex items-center justify-between" href="{% url 'profile' %}">
|
<a class="flex items-center justify-between" href="{% url 'profile' %}">
|
||||||
<div>Profile</div>
|
Profile
|
||||||
<div>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load trade_offer_tags %}
|
{% load trade_offer_tags %}
|
||||||
|
|
||||||
{% block title %}Trade Offer Detail{% endblock title %}
|
{% block title %}{{title}}{% endblock title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto max-w-2xl mt-6">
|
<div class="container mx-auto max-w-2xl mt-6">
|
||||||
<h2 class="text-2xl font-bold">Trade Offer Details</h2>
|
<h2 class="text-2xl font-bold">Trade Offer Details</h2>
|
||||||
<div class="flex justify-center mt-10">
|
<div class="flex justify-center mt-10">
|
||||||
{% render_trade_offer object %}
|
{% if screenshot_mode == "true" %}
|
||||||
|
{% render_trade_offer object True %}
|
||||||
|
{% else %}
|
||||||
|
{% render_trade_offer object %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if acceptance_form %}
|
{% if acceptance_form %}
|
||||||
<div class="w-3/4 mx-auto mt-4">
|
<div class="w-3/4 mx-auto mt-4">
|
||||||
|
|
@ -50,7 +54,7 @@
|
||||||
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary">Back to Trade Offers</a>
|
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary">Back to Trade Offers</a>
|
||||||
{% if is_initiator %}
|
{% if is_initiator %}
|
||||||
<a href="{{ delete_close_url }}" class="btn btn-danger">Delete/Close Trade Offer</a>
|
<a href="{{ delete_close_url }}" class="btn btn-danger">Delete/Close Trade Offer</a>
|
||||||
{% else %}
|
{% elif request.user.is_authenticated %}
|
||||||
<button type="submit" class="btn btn-primary">Submit Acceptance</button>
|
<button type="submit" class="btn btn-primary">Submit Acceptance</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<div class="relative inline-block">
|
<div class="card-badge relative inline-block">
|
||||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="{{ style }}">
|
<div class="card-badge-inner freeze-bg-color grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="{{ style }}">
|
||||||
<div class="row-span-1 col-span-4 truncate text-ellipsis self-start font-semibold leading-tight text-sm max-w-7/8">{{ name }}</div>
|
<div class="cardname row-span-1 col-span-4 truncate text-ellipsis self-start font-semibold leading-tight text-sm max-w-7/8">{{ name }}</div>
|
||||||
<div class="row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ rarity }}</div>
|
<div class="rarity row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ rarity }}</div>
|
||||||
<div class="row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ cardset }}</div>
|
<div class="cardset row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ cardset }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="card-quantity-badge absolute top-3.5 right-1 bg-gray-600 text-white text-xs font-semibold rounded-full px-2">
|
<span class="card-quantity-badge freeze-bg-color absolute top-3.5 right-1 bg-gray-600 text-white text-xs font-semibold rounded-full px-2">
|
||||||
{{ quantity }}
|
{{ quantity }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -16,8 +16,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<div class="trade-offer-card-screenshot p-4 h-full w-auto flex justify-center"
|
||||||
<div x-data="tradeOfferCard()" class="transition-all duration-500 trade-offer-card"
|
{% if screenshot_mode %}
|
||||||
|
x-data="{
|
||||||
|
setDimension() {
|
||||||
|
const aspectRatio = 1.91;
|
||||||
|
// If the element is taller than it is wide,
|
||||||
|
// adjust the width based on the element's height.
|
||||||
|
if ($el.offsetHeight > $el.offsetWidth) {
|
||||||
|
$el.style.width = ($el.offsetHeight * aspectRatio) + 'px';
|
||||||
|
} else {
|
||||||
|
// Otherwise, adjust the height based on the element's width.
|
||||||
|
$el.style.height = ($el.offsetWidth / aspectRatio) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
x-init="setDimension(); window.addEventListener('resize', setDimension)"
|
||||||
|
{% endif %}>
|
||||||
|
<div x-data="tradeOfferCard()" class="transition-all duration-500 trade-offer-card my-auto"
|
||||||
@toggle-all.window="setBadge($event.detail.expanded)">
|
@toggle-all.window="setBadge($event.detail.expanded)">
|
||||||
|
|
||||||
<!-- Flip container providing perspective -->
|
<!-- Flip container providing perspective -->
|
||||||
|
|
@ -26,85 +42,136 @@
|
||||||
The rotating element (.flip-inner) now uses CSS Grid to stack its children in a single cell.
|
The rotating element (.flip-inner) now uses CSS Grid to stack its children in a single cell.
|
||||||
Persistent border, shadow, and rounding are applied here and the card rotates entirely.
|
Persistent border, shadow, and rounding are applied here and the card rotates entirely.
|
||||||
-->
|
-->
|
||||||
<div class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-96 md:w-80 lg:w-96 transform transition-transform duration-700 ease-in-out"
|
<div class="flip-inner freeze-bg-color grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg {% if screenshot_mode and num_cards_available >= 4 %}w-160{% else %}w-96 md:w-80 lg:w-96{% endif %} transform transition-transform duration-700 ease-in-out"
|
||||||
:class="{'rotate-y-180': flipped}">
|
:class="{'rotate-y-180': flipped}">
|
||||||
|
|
||||||
<!-- Front Face: Trade Offer -->
|
<!-- Front Face: Trade Offer -->
|
||||||
<!-- Using grid placement classes (col-start-1 row-start-1) ensures both faces overlap -->
|
<!-- Using grid placement classes (col-start-1 row-start-1) ensures both faces overlap -->
|
||||||
<div class="flip-face front col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between">
|
<div class="flip-face front {% if screenshot_mode %}mb-2{% endif %} col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="self-start">
|
<div class="flip-face-header self-start">
|
||||||
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
|
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
|
||||||
<div class="py-4 mx-2 sm:mx-4">
|
<!-- Set this container as relative to position the avatar absolutely -->
|
||||||
<div class="grid grid-cols-3 items-center">
|
<div class="relative mt-6 mb-4 mx-2 sm:mx-4">
|
||||||
<div class="flex justify-center items-center">
|
<!-- Two-column grid for the labels -->
|
||||||
<span class="text-sm font-semibold">Has</span>
|
<div class="grid grid-cols-2 items-center">
|
||||||
</div>
|
<span class="text-sm font-semibold text-center">Has</span>
|
||||||
<div class="flex justify-center items-center">
|
<span class="text-sm font-semibold text-center">Wants</span>
|
||||||
<div class="avatar">
|
</div>
|
||||||
<div class="w-10 rounded-full">
|
<!-- The avatar is placed absolutely and centered -->
|
||||||
{{ initiated_by_email|gravatar:40 }}
|
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
|
||||||
</div>
|
<div class="avatar">
|
||||||
|
<div class="w-10 rounded-full">
|
||||||
|
{{ initiated_by_email|gravatar:40 }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center items-center">
|
|
||||||
<span class="text-sm font-semibold">Wants</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Main Trade Offer Row -->
|
<!-- Main Trade Offer Row -->
|
||||||
<div class="self-start">
|
<div class="flip-face-body self-start">
|
||||||
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
|
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
|
||||||
<div class="px-2 pb-0">
|
<div class="px-2 pb-0 main-badges">
|
||||||
<div class="grid grid-cols-2 gap-2 items-center">
|
{% if screenshot_mode and num_cards_available >= 4 %}
|
||||||
<div class="flex flex-col items-center">
|
<!-- When screenshot_mode is true, use an outer grid with 3 columns: Has side, a vertical divider, and Wants side -->
|
||||||
{% if have_cards_available %}
|
<div class="flex flex-row gap-2 justify-between">
|
||||||
{% with first_have=have_cards_available.0 %}
|
<!-- Has Side (inner grid of 2 columns) -->
|
||||||
{% card_badge first_have.card first_have.quantity %}
|
<div class="flex flex-row gap-2">
|
||||||
{% endwith %}
|
{% for card in have_cards_available|slice:"0:2" %}
|
||||||
{% endif %}
|
{% card_badge card.card card.quantity %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<!-- Vertical Divider -->
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="w-px bg-gray-300 h-full"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Wants Side (inner grid of 2 columns) -->
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
{% for card in want_cards_available|slice:"0:2" %}
|
||||||
|
{% card_badge card.card card.quantity %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center">
|
{% else %}
|
||||||
{% if want_cards_available %}
|
<!-- Normal mode: just use an outer grid with 2 columns -->
|
||||||
{% with first_want=want_cards_available.0 %}
|
<div class="flex flex-row gap-2 {% if not screenshot_mode %}justify-between{% else %}justify-around{% endif %}">
|
||||||
{% card_badge first_want.card first_want.quantity %}
|
<!-- Has Side -->
|
||||||
{% endwith %}
|
<div class="flex flex-col gap-2">
|
||||||
{% endif %}
|
{% for card in have_cards_available|slice:"0:1" %}
|
||||||
|
{% card_badge card.card card.quantity %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<!-- Wants Side -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{% for card in want_cards_available|slice:"0:1" %}
|
||||||
|
{% card_badge card.card card.quantity %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Extra Card Badges (Collapsible) -->
|
||||||
|
{% if screenshot_mode and num_cards_available >= 4 %}
|
||||||
|
<div class="px-2 extra-badges">
|
||||||
|
<!-- In screenshot mode, add a vertical divider between the Has and Wants sides -->
|
||||||
|
<div class="flex flex-row gap-2 justify-between">
|
||||||
|
<!-- Has Side Extra Badges -->
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
{% for card in have_cards_available|slice:"2:" %}
|
||||||
|
{% card_badge card.card card.quantity %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<!-- Vertical Divider -->
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="w-px bg-gray-300 h-full"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Wants Side Extra Badges -->
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
{% for card in want_cards_available|slice:"2:" %}
|
||||||
|
{% card_badge card.card card.quantity %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
<!-- Extra Card Badges (Collapsible) -->
|
<div {% if screenshot_mode %}x-show="badgeExpanded" x-collapse.duration.500ms{% endif %} class="px-2 extra-badges">
|
||||||
<div x-show="badgeExpanded" x-collapse.duration.500ms class="px-2">
|
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="flex flex-row gap-2 {% if not screenshot_mode %}justify-between{% else %}justify-around{% endif %}">
|
||||||
<div class="flex flex-col items-center">
|
<!-- Has Side Extra Badges -->
|
||||||
{% for th in have_cards_available|slice:"1:" %}
|
<div class="flex flex-col gap-2">
|
||||||
{% card_badge th.card th.quantity %}
|
{% for card in have_cards_available|slice:"1:" %}
|
||||||
|
{% card_badge card.card card.quantity %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center">
|
<!-- Wants Side Extra Badges -->
|
||||||
{% for th in want_cards_available|slice:"1:" %}
|
<div class="flex flex-col gap-2">
|
||||||
{% card_badge th.card th.quantity %}
|
{% for card in want_cards_available|slice:"1:" %}
|
||||||
|
{% card_badge card.card card.quantity %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
{% endif %}
|
||||||
<div class="flex justify-center my-1 h-5">
|
|
||||||
{% if have_cards_available|length > 1 or want_cards_available|length > 1 %}
|
|
||||||
<svg @click="badgeExpanded = !badgeExpanded"
|
|
||||||
x-bind:class="{ 'rotate-180': badgeExpanded }"
|
|
||||||
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="self-end">
|
{% if not screenshot_mode %}
|
||||||
|
<div class="flex justify-center h-5">
|
||||||
|
{% if have_cards_available|length > 1 or want_cards_available|length > 1 %}
|
||||||
|
<svg @click="badgeExpanded = !badgeExpanded"
|
||||||
|
x-bind:class="{ 'rotate-180': badgeExpanded }"
|
||||||
|
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if not screenshot_mode %}
|
||||||
|
<div class="flip-face-footer self-end">
|
||||||
<div class="flex justify-between px-2 pb-2">
|
<div class="flex justify-between px-2 pb-2">
|
||||||
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
|
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
|
|
@ -123,10 +190,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="flip-face-footer self-end">
|
||||||
|
<div class="flex flex-col gap-2 text-center">
|
||||||
|
<div class="text-sm font-semibold text-base-content">{{ in_game_name }} <span class="text-base-content/50">•</span> {{ friend_code }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Back Face: Acceptances View -->
|
<!-- Back Face: Acceptances View -->
|
||||||
<!-- Placed in the same grid cell as the front face -->
|
<!-- Placed in the same grid cell as the front face -->
|
||||||
|
{% if not screenshot_mode %}
|
||||||
<div class="flip-face back col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between" style="transform: rotateY(180deg);">
|
<div class="flip-face back col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between" style="transform: rotateY(180deg);">
|
||||||
<div class="self-start">
|
<div class="self-start">
|
||||||
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline">
|
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline">
|
||||||
|
|
@ -208,17 +283,17 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center my-1 h-5">
|
<div class="flex justify-center h-5">
|
||||||
{% if acceptances|length > 1 %}
|
{% if acceptances|length > 1 %}
|
||||||
<svg @click="acceptanceExpanded = !acceptanceExpanded"
|
<svg @click="acceptanceExpanded = !acceptanceExpanded"
|
||||||
x-bind:class="{ 'rotate-180': acceptanceExpanded }"
|
x-bind:class="{ 'rotate-180': acceptanceExpanded }"
|
||||||
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
|
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
|
||||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M19 9l-7 7-7-7" />
|
d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between px-2 pb-2 self-end">
|
<div class="flex justify-between px-2 pb-2 self-end">
|
||||||
<!-- Back-to-front flip button -->
|
<!-- Back-to-front flip button -->
|
||||||
|
|
@ -242,11 +317,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<style>
|
<style>
|
||||||
/* Ensure proper 3D transformations on the rotating element */
|
/* Ensure proper 3D transformations on the rotating element */
|
||||||
.flip-inner {
|
.flip-inner {
|
||||||
|
|
@ -265,5 +340,10 @@
|
||||||
.rotate-y-180 {
|
.rotate-y-180 {
|
||||||
transform: rotateY(180deg);
|
transform: rotateY(180deg);
|
||||||
}
|
}
|
||||||
|
{% if screenshot_mode %}
|
||||||
|
*:not(.freeze-bg-color) {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
</style>
|
</style>
|
||||||
{% endcache %}
|
{% endcache %}
|
||||||
|
|
@ -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
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from django import template
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@register.inclusion_tag('templatetags/trade_offer.html', takes_context=True)
|
@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.
|
Renders a trade offer including detailed trade acceptance information.
|
||||||
Freezes the through-model querysets to avoid extra DB hits.
|
Freezes the through-model querysets to avoid extra DB hits.
|
||||||
|
|
@ -33,6 +33,10 @@ def render_trade_offer(context, offer):
|
||||||
'acceptances': acceptances,
|
'acceptances': acceptances,
|
||||||
'have_cards_available': have_cards_available,
|
'have_cards_available': have_cards_available,
|
||||||
'want_cards_available': want_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)
|
@register.inclusion_tag('templatetags/trade_acceptance.html', takes_context=True)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from .views import (
|
||||||
TradeAcceptanceUpdateView,
|
TradeAcceptanceUpdateView,
|
||||||
TradeOfferDeleteView,
|
TradeOfferDeleteView,
|
||||||
TradeOfferSearchView,
|
TradeOfferSearchView,
|
||||||
|
TradeOfferPNGView,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
@ -17,6 +18,7 @@ urlpatterns = [
|
||||||
path("my/", TradeOfferMyListView.as_view(), name="trade_offer_my_list"),
|
path("my/", TradeOfferMyListView.as_view(), name="trade_offer_my_list"),
|
||||||
path("search/", TradeOfferSearchView.as_view(), name="trade_offer_search"),
|
path("search/", TradeOfferSearchView.as_view(), name="trade_offer_search"),
|
||||||
path("<int:pk>/", TradeOfferDetailView.as_view(), name="trade_offer_detail"),
|
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("delete/<int:pk>/", TradeOfferDeleteView.as_view(), name="trade_offer_delete"),
|
||||||
path("offer/<int:offer_pk>", TradeAcceptanceCreateView.as_view(), name="trade_acceptance_create"),
|
path("offer/<int:offer_pk>", TradeAcceptanceCreateView.as_view(), name="trade_acceptance_create"),
|
||||||
path("accept/<int:pk>/", TradeAcceptanceUpdateView.as_view(), name="trade_acceptance_update"),
|
path("accept/<int:pk>/", TradeAcceptanceUpdateView.as_view(), name="trade_acceptance_update"),
|
||||||
|
|
|
||||||
177
trades/views.py
177
trades/views.py
|
|
@ -1,4 +1,5 @@
|
||||||
from django.views.generic import TemplateView, DeleteView, CreateView, ListView, DetailView, UpdateView, FormView
|
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.urls import reverse_lazy
|
||||||
from django.http import HttpResponseRedirect, JsonResponse
|
from django.http import HttpResponseRedirect, JsonResponse
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
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.core.paginator import Paginator
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
|
||||||
|
from meta.views import Meta
|
||||||
from .models import TradeOffer, TradeAcceptance
|
from .models import TradeOffer, TradeAcceptance
|
||||||
from .forms import (TradeOfferAcceptForm,
|
from .forms import (TradeOfferAcceptForm,
|
||||||
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
|
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
|
||||||
from cards.models import Card
|
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):
|
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
|
||||||
model = TradeOffer
|
model = TradeOffer
|
||||||
|
|
@ -383,7 +391,7 @@ class TradeOfferSearchView(ListView):
|
||||||
else:
|
else:
|
||||||
return super().render_to_response(context, **response_kwargs)
|
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.
|
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,
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
trade_offer = self.get_object()
|
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:
|
# Define terminal (closed) acceptance states based on our new system:
|
||||||
terminal_states = [
|
terminal_states = [
|
||||||
|
|
@ -412,26 +471,31 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
|
||||||
# Option 1: Filter active acceptances using the queryset lookup.
|
# Option 1: Filter active acceptances using the queryset lookup.
|
||||||
context["active_acceptances"] = trade_offer.acceptances.exclude(state__in=terminal_states)
|
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()
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
# Add context flag and deletion URL if the current user is the initiator
|
# Determine the user's default friend code (or fallback as needed).
|
||||||
if trade_offer.initiated_by in user_friend_codes:
|
default_friend_code = self.request.user.default_friend_code or user_friend_codes.first()
|
||||||
context["is_initiator"] = True
|
|
||||||
context["delete_close_url"] = reverse_lazy("trade_offer_delete", kwargs={"pk": trade_offer.pk})
|
# 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:
|
else:
|
||||||
context["is_initiator"] = False
|
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
|
return context
|
||||||
|
|
||||||
class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
|
class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
|
@ -539,3 +603,82 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy("trade_offer_detail", kwargs={"pk": self.object.trade_offer.pk})
|
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