various small bugfixes; add cards list view
This commit is contained in:
parent
d5f8345581
commit
05a279fa3a
10 changed files with 264 additions and 23 deletions
|
|
@ -34,7 +34,7 @@ EXPOSE 8000
|
|||
|
||||
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
|
||||
CMD ["gunicorn", "--bind", ":8000", "django_project.wsgi", "--timeout", "300"]
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ from .views import (
|
|||
CardDetailView,
|
||||
TradeOfferHaveCardListView,
|
||||
TradeOfferWantCardListView,
|
||||
CardListView,
|
||||
)
|
||||
|
||||
app_name = "cards"
|
||||
|
||||
urlpatterns = [
|
||||
path('', CardListView.as_view(), name='card_list'),
|
||||
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-want/', TradeOfferWantCardListView.as_view(), name='card_trade_offer_want_list'),
|
||||
|
|
|
|||
109
cards/views.py
109
cards/views.py
|
|
@ -27,7 +27,7 @@ class TradeOfferHaveCardListView(ListView):
|
|||
model = TradeOffer
|
||||
template_name = "cards/_trade_offer_list.html"
|
||||
context_object_name = "trade_offers"
|
||||
paginate_by = 2
|
||||
paginate_by = 6
|
||||
|
||||
def get_queryset(self):
|
||||
card_id = self.kwargs.get("pk")
|
||||
|
|
@ -47,7 +47,7 @@ class TradeOfferWantCardListView(ListView):
|
|||
model = TradeOffer
|
||||
template_name = "cards/_trade_offer_list.html"
|
||||
context_object_name = "trade_offers"
|
||||
paginate_by = 2
|
||||
paginate_by = 6
|
||||
|
||||
def get_queryset(self):
|
||||
card_id = self.kwargs.get("pk")
|
||||
|
|
@ -62,3 +62,108 @@ class TradeOfferWantCardListView(ListView):
|
|||
context['side'] = 'want'
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% load static tailwind_tags gravatar %}
|
||||
{% url 'home' as home_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 %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
tabindex="0"
|
||||
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 'cards:card_list' %}">Cards</a></li>
|
||||
<li>
|
||||
<a>Trades</a>
|
||||
<ul class="p-2">
|
||||
|
|
@ -87,6 +88,7 @@
|
|||
<div class="navbar-center hidden md:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<li><a href="{% url 'home' %}">Home</a></li>
|
||||
<li><a href="{% url 'cards:card_list' %}">Cards</a></li>
|
||||
<li>
|
||||
<details>
|
||||
<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>
|
||||
<span class="dock-label">Home</span>
|
||||
</button>
|
||||
<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.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">All Offers</span>
|
||||
<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"><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">Cards</span>
|
||||
</button>
|
||||
<button @click="window.location.href = '{{ trade_offer_my_list_url }}'" class="{% if request.path == trade_offer_my_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>
|
||||
<span class="dock-label">My Trades</span>
|
||||
<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"><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">Trades</span>
|
||||
</button>
|
||||
<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 %}
|
||||
|
|
|
|||
40
theme/templates/cards/_card_list.html
Normal file
40
theme/templates/cards/_card_list.html
Normal 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>
|
||||
52
theme/templates/cards/card_list.html
Normal file
52
theme/templates/cards/card_list.html
Normal 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 %}
|
||||
|
|
@ -92,7 +92,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
searchEnabled: true,
|
||||
shouldSort: false,
|
||||
allowHTML: true,
|
||||
closeDropdownOnSelect: false,
|
||||
closeDropdownOnSelect: true,
|
||||
removeItemButton: true,
|
||||
searchFields: ['label'],
|
||||
resetScrollPosition: false,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
{% load gravatar card_badge tailwind_tags %}<!DOCTYPE html>
|
||||
<html>
|
||||
<html style="background-color: transparent !important;">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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>
|
||||
<body>
|
||||
<div class="trade-offer-card-screenshot p-4 h-full w-auto flex justify-center">
|
||||
<body style="background-color: transparent !important;">
|
||||
<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">
|
||||
|
||||
<!-- Flip container providing perspective -->
|
||||
|
|
|
|||
|
|
@ -103,7 +103,11 @@ def render_trade_offer_png(context, offer, show_friend_code=False):
|
|||
image_width = base_width
|
||||
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 {
|
||||
'offer_pk': offer.pk,
|
||||
|
|
|
|||
|
|
@ -618,26 +618,60 @@ class TradeOfferPNGView(View):
|
|||
|
||||
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.
|
||||
with sync_playwright() as p:
|
||||
print("Launching browser")
|
||||
browser = p.chromium.launch(
|
||||
headless=True,
|
||||
args=[
|
||||
"--disable-gpu",
|
||||
"--no-sandbox",
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--no-first-run',
|
||||
'--disable-gpu'
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-accelerated-2d-canvas",
|
||||
"--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})
|
||||
print("Created context, creating 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")
|
||||
print("Found element, capturing screenshot")
|
||||
screenshot_bytes = element.screenshot(type="png", omit_background=True)
|
||||
print("Captured screenshot, closing browser")
|
||||
browser.close()
|
||||
print("Closed browser, returning screenshot")
|
||||
|
||||
return HttpResponse(screenshot_bytes, content_type="image/png")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue