Add ids to tags; use pygments to color JSON

This commit is contained in:
Joakim Hellsén 2025-09-07 22:31:31 +02:00
commit 8f438aca2d
18 changed files with 365 additions and 211 deletions

View file

@ -17,6 +17,7 @@ dependencies = [
"platformdirs>=4.3.8", "platformdirs>=4.3.8",
"python-dotenv>=1.1.1", "python-dotenv>=1.1.1",
"psycopg[binary]>=3.2.3", "psycopg[binary]>=3.2.3",
"pygments>=2.19.2",
] ]
[dependency-groups] [dependency-groups]

View file

@ -3,15 +3,15 @@
Login Login
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h4>Login</h4> <h4 id="page-title">Login</h4>
{% if form.errors %} {% if form.errors %}
<ul> <ul id="error-list">
{% for field, errors in form.errors.items %} {% for field, errors in form.errors.items %}
{% for error in errors %}<li>{{ error }}</li>{% endfor %} {% for error in errors %}<li>{{ error }}</li>{% endfor %}
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
<form method="post"> <form id="login-form" method="post">
{% csrf_token %} {% csrf_token %}
<label for="{{ form.username.id_for_label }}">Username</label> <label for="{{ form.username.id_for_label }}">Username</label>
<input type="text" <input type="text"
@ -24,10 +24,10 @@
name="password" name="password"
id="{{ form.password.id_for_label }}" id="{{ form.password.id_for_label }}"
required> required>
<button type="submit">Login</button> <button id="login-button" type="submit">Login</button>
</form> </form>
<p> <p>
Don't have an account? <a href="{% url 'accounts:signup' %}">Sign up here</a> Don't have an account? <a id="signup-link" href="{% url 'accounts:signup' %}">Sign up here</a>
</p> </p>
<style> <style>
.form-control { .form-control {

View file

@ -3,9 +3,11 @@
{{ user.username }} {{ user.username }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h2>{{ user.username }}</h2> <h2 id="username">{{ user.username }}</h2>
<p>Joined {{ user.date_joined|date:"F d, Y" }}</p> <p>
<table> Joined <time id="date-joined">{{ user.date_joined|date:"F d, Y" }}</time>
</p>
<table id="user-info-table">
<tr> <tr>
<td> <td>
<strong>Date Joined:</strong> <strong>Date Joined:</strong>
@ -25,12 +27,12 @@
<td>{{ user.email|default:"Not provided" }}</td> <td>{{ user.email|default:"Not provided" }}</td>
</tr> </tr>
</table> </table>
<a href="{% url 'accounts:logout' %}">Logout</a> <a id="logout-link" href="{% url 'accounts:logout' %}">Logout</a>
<h2>Will get notifications for these subscriptions:</h2> <h2>Will get notifications for these subscriptions:</h2>
<h3>Games</h3> <h3 id="games-subscriptions-header">Games</h3>
<ul> <ul id="games-subscriptions-list">
{% for item in games_with_inheritance %} {% for item in games_with_inheritance %}
<li> <li id="game-subscription-{{ item.game.id }}">
<a href="{% url 'twitch:game_detail' item.game.id %}">{{ item.game.display_name }}</a> <a href="{% url 'twitch:game_detail' item.game.id %}">{{ item.game.display_name }}</a>
{% if item.is_inherited %} {% if item.is_inherited %}
<span style="font-size: 0.85em; color: #666; font-style: italic;">(inherited from {{ item.inherited_from }})</span> <span style="font-size: 0.85em; color: #666; font-style: italic;">(inherited from {{ item.inherited_from }})</span>
@ -40,10 +42,10 @@
<li>You have no game subscriptions yet.</li> <li>You have no game subscriptions yet.</li>
{% endfor %} {% endfor %}
</ul> </ul>
<h3>Organizations</h3> <h3 id="org-subscriptions-header">Organizations</h3>
<ul> <ul id="org-subscriptions-list">
{% for subscription in org_subscriptions %} {% for subscription in org_subscriptions %}
<li> <li id="org-subscription-{{ subscription.organization_id }}">
<a href="{% url 'twitch:organization_detail' subscription.organization_id %}">{{ subscription.organization.name }}</a> <a href="{% url 'twitch:organization_detail' subscription.organization_id %}">{{ subscription.organization.name }}</a>
</li> </li>
{% empty %} {% empty %}

View file

@ -3,15 +3,15 @@
Sign Up Sign Up
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h4>Sign Up</h4> <h4 id="page-title">Sign Up</h4>
{% if form.errors %} {% if form.errors %}
<ul> <ul id="error-list">
{% for field, errors in form.errors.items %} {% for field, errors in form.errors.items %}
{% for error in errors %}<li>{{ error }}</li>{% endfor %} {% for error in errors %}<li>{{ error }}</li>{% endfor %}
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
<form method="post"> <form id="signup-form" method="post">
{% csrf_token %} {% csrf_token %}
<label for="{{ form.username.id_for_label }}">Username</label> <label for="{{ form.username.id_for_label }}">Username</label>
<input type="text" <input type="text"
@ -19,22 +19,22 @@
id="{{ form.username.id_for_label }}" id="{{ form.username.id_for_label }}"
value="{{ form.username.value|default:'' }}" value="{{ form.username.value|default:'' }}"
required> required>
{% if form.username.help_text %}{{ form.username.help_text }}{% endif %} {% if form.username.help_text %}<small id="username-help">{{ form.username.help_text }}</small>{% endif %}
<label for="{{ form.password1.id_for_label }}">Password</label> <label for="{{ form.password1.id_for_label }}">Password</label>
<input type="password" <input type="password"
name="password1" name="password1"
id="{{ form.password1.id_for_label }}" id="{{ form.password1.id_for_label }}"
required> required>
{% if form.password1.help_text %}{{ form.password1.help_text }}{% endif %} {% if form.password1.help_text %}<small id="password1-help">{{ form.password1.help_text }}</small>{% endif %}
<label for="{{ form.password2.id_for_label }}">Confirm Password</label> <label for="{{ form.password2.id_for_label }}">Confirm Password</label>
<input type="password" <input type="password"
name="password2" name="password2"
id="{{ form.password2.id_for_label }}" id="{{ form.password2.id_for_label }}"
required> required>
{% if form.password2.help_text %}{{ form.password2.help_text }}{% endif %} {% if form.password2.help_text %}<small id="password2-help">{{ form.password2.help_text }}</small>{% endif %}
<button type="submit">Sign Up</button> <button id="signup-button" type="submit">Sign Up</button>
</form> </form>
<p> <p>
Already have an account? <a href="{% url 'accounts:login' %}">Login here</a> Already have an account? <a id="login-link" href="{% url 'accounts:login' %}">Login here</a>
</p> </p>
{% endblock content %} {% endblock content %}

View file

@ -39,7 +39,33 @@
</title> </title>
<style> <style>
html { color-scheme: light dark; } html { color-scheme: light dark; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; line-height: 1.4; padding: 0 15px; font-size: 115%;}
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; line-height: 1.4; padding: 0; font-size: 115%; max-width: 75%; margin: 0 auto;}
@media (max-width: 900px) { body { max-width: 95%; } }
table { width: 100%; }
th, td { padding: 8px; text-align: left; vertical-align: middle; }
th {background-color: Canvas; color: CanvasText; font-weight: bold; }
tr:nth-child(even) { background-color: color-mix(in srgb, Canvas 95%, CanvasText 5%); }
td img { display: block; height: 160px; width: 160px; object-fit: cover; border-radius: 4px; }
@media (prefers-color-scheme: dark) {
.highlight { background: #0d1117; color: #E6EDF3; }
.highlight .p { color: #E6EDF3; }
.highlight .nt { color: #7EE787; }
.highlight .s2, .highlight .mi { color: #A5D6FF; }
.highlight .kc { color: #79C0FF; }
.highlight .w { color: #6E7681; }
}
@media (prefers-color-scheme: light) {
.highlight { background: #f6f8fa; color: #24292e; }
.highlight .p { color: #24292e; }
.highlight .nt { color: #005cc5; }
.highlight .s2, .highlight .mi { color: #032f62; }
.highlight .kc { color: #d73a49; }
.highlight .w { color: #6a737d; }
}
</style> </style>
</head> </head>
<body> <body>
@ -47,17 +73,7 @@
<a href="{% url 'twitch:campaign_list' %}">Campaigns</a> | <a href="{% url 'twitch:campaign_list' %}">Campaigns</a> |
<a href="{% url 'twitch:game_list' %}">Games</a> | <a href="{% url 'twitch:game_list' %}">Games</a> |
<a href="{% url 'twitch:org_list' %}">Organizations</a> | <a href="{% url 'twitch:org_list' %}">Organizations</a> |
<a href="{% url 'twitch:docs_rss' %}">RSS Docs</a> | <a href="{% url 'twitch:docs_rss' %}">RSS</a> |
<form action="{% url 'twitch:search' %}"
method="get"
style="display: inline">
<input type="search"
name="q"
placeholder="Search..."
value="{{ request.GET.q }}">
<button type="submit">Search</button>
</form>
|
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a href="{% url 'twitch:debug' %}">Debug</a> | <a href="{% url 'twitch:debug' %}">Debug</a> |
{% if user.is_staff %} {% if user.is_staff %}
@ -68,6 +84,16 @@
<a href="{% url 'accounts:login' %}">Login</a> | <a href="{% url 'accounts:login' %}">Login</a> |
<a href="{% url 'accounts:signup' %}">Sign Up</a> <a href="{% url 'accounts:signup' %}">Sign Up</a>
{% endif %} {% endif %}
|
<form action="{% url 'twitch:search' %}"
method="get"
style="display: inline">
<input type="search"
name="q"
placeholder="Search..."
value="{{ request.GET.q }}">
<button type="submit">Search</button>
</form>
{% if messages %} {% if messages %}
<ul> <ul>
{% for message in messages %} {% for message in messages %}

View file

@ -4,96 +4,136 @@
{{ campaign.clean_name }} {{ campaign.clean_name }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h1> <!-- Campaign Title -->
<a href="{% url 'twitch:game_detail' campaign.game.id %}">{{ campaign.game.display_name }}</a> - {{ campaign.clean_name }} {% if campaign.game %}
<h1 id="campaign-title">
<a href="{% url 'twitch:game_detail' campaign.game.id %}">{{ campaign.game.name }}</a> - {{ campaign.clean_name }}
</h1> </h1>
{% if campaign.owner %}
<p>
<a href="{% url 'twitch:organization_detail' campaign.owner.id %}">{{ campaign.owner.name }}</a>
</p>
{% else %} {% else %}
<p>Organization Unknown</p> <h1 id="campaign-title">{{ campaign.clean_name }}</h1>
{% endif %} {% endif %}
{% if owner %}
<p id="campaign-owner">
<a href="{% url 'twitch:organization_detail' owner.id %}">{{ owner.name }}</a>
</p>
{% endif %}
<!-- Campaign image -->
{% if campaign.image_url %} {% if campaign.image_url %}
<img height="70" <img id="campaign-image"
width="70" height="160"
width="160"
src="{{ campaign.image_url }}" src="{{ campaign.image_url }}"
alt="{{ campaign.name }}"> alt="{{ campaign.name }}">
{% endif %} {% endif %}
<p>{{ campaign.description|linebreaksbr }}</p> <!-- Campaign description -->
<p> <p id="campaign-description">{{ campaign.description|linebreaksbr }}</p>
Start: <!-- Campaign end times -->
{{ campaign.start_at }} <div>
</p> <time id="campaign-end-time"
<p> datetime="{{ campaign.end_at|date:'c' }}"
End: title="{{ campaign.end_at|date:'DATETIME_FORMAT' }}">
{{ campaign.end_at }} <strong>Ends in</strong> {{ campaign.end_at|timeuntil }}
</p> </time>
</div>
<!-- Campaign start times -->
<div>
<time id="campaign-start-time"
datetime="{{ campaign.start_at|date:'c' }}"
title="{{ campaign.start_at|date:'DATETIME_FORMAT' }}">
<strong>Started</strong> {{ campaign.start_at|timesince }} ago
</time>
</div>
<!-- Campaign added times -->
<div>
<time id="campaign-added-time"
datetime="{{ campaign.added_at|date:'c' }}"
title="{{ campaign.added_at|date:'DATETIME_FORMAT' }}">
<strong>Scraped</strong> {{ campaign.added_at|timesince }} ago
</time>
</div>
<!-- Campaign duration -->
<div>
<time id="campaign-duration"
datetime="{{ campaign.start_at|date:'c' }} to {{ campaign.end_at|date:'c' }}"
title="{{ campaign.start_at|date:'DATETIME_FORMAT' }} to {{ campaign.end_at|date:'DATETIME_FORMAT' }}">
<strong>Duration</strong> {{ campaign.start_at|timesince:campaign.end_at }} ago
</time>
</div>
<!-- Campaign Detail URL -->
{% if campaign.details_url %} {% if campaign.details_url %}
{# TODO: Archive this URL automatically #} {# TODO: Archive this URL automatically #}
<p> <p>
<a href="{{ campaign.details_url }}" target="_blank">Official Details</a> <a id="campaign-details-url"
href="{{ campaign.details_url }}"
target="_blank">Official Details</a>
</p> </p>
{% endif %} {% endif %}
<!-- Campaign Account Link URL -->
{% if campaign.account_link_url %} {% if campaign.account_link_url %}
{# TODO: Archive this URL automatically #} {# TODO: Archive this URL automatically #}
<p> <p>
<a href="{{ campaign.account_link_url }}" target="_blank">Connect Account</a> <a id="campaign-account-link-url"
href="{{ campaign.account_link_url }}"
target="_blank">Connect Account</a>
</p> </p>
{% endif %} {% endif %}
<h5>Campaign Info</h5> <h5>Campaign Info</h5>
{% if user.is_staff %}
<p>
{% if campaign.is_account_connected %}
Connected
{% else %}
Not Connected
{% endif %}
</p>
{% endif %}
{% if drops %} {% if drops %}
<table> <table id="drops-table" style="border-collapse: collapse; width: 100%;">
<thead>
<tr> <tr>
<th>Image</th> <th>Benefits</th>
<th>Name</th> <th>Drop Name</th>
<th>Requirements</th> <th>Requirements</th>
<th>Availability</th> <th>Period</th>
</tr> </tr>
</thead>
<tbody>
{% for drop in drops %} {% for drop in drops %}
<tr> <tr id="drop-{{ drop.drop.id }}">
<td> <td>
{% for benefit in drop.benefits.all %} {% for benefit in drop.drop.benefits.all %}
{% if benefit.image_asset_url %} {% if benefit.image_asset_url %}
<img height="120" <img height="160"
width="120" width="160"
style="object-fit: cover" style="object-fit: cover;
margin-right: 3px"
src="{{ benefit.image_asset_url }}" src="{{ benefit.image_asset_url }}"
alt="{{ benefit.name }}"> alt="{{ benefit.name }}">
{% else %} {% else %}
<img height="120" <img height="160"
width="120" width="160"
style="object-fit: cover" style="object-fit: cover;
margin-right: 3px"
src="{% static 'images/placeholder.png' %}" src="{% static 'images/placeholder.png' %}"
alt="No Image Available"> alt="No Image Available">
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</td> </td>
<td>{{ drop.name }}</td> <td>{{ drop.drop.name }}</td>
<td> <td>
{{ drop.required_minutes_watched }} minutes watched {% if drop.drop.required_minutes_watched %}{{ drop.drop.required_minutes_watched }} minutes watched{% endif %}
{% if drop.required_subs > 0 %}and {{ drop.required_subs }} subscriptions required{% endif %} {% if drop.drop.required_subs > 0 %}
{% if drop.drop.required_minutes_watched %}and{% endif %}
{{ drop.drop.required_subs }} subscriptions required
{% endif %}
</td>
<td>
<div>
{% if drop.local_start %}{{ drop.local_start|date:"M j, Y H:i" }}{% endif %}
{% if drop.local_start and drop.local_end %}-{% endif %}
{% if drop.local_end %}{{ drop.local_end|date:"M j, Y H:i" }}{% endif %}
<small style="color: #666;">{{ drop.timezone_name }}</small>
</div>
<div style="font-size: 0.8em; color: #666;">{{ drop.countdown_text }}</div>
</td> </td>
<td>{{ drop.start_at }} - {{ drop.end_at }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% if not drops %} </tbody>
<tr>
<td colspan="6">No drops found for this campaign.</td>
</tr>
{% endif %}
</table> </table>
{% else %} {% else %}
<p>No drops available for this campaign.</p> <p>No drops available for this campaign.</p>
{% endif %} {% endif %}
<pre><code>{{ campaign_data }}</code></pre> <!-- Campaign JSON -->
{{ campaign_data|safe }}
{% endblock content %} {% endblock content %}

View file

@ -6,10 +6,11 @@
{% block content %} {% block content %}
<main> <main>
<header> <header>
<h1>Drop Campaigns</h1> <h1 id="page-title">Drop Campaigns</h1>
<p>Browse all available drop campaigns</p> <p>Browse all available drop campaigns</p>
</header> </header>
<form method="get" <form id="filter-form"
method="get"
action="{% url 'twitch:campaign_list' %}" action="{% url 'twitch:campaign_list' %}"
style="margin-bottom: 2rem; style="margin-bottom: 2rem;
padding: 1rem; padding: 1rem;
@ -42,13 +43,14 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<button type="submit">Apply Filters</button> <button id="apply-filters-button" type="submit">Apply Filters</button>
</div> </div>
</form> </form>
{% if campaigns %} {% if campaigns %}
{% regroup campaigns by game as campaigns_by_game %} {% regroup campaigns by game as campaigns_by_game %}
{% for game_group in campaigns_by_game %} {% for game_group in campaigns_by_game %}
<section style="margin-bottom: 3rem;"> <section id="game-group-{{ game_group.grouper.id }}"
style="margin-bottom: 3rem">
<div style="display: flex; gap: 1rem;"> <div style="display: flex; gap: 1rem;">
<div style="flex-shrink: 0;"> <div style="flex-shrink: 0;">
{% if game_group.grouper.box_art_base_url %} {% if game_group.grouper.box_art_base_url %}
@ -76,13 +78,15 @@
{% comment %} Find this header section in your template {% endcomment %} {% comment %} Find this header section in your template {% endcomment %}
<header style="margin-bottom: 1rem;"> <header style="margin-bottom: 1rem;">
<h2 style="margin: 0 0 0.5rem 0;"> <h2 style="margin: 0 0 0.5rem 0;">
<a href="{% url 'twitch:game_detail' game_group.grouper.id %}" <a id="game-link-{{ game_group.grouper.id }}"
href="{% url 'twitch:game_detail' game_group.grouper.id %}"
style="text-decoration: none">{{ game_group.grouper.display_name|default:game_group.grouper.name|default:game_group.grouper.slug|default:game_group.grouper.id }}</a> style="text-decoration: none">{{ game_group.grouper.display_name|default:game_group.grouper.name|default:game_group.grouper.slug|default:game_group.grouper.id }}</a>
</h2> </h2>
{% comment %} MODIFICATION: Check if the owner exists before creating the link {% endcomment %} {% comment %} MODIFICATION: Check if the owner exists before creating the link {% endcomment %}
{% if game_group.grouper.owner %} {% if game_group.grouper.owner %}
<p style="margin: 0;"> <p style="margin: 0;">
<a href="{% url 'twitch:organization_detail' game_group.grouper.owner.id %}" <a id="org-link-{{ game_group.grouper.owner.id }}"
href="{% url 'twitch:organization_detail' game_group.grouper.owner.id %}"
style="text-decoration: none">{{ game_group.grouper.owner.name }}</a> style="text-decoration: none">{{ game_group.grouper.owner.name }}</a>
</p> </p>
{% endif %} {% endif %}
@ -90,16 +94,19 @@
<div style="overflow-x: auto;"> <div style="overflow-x: auto;">
<div style="display: flex; gap: 1rem; min-width: max-content;"> <div style="display: flex; gap: 1rem; min-width: max-content;">
{% for campaign in game_group.list %} {% for campaign in game_group.list %}
<article style="display: flex; <article id="campaign-{{ campaign.id }}"
style="display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
padding: 0.5rem; padding: 0.5rem;
flex-shrink: 0"> flex-shrink: 0">
<div> <div>
<a href="{% url 'twitch:campaign_detail' campaign.id %}" <a id="campaign-link-{{ campaign.id }}"
href="{% url 'twitch:campaign_detail' campaign.id %}"
style="text-decoration: none"> style="text-decoration: none">
{% if campaign.image_url %} {% if campaign.image_url %}
<img src="{{ campaign.image_url }}" <img id="campaign-image-{{ campaign.id }}"
src="{{ campaign.image_url }}"
alt="Campaign artwork for {{ campaign.name }}" alt="Campaign artwork for {{ campaign.name }}"
width="120" width="120"
height="120" height="120"
@ -118,7 +125,9 @@
No Image No Image
</div> </div>
{% endif %} {% endif %}
<h4 style="margin: 0.5rem 0; text-align: left;">{{ campaign.clean_name }}</h4> <h4 id="campaign-name-{{ campaign.id }}"
style="margin: 0.5rem 0;
text-align: left">{{ campaign.clean_name }}</h4>
</a> </a>
<div style="font-size: 0.9rem;"> <div style="font-size: 0.9rem;">
<time datetime="{{ campaign.start_at|date:'c' }}" <time datetime="{{ campaign.start_at|date:'c' }}"
@ -133,11 +142,15 @@
</div> </div>
<div style="margin-top: 0.5rem;"> <div style="margin-top: 0.5rem;">
{% if campaign.start_at <= now and campaign.end_at >= now %} {% if campaign.start_at <= now and campaign.end_at >= now %}
<span style="font-weight: 600; color: #28a745;">Active</span> <span id="campaign-status-{{ campaign.id }}"
style="font-weight: 600;
color: #28a745">Active</span>
{% elif campaign.start_at > now %} {% elif campaign.start_at > now %}
<span style="font-weight: 600;">Upcoming</span> <span id="campaign-status-{{ campaign.id }}" style="font-weight: 600;">Upcoming</span>
{% else %} {% else %}
<span style="font-weight: 600; color: #dc3545;">Expired</span> <span id="campaign-status-{{ campaign.id }}"
style="font-weight: 600;
color: #dc3545">Expired</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -162,7 +175,7 @@
{% endif %} {% endif %}
<!-- Pagination --> <!-- Pagination -->
{% if is_paginated %} {% if is_paginated %}
<nav style="margin-top: 3rem; text-align: center;"> <nav id="pagination" style="margin-top: 3rem; text-align: center;">
<div style="display: flex; <div style="display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View file

@ -5,14 +5,14 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<main> <main>
<h1>Twitch Drops</h1> <h1 id="page-title">Twitch Drops</h1>
<pre> <pre>
Drops are sorted alphabetically by organization and game. Click on a campaign or game title to see more details. Drops are sorted alphabetically by organization and game. Click on a campaign or game title to see more details.
Hover over the end time to see the exact date and time. Hover over the end time to see the exact date and time.
</pre> </pre>
{% if campaigns_by_org_game %} {% if campaigns_by_org_game %}
{% for org_id, org_data in campaigns_by_org_game.items %} {% for org_id, org_data in campaigns_by_org_game.items %}
<section> <section id="org-section-{{ org_id }}">
<h2> <h2>
{% if org_data.name %} {% if org_data.name %}
{{ org_data.name }} {{ org_data.name }}
@ -21,7 +21,7 @@ Hover over the end time to see the exact date and time.
{% endif %} {% endif %}
</h2> </h2>
{% for game_id, game_data in org_data.games.items %} {% for game_id, game_data in org_data.games.items %}
<article> <article id="game-article-{{ game_id }}">
<header style="margin-bottom: 1rem;"> <header style="margin-bottom: 1rem;">
<h3 style="margin: 0 0 0.5rem 0;"> <h3 style="margin: 0 0 0.5rem 0;">
<a href="{% url 'twitch:game_detail' game_id %}">{{ game_data.name }}</a> <a href="{% url 'twitch:game_detail' game_id %}">{{ game_data.name }}</a>
@ -38,7 +38,8 @@ Hover over the end time to see the exact date and time.
<div style="flex: 1; overflow-x: auto;"> <div style="flex: 1; overflow-x: auto;">
<div style="display: flex; gap: 1rem; min-width: max-content;"> <div style="display: flex; gap: 1rem; min-width: max-content;">
{% for campaign in game_data.campaigns %} {% for campaign in game_data.campaigns %}
<article style="display: flex; <article id="campaign-article-{{ campaign.id }}"
style="display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 0.5rem; padding: 0.5rem;
@ -68,13 +69,13 @@ Hover over the end time to see the exact date and time.
text-align: left"> text-align: left">
Started {{ campaign.start_at|timesince }} ago Started {{ campaign.start_at|timesince }} ago
</time> </time>
<time datetime="{{ campaign.created_at|date:'c' }}" <time datetime="{{ campaign.added_at|date:'c' }}"
title="{{ campaign.created_at|date:'DATETIME_FORMAT' }}" title="{{ campaign.added_at|date:'DATETIME_FORMAT' }}"
style="font-size: 0.9rem; style="font-size: 0.9rem;
color: #666; color: #666;
display: block; display: block;
text-align: left"> text-align: left">
Scraped {{ campaign.created_at|timesince }} ago Scraped {{ campaign.added_at|timesince }} ago
</time> </time>
<time datetime="{{ campaign.start_at|date:'c' }} to {{ campaign.end_at|date:'c' }}" <time datetime="{{ campaign.start_at|date:'c' }} to {{ campaign.end_at|date:'c' }}"
title="{{ campaign.start_at|date:'DATETIME_FORMAT' }} to {{ campaign.end_at|date:'DATETIME_FORMAT' }}" title="{{ campaign.start_at|date:'DATETIME_FORMAT' }} to {{ campaign.end_at|date:'DATETIME_FORMAT' }}"

View file

@ -3,14 +3,16 @@
Debug Debug
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h1>Debug Data Integrity Report</h1> <h1 id="page-title">Debug Data Integrity Report</h1>
<p>Generated at: {{ now }}</p> <p>
Generated at: <time id="generation-time">{{ now }}</time>
</p>
<section> <section>
<h2>Games Without an Assigned Owner ({{ games_without_owner|length }})</h2> <h2 id="games-without-owner-header">Games Without an Assigned Owner ({{ games_without_owner|length }})</h2>
{% if games_without_owner %} {% if games_without_owner %}
<ul> <ul id="games-without-owner-list">
{% for game in games_without_owner %} {% for game in games_without_owner %}
<li> <li id="game-{{ game.id }}">
<a href="{% url 'twitch:game_detail' game.id %}">{{ game.display_name }}</a> (ID: {{ game.id }}) <a href="{% url 'twitch:game_detail' game.id %}">{{ game.display_name }}</a> (ID: {{ game.id }})
</li> </li>
{% endfor %} {% endfor %}
@ -20,11 +22,11 @@
{% endif %} {% endif %}
</section> </section>
<section> <section>
<h2>Campaigns With Broken Image URLs ({{ broken_image_campaigns|length }})</h2> <h2 id="broken-image-campaigns-header">Campaigns With Broken Image URLs ({{ broken_image_campaigns|length }})</h2>
{% if broken_image_campaigns %} {% if broken_image_campaigns %}
<ul> <ul id="broken-image-campaigns-list">
{% for c in broken_image_campaigns %} {% for c in broken_image_campaigns %}
<li> <li id="campaign-{{ c.id }}">
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a> <a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a>
(Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>) (Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>)
- URL: {{ c.image_url|default:'(empty)' }} - URL: {{ c.image_url|default:'(empty)' }}
@ -36,14 +38,14 @@
{% endif %} {% endif %}
</section> </section>
<section> <section>
<h2>Benefits With Broken Image URLs ({{ broken_benefit_images|length }})</h2> <h2 id="broken-benefit-images-header">Benefits With Broken Image URLs ({{ broken_benefit_images|length }})</h2>
{% if broken_benefit_images %} {% if broken_benefit_images %}
<ul> <ul id="broken-benefit-images-list">
{% for b in broken_benefit_images %} {% for b in broken_benefit_images %}
{# A benefit is linked to a game via a drop and a campaign. #} {# A benefit is linked to a game via a drop and a campaign. #}
{# We use the 'with' tag to get the first drop for cleaner access. #} {# We use the 'with' tag to get the first drop for cleaner access. #}
{% with first_drop=b.drops.all.0 %} {% with first_drop=b.drops.all.0 %}
<li> <li id="benefit-{{ b.id }}">
{{ b.name }} {{ b.name }}
{# Check if the relationship path to the game exists #} {# Check if the relationship path to the game exists #}
{% if first_drop and first_drop.campaign and first_drop.campaign.game %} {% if first_drop and first_drop.campaign and first_drop.campaign.game %}
@ -61,11 +63,11 @@
{% endif %} {% endif %}
</section> </section>
<section> <section>
<h2>Active Campaigns Missing Image ({{ active_missing_image|length }})</h2> <h2 id="active-missing-image-header">Active Campaigns Missing Image ({{ active_missing_image|length }})</h2>
{% if active_missing_image %} {% if active_missing_image %}
<ul> <ul id="active-missing-image-list">
{% for c in active_missing_image %} {% for c in active_missing_image %}
<li> <li id="campaign-{{ c.id }}">
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a> <a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a>
(Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>) (Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>)
</li> </li>
@ -76,11 +78,11 @@
{% endif %} {% endif %}
</section> </section>
<section> <section>
<h2>Time-Based Drops Without Benefits ({{ drops_without_benefits|length }})</h2> <h2 id="drops-without-benefits-header">Time-Based Drops Without Benefits ({{ drops_without_benefits|length }})</h2>
{% if drops_without_benefits %} {% if drops_without_benefits %}
<ul> <ul id="drops-without-benefits-list">
{% for d in drops_without_benefits %} {% for d in drops_without_benefits %}
<li> <li id="drop-{{ d.id }}">
{{ d.name }} {{ d.name }}
(Campaign: <a href="{% url 'twitch:campaign_detail' d.campaign.id %}">{{ d.campaign.name }}</a> (Campaign: <a href="{% url 'twitch:campaign_detail' d.campaign.id %}">{{ d.campaign.name }}</a>
in Game: <a href="{% url 'twitch:game_detail' d.campaign.game.id %}">{{ d.campaign.game.display_name }}</a>) in Game: <a href="{% url 'twitch:game_detail' d.campaign.game.id %}">{{ d.campaign.game.display_name }}</a>)
@ -92,11 +94,11 @@
{% endif %} {% endif %}
</section> </section>
<section> <section>
<h2>Campaigns With Invalid Dates ({{ invalid_date_campaigns|length }})</h2> <h2 id="invalid-date-campaigns-header">Campaigns With Invalid Dates ({{ invalid_date_campaigns|length }})</h2>
{% if invalid_date_campaigns %} {% if invalid_date_campaigns %}
<ul> <ul id="invalid-date-campaigns-list">
{% for c in invalid_date_campaigns %} {% for c in invalid_date_campaigns %}
<li> <li id="campaign-{{ c.id }}">
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a> <a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a>
(Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>) (Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>)
- Start: {{ c.start_at|default:'(none)' }} / End: {{ c.end_at|default:'(none)' }} - Start: {{ c.start_at|default:'(none)' }} / End: {{ c.end_at|default:'(none)' }}
@ -108,9 +110,9 @@
{% endif %} {% endif %}
</section> </section>
<section> <section>
<h2>Duplicate Campaign Names Per Game ({{ duplicate_name_campaigns|length }})</h2> <h2 id="duplicate-name-campaigns-header">Duplicate Campaign Names Per Game ({{ duplicate_name_campaigns|length }})</h2>
{% if duplicate_name_campaigns %} {% if duplicate_name_campaigns %}
<table> <table id="duplicate-name-campaigns-table">
<thead> <thead>
<tr> <tr>
<th>Game</th> <th>Game</th>

View file

@ -5,13 +5,13 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<main> <main>
<h1>RSS Feeds Documentation</h1> <h1 id="page-title">RSS Feeds Documentation</h1>
<p>This page lists all available RSS feeds for TTVDrops.</p> <p>This page lists all available RSS feeds for TTVDrops.</p>
<section> <section>
<h2>Available RSS Feeds</h2> <h2 id="available-feeds-header">Available RSS Feeds</h2>
<ul> <ul id="feeds-list">
{% for feed in feeds %} {% for feed in feeds %}
<li> <li id="feed-{{ forloop.counter }}">
<h3>{{ feed.title }}</h3> <h3>{{ feed.title }}</h3>
<p>{{ feed.description }}</p> <p>{{ feed.description }}</p>
<p> <p>

View file

@ -3,12 +3,27 @@
{{ game.display_name }} {{ game.display_name }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h1>{{ game.display_name }}</h1> <!-- Game Title -->
<h1 id="game-name">
{{ game.display_name }}
{% if game.display_name != game.name and game.name %}<small>({{ game.name }})</small>{% endif %}
</h1>
<!-- Game image -->
{% if game.box_art_url %}
<img id="game-image"
height="160"
width="160"
src="{{ game.box_art_url }}"
alt="{{ game.name }}">
{% endif %}
<!-- Game owner -->
{% if owner %} {% if owner %}
<small><a href="{% url 'twitch:organization_detail' owner.id %}">{{ owner.name }}</a></small> <small><a id="owner-link"
href="{% url 'twitch:organization_detail' owner.id %}">{{ owner.name }}</a></small>
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<form method="post" <form id="notification-form"
method="post"
action="{% url 'twitch:subscribe_notifications' game_id=game.id %}"> action="{% url 'twitch:subscribe_notifications' game_id=game.id %}">
{% csrf_token %} {% csrf_token %}
<div> <div>
@ -25,17 +40,17 @@
{% if subscription and subscription.notify_live %}checked{% endif %} /> {% if subscription and subscription.notify_live %}checked{% endif %} />
<label for="live">🎮 Get notified when the drop is live and ready to be farmed.</label> <label for="live">🎮 Get notified when the drop is live and ready to be farmed.</label>
</div> </div>
<button type="submit">Save notification preferences</button> <button id="save-notifications-button" type="submit">Save notification preferences</button>
</form> </form>
{% else %} {% else %}
Login to subscribe! <p id="login-prompt">Login to subscribe!</p>
{% endif %} {% endif %}
{% if active_campaigns %} {% if active_campaigns %}
<h5>Active Campaigns</h5> <h5 id="active-campaigns-header">Active Campaigns</h5>
<table> <table id="active-campaigns-table">
<tbody> <tbody>
{% for campaign in active_campaigns %} {% for campaign in active_campaigns %}
<tr> <tr id="campaign-row-{{ campaign.id }}">
<td> <td>
<a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a> <a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a>
</td> </td>
@ -48,11 +63,11 @@
</table> </table>
{% endif %} {% endif %}
{% if upcoming_campaigns %} {% if upcoming_campaigns %}
<h5>Upcoming Campaigns</h5> <h5 id="upcoming-campaigns-header">Upcoming Campaigns</h5>
<table> <table id="upcoming-campaigns-table">
<tbody> <tbody>
{% for campaign in upcoming_campaigns %} {% for campaign in upcoming_campaigns %}
<tr> <tr id="campaign-row-{{ campaign.id }}">
<td> <td>
<a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a> <a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a>
</td> </td>
@ -65,11 +80,11 @@
</table> </table>
{% endif %} {% endif %}
{% if expired_campaigns %} {% if expired_campaigns %}
<h5>Past Campaigns</h5> <h5 id="expired-campaigns-header">Past Campaigns</h5>
<table> <table id="expired-campaigns-table">
<tbody> <tbody>
{% for campaign in expired_campaigns %} {% for campaign in expired_campaigns %}
<tr> <tr id="campaign-row-{{ campaign.id }}">
<td> <td>
<a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a> <a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a>
</td> </td>
@ -82,7 +97,7 @@
</table> </table>
{% endif %} {% endif %}
{% if not active_campaigns and not upcoming_campaigns and not expired_campaigns %} {% if not active_campaigns and not upcoming_campaigns and not expired_campaigns %}
No campaigns found for this game. <p id="no-campaigns-message">No campaigns found for this game.</p>
{% endif %} {% endif %}
<pre><code>{{ game_data }}</code></pre> {{ game_data|safe }}
{% endblock content %} {% endblock content %}

View file

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<main> <main>
<header> <header>
<h1>All Games</h1> <h1 id="page-title">All Games</h1>
<p>Browse all available games</p> <p>Browse all available games</p>
<p> <p>
<a href="{% url 'twitch:game_list_simple' %}">List View</a> <a href="{% url 'twitch:game_list_simple' %}">List View</a>
@ -16,7 +16,8 @@
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;"> <div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
{% for organization, games in games_by_org.items %} {% for organization, games in games_by_org.items %}
{% for item in games %} {% for item in games %}
<article style="padding: 0.25rem; <article id="game-{{ item.game.id }}"
style="padding: 0.25rem;
border-radius: 8px; border-radius: 8px;
flex: 1 1 160px; flex: 1 1 160px;
text-align: center"> text-align: center">

View file

@ -4,16 +4,16 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<main> <main>
<h1>Games List</h1> <h1 id="page-title">Games List</h1>
<p> <p>
<a href="{% url 'twitch:game_list' %}">Grid View</a> <a href="{% url 'twitch:game_list' %}">Grid View</a>
</p> </p>
{% if games_by_org %} {% if games_by_org %}
{% for organization, games in games_by_org.items %} {% for organization, games in games_by_org.items %}
<h2>{{ organization.name }}</h2> <h2 id="org-{{ organization.id }}">{{ organization.name }}</h2>
<ul style="list-style: none; padding: 0; margin: 0;"> <ul style="list-style: none; padding: 0; margin: 0;">
{% for item in games %} {% for item in games %}
<li> <li id="game-{{ item.game.id }}">
<a href="{% url 'twitch:game_detail' item.game.id %}">{{ item.game.display_name }}</a> <a href="{% url 'twitch:game_detail' item.game.id %}">{{ item.game.display_name }}</a>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -3,16 +3,16 @@
Games Games
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h1>Organizations</h1> <h1 id="page-title">Organizations</h1>
{% if orgs %} {% if orgs %}
<ul> <ul id="org-list">
{% for organization in orgs %} {% for organization in orgs %}
<li> <li id="org-{{ organization.id }}">
<a href="{% url 'twitch:organization_detail' organization.id %}">{{ organization.name }}</a> <a href="{% url 'twitch:organization_detail' organization.id %}">{{ organization.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
No games found. <p>No games found.</p>
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View file

@ -3,9 +3,10 @@
{{ organization.name }} {{ organization.name }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h1>{{ organization.name }}</h1> <h1 id="org-name">{{ organization.name }}</h1>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<form method="post" <form id="notification-form"
method="post"
action="{% url 'twitch:subscribe_org_notifications' org_id=organization.id %}"> action="{% url 'twitch:subscribe_org_notifications' org_id=organization.id %}">
{% csrf_token %} {% csrf_token %}
<div> <div>
@ -22,17 +23,17 @@
{% if subscription and subscription.notify_live %}checked{% endif %} /> {% if subscription and subscription.notify_live %}checked{% endif %} />
<label for="live">🎮 Get notified when the drop is live and ready to be farmed.</label> <label for="live">🎮 Get notified when the drop is live and ready to be farmed.</label>
</div> </div>
<button type="submit">Save preferences</button> <button id="save-preferences-button" type="submit">Save preferences</button>
</form> </form>
{% else %} {% else %}
Login to subscribe! <p id="login-prompt">Login to subscribe!</p>
{% endif %} {% endif %}
<ul> <ul id="games-list">
{% for game in games %} {% for game in games %}
<li> <li id="game-{{ game.id }}">
<a href="{% url 'twitch:game_detail' pk=game.id %}">{{ game }}</a> <a href="{% url 'twitch:game_detail' pk=game.id %}">{{ game }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<pre><code>{{ org_data }}</code></pre> <pre><code id="org-data">{{ org_data }}</code></pre>
{% endblock content %} {% endblock content %}

View file

@ -3,55 +3,55 @@
Search Results for "{{ query }}" Search Results for "{{ query }}"
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="container"> <div class="container" id="search-results-container">
<h1>Search Results for "{{ query }}"</h1> <h1 id="page-title">Search Results for "{{ query }}"</h1>
{% if not results.organizations and not results.games and not results.campaigns and not results.drops and not results.benefits %} {% if not results.organizations and not results.games and not results.campaigns and not results.drops and not results.benefits %}
<p>No results found.</p> <p id="no-results">No results found.</p>
{% else %} {% else %}
{% if results.organizations %} {% if results.organizations %}
<h2>Organizations</h2> <h2 id="organizations-header">Organizations</h2>
<ul> <ul id="organizations-list">
{% for org in results.organizations %} {% for org in results.organizations %}
<li> <li id="org-{{ org.pk }}">
<a href="{% url 'twitch:organization_detail' org.pk %}">{{ org.name }}</a> <a href="{% url 'twitch:organization_detail' org.pk %}">{{ org.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if results.games %} {% if results.games %}
<h2>Games</h2> <h2 id="games-header">Games</h2>
<ul> <ul id="games-list">
{% for game in results.games %} {% for game in results.games %}
<li> <li id="game-{{ game.pk }}">
<a href="{% url 'twitch:game_detail' game.pk %}">{{ game.display_name }}</a> <a href="{% url 'twitch:game_detail' game.pk %}">{{ game.display_name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if results.campaigns %} {% if results.campaigns %}
<h2>Campaigns</h2> <h2 id="campaigns-header">Campaigns</h2>
<ul> <ul id="campaigns-list">
{% for campaign in results.campaigns %} {% for campaign in results.campaigns %}
<li> <li id="campaign-{{ campaign.pk }}">
<a href="{% url 'twitch:campaign_detail' campaign.pk %}">{{ campaign.name }}</a> <a href="{% url 'twitch:campaign_detail' campaign.pk %}">{{ campaign.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if results.drops %} {% if results.drops %}
<h2>Drops</h2> <h2 id="drops-header">Drops</h2>
<ul> <ul id="drops-list">
{% for drop in results.drops %} {% for drop in results.drops %}
<li> <li id="drop-{{ drop.id }}">
<a href="{% url 'twitch:campaign_detail' drop.campaign.pk %}#drop-{{ drop.id }}">{{ drop.name }}</a> (in {{ drop.campaign.name }}) <a href="{% url 'twitch:campaign_detail' drop.campaign.pk %}#drop-{{ drop.id }}">{{ drop.name }}</a> (in {{ drop.campaign.name }})
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if results.benefits %} {% if results.benefits %}
<h2>Benefits</h2> <h2 id="benefits-header">Benefits</h2>
<ul> <ul id="benefits-list">
{% for benefit in results.benefits %}<li>{{ benefit.name }}</li>{% endfor %} {% for benefit in results.benefits %}<li id="benefit-{{ benefit.id }}">{{ benefit.name }}</li>{% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% endif %} {% endif %}

View file

@ -4,7 +4,7 @@ import datetime
import json import json
import logging import logging
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@ -18,6 +18,9 @@ from django.http.response import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone from django.utils import timezone
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers.data import JsonLexer
from twitch.models import DropBenefit, DropCampaign, Game, NotificationSubscription, Organization, TimeBasedDrop from twitch.models import DropBenefit, DropCampaign, Game, NotificationSubscription, Organization, TimeBasedDrop
@ -185,7 +188,6 @@ class DropCampaignListView(ListView):
Returns: Returns:
dict: Context data. dict: Context data.
""" """
kwargs = cast("dict[str, Any]", kwargs)
context: dict[str, Any] = super().get_context_data(**kwargs) context: dict[str, Any] = super().get_context_data(**kwargs)
context["games"] = Game.objects.all().order_by("display_name") context["games"] = Game.objects.all().order_by("display_name")
@ -198,6 +200,19 @@ class DropCampaignListView(ListView):
return context return context
def format_and_color_json(code: str) -> str:
"""Format and color a JSON string for HTML display.
Args:
code: The code string to format.
Returns:
str: The formatted code with HTML styles.
"""
formatted_code: str = json.dumps(code, indent=4)
return highlight(formatted_code, JsonLexer(), HtmlFormatter())
class DropCampaignDetailView(DetailView): class DropCampaignDetailView(DetailView):
"""Detail view for a drop campaign.""" """Detail view for a drop campaign."""
@ -221,7 +236,7 @@ class DropCampaignDetailView(DetailView):
return super().get_object(queryset=queryset) return super().get_object(queryset=queryset)
def get_context_data(self, **kwargs: object) -> dict[str, Any]: def get_context_data(self, **kwargs: object) -> dict[str, Any]: # noqa: PLR0914
"""Add additional context data. """Add additional context data.
Args: Args:
@ -232,7 +247,9 @@ class DropCampaignDetailView(DetailView):
""" """
context: dict[str, Any] = super().get_context_data(**kwargs) context: dict[str, Any] = super().get_context_data(**kwargs)
campaign = context["campaign"] campaign = context["campaign"]
drops = TimeBasedDrop.objects.filter(campaign=campaign).select_related("campaign").prefetch_related("benefits").order_by("required_minutes_watched") drops: QuerySet[TimeBasedDrop, TimeBasedDrop] = (
TimeBasedDrop.objects.filter(campaign=campaign).select_related("campaign").prefetch_related("benefits").order_by("required_minutes_watched")
)
serialized_campaign = serialize( serialized_campaign = serialize(
"json", "json",
@ -280,11 +297,46 @@ class DropCampaignDetailView(DetailView):
campaign_data[0]["fields"]["drops"] = drops_data campaign_data[0]["fields"]["drops"] = drops_data
pretty_campaign_data = json.dumps(campaign_data[0], indent=4) # Enhance drops with additional context data
enhanced_drops = []
now: datetime.datetime = timezone.now()
for drop in drops:
# Ensure benefits are loaded
benefits = list(drop.benefits.all())
context["now"] = timezone.now() # Calculate countdown text
context["drops"] = drops if drop.end_at and drop.end_at > now:
context["campaign_data"] = pretty_campaign_data time_diff: datetime.timedelta = drop.end_at - now
days: int = time_diff.days
hours, remainder = divmod(time_diff.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
countdown_text: str = f"{days}d {hours}h {minutes}m"
elif hours > 0:
countdown_text = f"{hours}h {minutes}m"
elif minutes > 0:
countdown_text = f"{minutes}m {seconds}s"
else:
countdown_text = f"{seconds}s"
elif drop.start_at and drop.start_at > now:
countdown_text = "Not started"
else:
countdown_text = "Expired"
enhanced_drop: dict[str, str | datetime.datetime | TimeBasedDrop] = {
"drop": drop,
"local_start": drop.start_at,
"local_end": drop.end_at,
"timezone_name": "UTC",
"countdown_text": countdown_text,
}
enhanced_drops.append(enhanced_drop)
context["now"] = now
context["drops"] = enhanced_drops
context["campaign_data"] = format_and_color_json(campaign_data[0])
context["owner"] = campaign.game.owner
return context return context
@ -432,8 +484,6 @@ class GameDetailView(DetailView):
campaigns_data = json.loads(serialized_campaigns) campaigns_data = json.loads(serialized_campaigns)
game_data[0]["fields"]["campaigns"] = campaigns_data game_data[0]["fields"]["campaigns"] = campaigns_data
pretty_game_data = json.dumps(game_data[0], indent=4)
context.update({ context.update({
"active_campaigns": active_campaigns, "active_campaigns": active_campaigns,
"upcoming_campaigns": upcoming_campaigns, "upcoming_campaigns": upcoming_campaigns,
@ -441,7 +491,7 @@ class GameDetailView(DetailView):
"subscription": subscription, "subscription": subscription,
"owner": game.owner, "owner": game.owner,
"now": now, "now": now,
"game_data": pretty_game_data, "game_data": format_and_color_json(game_data[0]),
}) })
return context return context

2
uv.lock generated
View file

@ -599,6 +599,7 @@ dependencies = [
{ name = "orjson" }, { name = "orjson" },
{ name = "platformdirs" }, { name = "platformdirs" },
{ name = "psycopg", extra = ["binary"] }, { name = "psycopg", extra = ["binary"] },
{ name = "pygments" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
] ]
@ -622,6 +623,7 @@ requires-dist = [
{ name = "orjson", specifier = ">=3.11.1" }, { name = "orjson", specifier = ">=3.11.1" },
{ name = "platformdirs", specifier = ">=4.3.8" }, { name = "platformdirs", specifier = ">=4.3.8" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.3" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.3" },
{ name = "pygments", specifier = ">=2.19.2" },
{ name = "python-dotenv", specifier = ">=1.1.1" }, { name = "python-dotenv", specifier = ">=1.1.1" },
] ]