Compare commits

...

2 commits

Author SHA1 Message Date
0101a82ddf
Sort badges numerically instead of alphabetically
All checks were successful
Deploy to Server / deploy (push) Successful in 10s
2026-03-10 14:06:15 +01:00
719e5d416b
Make templates sexier 2026-03-10 13:24:30 +01:00
6 changed files with 54 additions and 59 deletions

View file

@ -8,26 +8,26 @@
{% if badge_sets %}
{% for data in badge_data %}
<h2>
<a href="{% url 'twitch:badge_set_detail' set_id=data.set.set_id %}">[{{ data.set.set_id }}]</a>
<a href="{% url 'twitch:badge_set_detail' set_id=data.set.set_id %}">{{ data.set.set_id }}</a>
</h2>
{% for badge in data.badges %}
<table>
{% for badge in data.badges %}
<tr>
<td style="width: 40px;">
<a href="{% url 'twitch:badge_set_detail' set_id=data.set.set_id %}">
{% picture badge.image_url_4x alt=badge.title width=36 height=36 %}
{% picture badge.image_url_4x alt=badge.title width=36 %}
</a>
</td>
<td>
<strong>{{ badge.title }}</strong>
{% if badge.description != badge.title %}
<br>
<br />
{{ badge.description }}
{% endif %}
</td>
</tr>
</table>
{% endfor %}
</table>
<br />
{% if data.badges|length > 1 %}<small>versions: {{ data.badges|length }}</small>{% endif %}
{% endfor %}

View file

@ -6,25 +6,7 @@
{% block content %}
<h1>{{ badge_set.set_id }}</h1>
{% if badges %}
<h2>
{{ badges.count }}
{% if badges.count == 1 %}
version
{% else %}
versions
{% endif %}
</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th></th>
<th>Title</th>
<th>Description</th>
<th>Images</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for badge in badges %}
<tr>
@ -32,7 +14,7 @@
<code>{{ badge.badge_id }}</code>
</td>
<td>
{% picture badge.image_url_4x alt=badge.title width=72 height=72 style="width: 72px !important; height: 72px !important; object-fit: contain" %}
{% picture badge.image_url_4x alt=badge.title width=72 style="width: 72px !important; height: 72px !important; object-fit: contain" %}
</td>
<td>{{ badge.title }}</td>
<td>{{ badge.description }}</td>

View file

@ -16,19 +16,13 @@
{% endblock extra_head %}
{% block content %}
<main>
<h1>Twitch Drops</h1>
<pre>
Latest drops are shown first within each game. Click on a campaign or game title to see more details.
Hover over the end time to see the exact date and time.
</pre>
<h1>Active Twitch Drops Campaigns</h1>
<!-- RSS Feeds -->
<div style="margin-bottom: 1rem;">
<div>
<a href="{% url 'twitch:campaign_feed' %}"
style="margin-right: 1rem"
title="RSS feed for all campaigns">RSS feed for campaigns</a>
&nbsp;|&nbsp;
title="RSS feed for all campaigns">[rss - all campaigns]</a>
<a href="{% url 'twitch:campaign_feed_atom' %}"
title="Atom feed for campaigns">Atom feed for campaigns</a>
title="Atom feed for campaigns">[atom - all campaigns]</a>
</div>
{% if campaigns_by_game %}
{% for game_id, game_data in campaigns_by_game.items %}

View file

@ -21,14 +21,6 @@
{{ game.display_name }}
{% if game.display_name != game.name and game.name %}<small>({{ game.name }})</small>{% endif %}
</h1>
<!-- RSS Feeds -->
<div>
<a href="{% url 'twitch:game_campaign_feed' game.twitch_id %}"
title="RSS feed for {{ game.display_name }} campaigns">RSS feed for {{ game.display_name }} campaigns</a>
&nbsp;|&nbsp;
<a href="{% url 'twitch:game_campaign_feed_atom' game.twitch_id %}"
title="Atom feed for {{ game.display_name }} campaigns">Atom feed for {{ game.display_name }} campaigns</a>
</div>
<!-- Game image -->
{% if game.box_art_best_url %}
{% picture game.box_art_best_url alt=game.name width=160 %}
@ -43,6 +35,13 @@
{% endfor %}
</small>
{% endif %}
<!-- RSS Feeds -->
<div>
<a href="{% url 'twitch:game_campaign_feed' game.twitch_id %}"
title="RSS feed for {{ game.display_name }} campaigns">[rss - {{ game.display_name|default:game.name|lower }}]</a>
<a href="{% url 'twitch:game_campaign_feed_atom' game.twitch_id %}"
title="Atom feed for {{ game.display_name }} campaigns">[atom - {{ game.display_name|default:game.name|lower }}]</a>
</div>
{% if active_campaigns %}
<h5 id="active-campaigns-header">Active Campaigns</h5>
<table id="active-campaigns-table">

View file

@ -3,26 +3,21 @@
Organizations
{% endblock title %}
{% block content %}
<h1 id="page-title">Organizations</h1>
<!-- RSS Feeds -->
<div style="margin-bottom: 1rem;">
<h1>Organizations</h1>
<div>
<a href="{% url 'twitch:organization_feed' %}"
style="margin-right: 1rem"
title="RSS feed for all organizations">RSS feed for organizations</a>
title="RSS feed for all organizations">[rss]</a>
<a href="{% url 'twitch:organization_feed_atom' %}"
title="Atom feed for all organizations">[atom]</a>
</div>
<!-- Export Options -->
<div style="margin-bottom: 1rem; display: flex; gap: 1rem;">
<a href="{% url 'twitch:export_organizations_csv' %}"
title="Export all organizations as CSV">[csv]</a>
<a href="{% url 'twitch:export_organizations_json' %}"
title="Export all organizations as JSON">[json]</a>
</div>
{% if orgs %}
<ul id="org-list">
<ul>
{% for organization in orgs %}
<li id="org-{{ organization.twitch_id }}">
<li>
<a href="{% url 'twitch:organization_detail' organization.twitch_id %}">{{ organization.name }}</a>
</li>
{% endfor %}

View file

@ -17,12 +17,14 @@ from django.core.paginator import PageNotAnInteger
from django.core.paginator import Paginator
from django.core.serializers import serialize
from django.db import connection
from django.db.models import Case
from django.db.models import Count
from django.db.models import Exists
from django.db.models import F
from django.db.models import OuterRef
from django.db.models import Prefetch
from django.db.models import Q
from django.db.models import When
from django.db.models.functions import Trim
from django.db.models.query import QuerySet
from django.http import FileResponse
@ -64,10 +66,9 @@ if TYPE_CHECKING:
from pathlib import Path
from debug_toolbar.utils import QueryDict
from django.db.models.query import QuerySet
from django.db.models import QuerySet
from django.http import HttpRequest
logger: logging.Logger = logging.getLogger("ttvdrops.views")
MIN_QUERY_LENGTH_FOR_FTS = 3
@ -2234,7 +2235,31 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse:
msg = "No badge set found matching the query"
raise Http404(msg) from exc
badges: QuerySet[ChatBadge] = badge_set.badges.all() # pyright: ignore[reportAttributeAccessIssue]
def get_sorted_badges(badge_set: ChatBadgeSet) -> QuerySet[ChatBadge]:
badges = badge_set.badges.all() # pyright: ignore[reportAttributeAccessIssue]
def sort_badges(badge: ChatBadge) -> tuple:
"""Sort badges by badge_id, treating numeric IDs as integers.
Args:
badge: The ChatBadge to sort.
Returns:
A tuple used for sorting, where numeric badge_ids are sorted as integers.
"""
try:
return (int(badge.badge_id),)
except ValueError:
return (badge.badge_id,)
sorted_badges: list[ChatBadge] = sorted(badges, key=sort_badges)
badge_ids: list[int] = [badge.pk for badge in sorted_badges]
preserved_order = Case(
*[When(pk=pk, then=pos) for pos, pk in enumerate(badge_ids)],
)
return ChatBadge.objects.filter(pk__in=badge_ids).order_by(preserved_order)
badges = get_sorted_badges(badge_set)
# Attach award_campaigns attribute to each badge for template use
for badge in badges:
@ -2255,7 +2280,7 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse:
)
set_data: list[dict[str, Any]] = json.loads(serialized_set)
if badges.exists():
if badges:
serialized_badges: str = serialize(
"json",
badges,
@ -2276,7 +2301,7 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse:
set_data[0]["fields"]["badges"] = badges_data
badge_set_name: str = badge_set.set_id
badge_set_description: str = f"Twitch chat badge set {badge_set_name} with {badges.count()} badge{'s' if badges.count() != 1 else ''} awarded through drop campaigns."
badge_set_description: str = f"Twitch chat badge set {badge_set_name} with {len(badges)} badge{'s' if len(badges) != 1 else ''} awarded through drop campaigns."
badge_schema: dict[str, Any] = {
"@context": "https://schema.org",