Add ids to tags; use pygments to color JSON
This commit is contained in:
parent
f83fee99f3
commit
8f438aca2d
18 changed files with 365 additions and 211 deletions
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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' }}"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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
2
uv.lock
generated
|
|
@ -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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue