various small bugfixes; add cards list view

This commit is contained in:
badblocks 2025-03-28 15:50:20 -07:00
parent d5f8345581
commit 05a279fa3a
10 changed files with 264 additions and 23 deletions

View file

@ -34,7 +34,7 @@ EXPOSE 8000
RUN python manage.py collectstatic --noinput RUN python manage.py collectstatic --noinput
#RUN python manage.py createcachetable django_cache #RUN python manage.py loaddata seed/* && python manage.py createcachetable django_cache
# Use gunicorn on port 8000 # Use gunicorn on port 8000
CMD ["gunicorn", "--bind", ":8000", "django_project.wsgi", "--timeout", "300"] CMD ["gunicorn", "--bind", ":8000", "django_project.wsgi", "--timeout", "300"]

View file

@ -3,11 +3,13 @@ from .views import (
CardDetailView, CardDetailView,
TradeOfferHaveCardListView, TradeOfferHaveCardListView,
TradeOfferWantCardListView, TradeOfferWantCardListView,
CardListView,
) )
app_name = "cards" app_name = "cards"
urlpatterns = [ urlpatterns = [
path('', CardListView.as_view(), name='card_list'),
path('<int:pk>/', CardDetailView.as_view(), name='card_detail'), path('<int:pk>/', CardDetailView.as_view(), name='card_detail'),
path('<int:pk>/trade-offers-have/', TradeOfferHaveCardListView.as_view(), name='card_trade_offer_have_list'), path('<int:pk>/trade-offers-have/', TradeOfferHaveCardListView.as_view(), name='card_trade_offer_have_list'),
path('<int:pk>/trade-offers-want/', TradeOfferWantCardListView.as_view(), name='card_trade_offer_want_list'), path('<int:pk>/trade-offers-want/', TradeOfferWantCardListView.as_view(), name='card_trade_offer_want_list'),

View file

@ -27,7 +27,7 @@ class TradeOfferHaveCardListView(ListView):
model = TradeOffer model = TradeOffer
template_name = "cards/_trade_offer_list.html" template_name = "cards/_trade_offer_list.html"
context_object_name = "trade_offers" context_object_name = "trade_offers"
paginate_by = 2 paginate_by = 6
def get_queryset(self): def get_queryset(self):
card_id = self.kwargs.get("pk") card_id = self.kwargs.get("pk")
@ -47,7 +47,7 @@ class TradeOfferWantCardListView(ListView):
model = TradeOffer model = TradeOffer
template_name = "cards/_trade_offer_list.html" template_name = "cards/_trade_offer_list.html"
context_object_name = "trade_offers" context_object_name = "trade_offers"
paginate_by = 2 paginate_by = 6
def get_queryset(self): def get_queryset(self):
card_id = self.kwargs.get("pk") card_id = self.kwargs.get("pk")
@ -62,3 +62,108 @@ class TradeOfferWantCardListView(ListView):
context['side'] = 'want' context['side'] = 'want'
return context return context
class CardListView(ListView):
model = Card
paginate_by = 100 # For non-grouped mode; grouping mode will override default pagination.
context_object_name = "cards"
def get_template_names(self):
if self.request.headers.get("x-requested-with") == "XMLHttpRequest":
return ["cards/_card_list.html"]
return ["cards/card_list.html"]
def get_ordering(self):
order = self.request.GET.get("order", "absolute")
if order == "alphabetical":
return "name"
elif order == "rarity":
return "-rarity_level"
else: # absolute ordering
return "id"
def get_queryset(self):
qs = super().get_queryset()
ordering = self.get_ordering()
qs = qs.order_by(ordering)
return qs.prefetch_related("decks").distinct()
def get_paginate_by(self, queryset):
group_by = self.request.GET.get("group_by")
if group_by in ("deck", "cardset", "rarity"):
# When grouping is enabled, we want to paginate manually so disable default pagination.
return None
return self.paginate_by
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
order = self.request.GET.get("order", "absolute")
group_by = self.request.GET.get("group_by")
context["order"] = order
context["group_by"] = group_by
if group_by in ("deck", "cardset", "rarity"):
# Fetch the complete queryset (no slicing)
full_qs = self.get_queryset()
all_cards = list(full_qs)
flat_cards = []
if group_by == "deck":
# Each card may belong to multiple decks reproduce the existing logic.
for card in all_cards:
for deck in card.decks.all():
flat_cards.append({"group": deck.name, "card": card})
flat_cards.sort(key=lambda x: x["group"].lower())
elif group_by == "cardset":
for card in all_cards:
flat_cards.append({"group": card.cardset, "card": card})
flat_cards.sort(key=lambda x: x["group"].lower())
elif group_by == "rarity":
for card in all_cards:
flat_cards.append({"group": card.rarity_level, "card": card})
flat_cards.sort(key=lambda x: x["group"], reverse=True)
total_cards = len(flat_cards)
try:
page_number = int(self.request.GET.get("page", 1))
if page_number < 1:
page_number = 1
except ValueError:
page_number = 1
per_page = 96
start = (page_number - 1) * per_page
end = page_number * per_page
page_flat_cards = flat_cards[start:end]
# Reassemble the flat list into grouped structure for just this page.
page_groups = []
for item in page_flat_cards:
group_value = item["group"]
card_obj = item["card"]
if page_groups and page_groups[-1]["group"] == group_value:
page_groups[-1]["cards"].append(card_obj)
else:
page_groups.append({"group": group_value, "cards": [card_obj]})
context["groups"] = page_groups
# Set up custom pagination context.
from math import ceil
num_pages = ceil(total_cards / per_page)
page_obj = {
"number": page_number,
"has_previous": page_number > 1,
"has_next": page_number < num_pages,
"previous_page_number": page_number - 1 if page_number > 1 else None,
"next_page_number": page_number + 1 if page_number < num_pages else None,
"paginator": {
"num_pages": num_pages,
},
}
context["page_obj"] = page_obj
context["is_paginated"] = total_cards > per_page
context["total_cards"] = total_cards
# Optionally, keep the full queryset in object_list.
context["object_list"] = full_qs
return context
return context

View file

@ -1,7 +1,7 @@
{% load static tailwind_tags gravatar %} {% load static tailwind_tags gravatar %}
{% url 'home' as home_url %} {% url 'home' as home_url %}
{% url 'trade_offer_list' as trade_offer_list_url %} {% url 'trade_offer_list' as trade_offer_list_url %}
{% url 'trade_offer_my_list' as trade_offer_my_list_url %} {% url 'cards:card_list' as cards_list_url %}
{% url 'settings' as settings_url %} {% url 'settings' as settings_url %}
<!DOCTYPE html> <!DOCTYPE html>
@ -67,6 +67,7 @@
tabindex="0" tabindex="0"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"> class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
<li><a href="{% url 'home' %}">Home</a></li> <li><a href="{% url 'home' %}">Home</a></li>
<li><a href="{% url 'cards:card_list' %}">Cards</a></li>
<li> <li>
<a>Trades</a> <a>Trades</a>
<ul class="p-2"> <ul class="p-2">
@ -87,6 +88,7 @@
<div class="navbar-center hidden md:flex"> <div class="navbar-center hidden md:flex">
<ul class="menu menu-horizontal px-1"> <ul class="menu menu-horizontal px-1">
<li><a href="{% url 'home' %}">Home</a></li> <li><a href="{% url 'home' %}">Home</a></li>
<li><a href="{% url 'cards:card_list' %}">Cards</a></li>
<li> <li>
<details> <details>
<summary>Trades</summary> <summary>Trades</summary>
@ -154,13 +156,13 @@
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><polyline points="1 11 12 2 23 11" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></polyline><path d="m5,13v7c0,1.105.895,2,2,2h10c1.105,0,2-.895,2-2v-7" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path><line x1="12" y1="22" x2="12" y2="18" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></line></g></svg> <svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><polyline points="1 11 12 2 23 11" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></polyline><path d="m5,13v7c0,1.105.895,2,2,2h10c1.105,0,2-.895,2-2v-7" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path><line x1="12" y1="22" x2="12" y2="18" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></line></g></svg>
<span class="dock-label">Home</span> <span class="dock-label">Home</span>
</button> </button>
<button @click="window.location.href = '{{ trade_offer_list_url }}'" class="{% if request.path == trade_offer_list_url %}dock-active{% endif %}"> <button @click="window.location.href = '{{ cards_list_url }}'" class="{% if request.path == cards_list_url %}dock-active{% endif %}">
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" /></svg> <svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v8.25A2.25 2.25 0 0 0 6 16.5h2.25m8.25-8.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-7.5A2.25 2.25 0 0 1 8.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 0 0-2.25 2.25v6" /></svg>
<span class="dock-label">All Offers</span> <span class="dock-label">Cards</span>
</button> </button>
<button @click="window.location.href = '{{ trade_offer_my_list_url }}'" class="{% if request.path == trade_offer_my_list_url %}dock-active{% endif %}"> <button @click="window.location.href = '{{ trade_offer_list_url }}'" class="{% if request.path == trade_offer_list_url %}dock-active{% endif %}">
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7.5 7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5" /></svg> <svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" /></svg>
<span class="dock-label">My Trades</span> <span class="dock-label">Trades</span>
</button> </button>
<button @click="window.location.href = '{{ settings_url }}'" class="{% if request.path == settings_url %}dock-active{% endif %}"> <button @click="window.location.href = '{{ settings_url }}'" class="{% if request.path == settings_url %}dock-active{% endif %}">
{% if user.is_authenticated %}<div tabindex="0" role="button" class="avatar"><div class="w-6 rounded-full">{{ user.email|gravatar:40 }}</div></div>{% else %}<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></circle><path d="m22,13.25v-2.5l-2.318-.966c-.167-.581-.395-1.135-.682-1.654l.954-2.318-1.768-1.768-2.318.954c-.518-.287-1.073-.515-1.654-.682l-.966-2.318h-2.5l-.966,2.318c-.581.167-1.135.395-1.654.682l-2.318-.954-1.768,1.768.954,2.318c-.287.518-.515,1.073-.682,1.654l-2.318.966v2.5l2.318.966c.167.581.395,1.135.682,1.654l-.954,2.318,1.768,1.768,2.318-.954c.518.287,1.073.515,1.654.682l.966,2.318h2.5l.966-2.318c.581-.167,1.135-.395,1.654-.682l2.318.954,1.768-1.768-.954-2.318c.287-.518.515-1.073.682-1.654l2.318-.966Z" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path></g></svg>{% endif %} {% if user.is_authenticated %}<div tabindex="0" role="button" class="avatar"><div class="w-6 rounded-full">{{ user.email|gravatar:40 }}</div></div>{% else %}<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></circle><path d="m22,13.25v-2.5l-2.318-.966c-.167-.581-.395-1.135-.682-1.654l.954-2.318-1.768-1.768-2.318.954c-.518-.287-1.073-.515-1.654-.682l-.966-2.318h-2.5l-.966,2.318c-.581.167-1.135.395-1.654.682l-2.318-.954-1.768,1.768.954,2.318c-.287.518-.515,1.073-.682,1.654l-2.318.966v2.5l2.318.966c.167.581.395,1.135.682,1.654l-.954,2.318,1.768,1.768,2.318-.954c.518.287,1.073.515,1.654.682l.966,2.318h2.5l.966-2.318c.581-.167,1.135-.395,1.654-.682l2.318.954,1.768-1.768-.954-2.318c.287-.518.515-1.073.682-1.654l2.318-.966Z" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path></g></svg>{% endif %}

View file

@ -0,0 +1,40 @@
{% load card_badge %}
{% if group_by and groups %}
{% for group in groups %}
<div class="divider">{{ group.group }}</div>
<div class="flex justify-center flex-wrap gap-4">
{% for card in group.cards %}
<a href="{% url 'cards:card_detail' card.pk %}">
{% card_badge card "" %}
</a>
{% endfor %}
</div>
{% endfor %}
{% else %}
<div class="flex justify-center flex-wrap gap-4">
{% for card in cards %}
<a href="{% url 'cards:card_detail' card.pk %}">
{% card_badge card "" %}
</a>
{% endfor %}
</div>
{% endif %}
<!-- Pagination Controls -->
<div class="mt-6">
{% if is_paginated %}
<div class="flex justify-center space-x-2">
{% if page_obj.has_previous %}
<button class="btn btn-outline" @click="$dispatch('change-page', { page: {{ page_obj.previous_page_number }} })">
Previous
</button>
{% endif %}
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<button class="btn btn-outline" @click="$dispatch('change-page', { page: {{ page_obj.next_page_number }} })">
Next
</button>
{% endif %}
</div>
{% endif %}
</div>

View file

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% load static card_badge %}
{% block content %}
<div class="container mx-auto p-4"
x-data="{
order: '{{ order }}',
groupBy: '{{ group_by|default:'none' }}',
page: 1,
loadCards() {
// Construct URL using current pathname and query parameters.
let groupParam = this.groupBy === 'none' ? '' : this.groupBy;
let url = window.location.pathname + '?order=' + this.order + '&group_by=' + groupParam + '&page=' + this.page;
fetch(url, { headers: { 'x-requested-with': 'XMLHttpRequest' } })
.then(response => response.text())
.then(html => { this.$refs.cardList.innerHTML = html; });
}
}"
x-init="loadCards()"
x-on:change-page.window="page = $event.detail.page; loadCards()"
>
<h1 class="text-2xl font-bold mb-4">Cards</h1>
<div class="flex flex-wrap items-center justify-between mb-6">
<!-- Sort Dropdown -->
<div class="dropdown dropdown-end m-1">
<div tabindex="0" class="btn">
<span x-text="order === 'absolute' ? 'Absolute' : (order === 'alphabetical' ? 'Alphabetical' : 'Rarity')"></span> 🞃
</div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="#" @click.prevent="order = 'absolute'; page = 1; loadCards()">Absolute</a></li>
<li><a href="#" @click.prevent="order = 'alphabetical'; page = 1; loadCards()">Alphabetical</a></li>
<li><a href="#" @click.prevent="order = 'rarity'; page = 1; loadCards()">Rarity</a></li>
</ul>
</div>
<!-- Grouping Dropdown -->
<div class="dropdown dropdown-end m-1">
<div tabindex="0" class="btn">
<span x-text="groupBy === 'none' ? 'No Group' : (groupBy.charAt(0).toUpperCase() + groupBy.slice(1))"></span> 🞃
</div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="#" @click.prevent="groupBy = 'none'; page = 1; loadCards()">No Group</a></li>
<li><a href="#" @click.prevent="groupBy = 'deck'; page = 1; loadCards()">Deck</a></li>
<li><a href="#" @click.prevent="groupBy = 'cardset'; page = 1; loadCards()">Cardset</a></li>
<li><a href="#" @click.prevent="groupBy = 'rarity'; page = 1; loadCards()">Rarity</a></li>
</ul>
</div>
</div>
<!-- Container for the partial card list -->
<div x-ref="cardList">
<!-- The contents of _card_list.html will be loaded here via AJAX -->
</div>
</div>
{% endblock %}

View file

@ -92,7 +92,7 @@ document.addEventListener('DOMContentLoaded', function() {
searchEnabled: true, searchEnabled: true,
shouldSort: false, shouldSort: false,
allowHTML: true, allowHTML: true,
closeDropdownOnSelect: false, closeDropdownOnSelect: true,
removeItemButton: true, removeItemButton: true,
searchFields: ['label'], searchFields: ['label'],
resetScrollPosition: false, resetScrollPosition: false,

View file

@ -1,12 +1,14 @@
{% load gravatar card_badge tailwind_tags %}<!DOCTYPE html> {% load gravatar card_badge tailwind_tags %}<!DOCTYPE html>
<html> <html style="background-color: transparent !important;">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{base_url}}/static/css/dist/styles.css"> <style>
{% include 'static/css/dist/styles.css' %}
</style>
</head> </head>
<body> <body style="background-color: transparent !important;">
<div class="trade-offer-card-screenshot p-4 h-full w-auto flex justify-center"> <div class="trade-offer-card-screenshot p-4 h-full w-auto flex justify-center" style="background-color: transparent !important;">
<div class="transition-all duration-500 trade-offer-card my-auto"> <div class="transition-all duration-500 trade-offer-card my-auto">
<!-- Flip container providing perspective --> <!-- Flip container providing perspective -->

View file

@ -103,7 +103,11 @@ def render_trade_offer_png(context, offer, show_friend_code=False):
image_width = base_width image_width = base_width
image_height = int(round(image_width / aspect_ratio)) image_height = int(round(image_width / aspect_ratio))
base_url = context.get('request').build_absolute_uri('/') request = context.get("request")
if request.get_host().startswith("localhost"):
base_url = "http://{0}".format(request.get_host())
else:
base_url = "https://{0}".format(request.get_host())
return { return {
'offer_pk': offer.pk, 'offer_pk': offer.pk,

View file

@ -618,26 +618,60 @@ class TradeOfferPNGView(View):
html = render_to_string("templatetags/trade_offer_png.html", tag_context) html = render_to_string("templatetags/trade_offer_png.html", tag_context)
# If there's a query parameter 'debug' set to true, render the HTML to the response.
if request.GET.get('debug'):
return HttpResponse(html, content_type="text/html")
css = render_to_string("static/css/dist/styles.css")
# Launch Playwright to render the HTML and capture a screenshot. # Launch Playwright to render the HTML and capture a screenshot.
with sync_playwright() as p: with sync_playwright() as p:
print("Launching browser")
browser = p.chromium.launch( browser = p.chromium.launch(
headless=True, headless=True,
args=[ args=[
"--disable-gpu",
"--no-sandbox", "--no-sandbox",
'--disable-setuid-sandbox', "--disable-setuid-sandbox",
'--disable-dev-shm-usage', "--disable-dev-shm-usage",
'--disable-accelerated-2d-canvas', "--disable-accelerated-2d-canvas",
'--no-first-run', "--disable-gpu",
'--disable-gpu' #"--single-process",
"--no-zygote",
"--disable-audio-output",
#"--disable-software-rasterizer",
"--disable-webgl",
#"--disable-web-security",
#"--disable-features=LazyFrameLoading",
#"--disable-features=IsolateOrigins",
#"--disable-background-networking",
"--no-first-run",
] ]
) )
print("Launched browser, creating context")
context_browser = browser.new_context(viewport={"width": image_width, "height": image_height}) context_browser = browser.new_context(viewport={"width": image_width, "height": image_height})
print("Created context, creating page")
page = context_browser.new_page() page = context_browser.new_page()
page.set_content(html, wait_until="networkidle") print("Created page, setting content")
# Listen for all console logs, errors, and warnings
page.on("console", lambda msg: print(f"Console {msg.type}: {msg.text}"))
page.on("pageerror", lambda err: print(f"Page error: {err}"))
# Listen specifically for failed resource loads
page.on("requestfailed", lambda request: print(f"Failed to load: {request.url} - {request.failure.error_text}"))
# # Instead of using a link tag, let's inject the CSS directly
# css = render_to_string("static/css/dist/styles.css")
# page.add_style_tag(content=css)
page.set_content(html, wait_until="domcontentloaded")
print("Set content, waiting for element")
element = page.wait_for_selector(".trade-offer-card-screenshot") element = page.wait_for_selector(".trade-offer-card-screenshot")
print("Found element, capturing screenshot")
screenshot_bytes = element.screenshot(type="png", omit_background=True) screenshot_bytes = element.screenshot(type="png", omit_background=True)
print("Captured screenshot, closing browser")
browser.close() browser.close()
print("Closed browser, returning screenshot")
return HttpResponse(screenshot_bytes, content_type="image/png") return HttpResponse(screenshot_bytes, content_type="image/png")