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 %} {% if badge_sets %}
{% for data in badge_data %} {% for data in badge_data %}
<h2> <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> </h2>
{% for badge in data.badges %}
<table> <table>
{% for badge in data.badges %}
<tr> <tr>
<td style="width: 40px;"> <td style="width: 40px;">
<a href="{% url 'twitch:badge_set_detail' set_id=data.set.set_id %}"> <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> </a>
</td> </td>
<td> <td>
<strong>{{ badge.title }}</strong> <strong>{{ badge.title }}</strong>
{% if badge.description != badge.title %} {% if badge.description != badge.title %}
<br> <br />
{{ badge.description }} {{ badge.description }}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
</table>
{% endfor %} {% endfor %}
</table>
<br /> <br />
{% if data.badges|length > 1 %}<small>versions: {{ data.badges|length }}</small>{% endif %} {% if data.badges|length > 1 %}<small>versions: {{ data.badges|length }}</small>{% endif %}
{% endfor %} {% endfor %}

View file

@ -6,25 +6,7 @@
{% block content %} {% block content %}
<h1>{{ badge_set.set_id }}</h1> <h1>{{ badge_set.set_id }}</h1>
{% if badges %} {% if badges %}
<h2>
{{ badges.count }}
{% if badges.count == 1 %}
version
{% else %}
versions
{% endif %}
</h2>
<table> <table>
<thead>
<tr>
<th>ID</th>
<th></th>
<th>Title</th>
<th>Description</th>
<th>Images</th>
<th>Action</th>
</tr>
</thead>
<tbody> <tbody>
{% for badge in badges %} {% for badge in badges %}
<tr> <tr>
@ -32,7 +14,7 @@
<code>{{ badge.badge_id }}</code> <code>{{ badge.badge_id }}</code>
</td> </td>
<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>
<td>{{ badge.title }}</td> <td>{{ badge.title }}</td>
<td>{{ badge.description }}</td> <td>{{ badge.description }}</td>

View file

@ -16,19 +16,13 @@
{% endblock extra_head %} {% endblock extra_head %}
{% block content %} {% block content %}
<main> <main>
<h1>Twitch Drops</h1> <h1>Active Twitch Drops Campaigns</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>
<!-- RSS Feeds --> <!-- RSS Feeds -->
<div style="margin-bottom: 1rem;"> <div>
<a href="{% url 'twitch:campaign_feed' %}" <a href="{% url 'twitch:campaign_feed' %}"
style="margin-right: 1rem" title="RSS feed for all campaigns">[rss - all campaigns]</a>
title="RSS feed for all campaigns">RSS feed for campaigns</a>
&nbsp;|&nbsp;
<a href="{% url 'twitch:campaign_feed_atom' %}" <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> </div>
{% if campaigns_by_game %} {% if campaigns_by_game %}
{% for game_id, game_data in campaigns_by_game.items %} {% for game_id, game_data in campaigns_by_game.items %}

View file

@ -21,14 +21,6 @@
{{ game.display_name }} {{ game.display_name }}
{% if game.display_name != game.name and game.name %}<small>({{ game.name }})</small>{% endif %} {% if game.display_name != game.name and game.name %}<small>({{ game.name }})</small>{% endif %}
</h1> </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 --> <!-- Game image -->
{% if game.box_art_best_url %} {% if game.box_art_best_url %}
{% picture game.box_art_best_url alt=game.name width=160 %} {% picture game.box_art_best_url alt=game.name width=160 %}
@ -43,6 +35,13 @@
{% endfor %} {% endfor %}
</small> </small>
{% endif %} {% 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 %} {% if active_campaigns %}
<h5 id="active-campaigns-header">Active Campaigns</h5> <h5 id="active-campaigns-header">Active Campaigns</h5>
<table id="active-campaigns-table"> <table id="active-campaigns-table">

View file

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

View file

@ -17,12 +17,14 @@ from django.core.paginator import PageNotAnInteger
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.core.serializers import serialize from django.core.serializers import serialize
from django.db import connection from django.db import connection
from django.db.models import Case
from django.db.models import Count from django.db.models import Count
from django.db.models import Exists from django.db.models import Exists
from django.db.models import F from django.db.models import F
from django.db.models import OuterRef from django.db.models import OuterRef
from django.db.models import Prefetch from django.db.models import Prefetch
from django.db.models import Q from django.db.models import Q
from django.db.models import When
from django.db.models.functions import Trim from django.db.models.functions import Trim
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.http import FileResponse from django.http import FileResponse
@ -64,10 +66,9 @@ if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
from debug_toolbar.utils import QueryDict from debug_toolbar.utils import QueryDict
from django.db.models.query import QuerySet from django.db.models import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
logger: logging.Logger = logging.getLogger("ttvdrops.views") logger: logging.Logger = logging.getLogger("ttvdrops.views")
MIN_QUERY_LENGTH_FOR_FTS = 3 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" msg = "No badge set found matching the query"
raise Http404(msg) from exc 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 # Attach award_campaigns attribute to each badge for template use
for badge in badges: 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) set_data: list[dict[str, Any]] = json.loads(serialized_set)
if badges.exists(): if badges:
serialized_badges: str = serialize( serialized_badges: str = serialize(
"json", "json",
badges, badges,
@ -2276,7 +2301,7 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse:
set_data[0]["fields"]["badges"] = badges_data set_data[0]["fields"]["badges"] = badges_data
badge_set_name: str = badge_set.set_id 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] = { badge_schema: dict[str, Any] = {
"@context": "https://schema.org", "@context": "https://schema.org",