Add rewards to more pages as we have support for them now
All checks were successful
Deploy to Server / deploy (push) Successful in 26s
All checks were successful
Deploy to Server / deploy (push) Successful in 26s
This commit is contained in:
parent
3535d7d2dd
commit
37c1390100
16 changed files with 966 additions and 216 deletions
19
core/urls.py
19
core/urls.py
|
|
@ -17,6 +17,9 @@ from twitch.feeds import GameCampaignDiscordFeed
|
|||
from twitch.feeds import GameCampaignFeed
|
||||
from twitch.feeds import GameDiscordFeed
|
||||
from twitch.feeds import GameFeed
|
||||
from twitch.feeds import GameRewardCampaignAtomFeed
|
||||
from twitch.feeds import GameRewardCampaignDiscordFeed
|
||||
from twitch.feeds import GameRewardCampaignFeed
|
||||
from twitch.feeds import OrganizationAtomFeed
|
||||
from twitch.feeds import OrganizationDiscordFeed
|
||||
from twitch.feeds import OrganizationRSSFeed
|
||||
|
|
@ -93,6 +96,12 @@ urlpatterns: list[URLPattern | URLResolver] = [
|
|||
view=OrganizationRSSFeed(),
|
||||
name="organization_feed",
|
||||
),
|
||||
# /rss/games/<twitch_id>/rewards/ - active reward campaigns for a specific game
|
||||
path(
|
||||
route="rss/games/<str:twitch_id>/rewards/",
|
||||
view=GameRewardCampaignFeed(),
|
||||
name="game_reward_feed",
|
||||
),
|
||||
# /rss/reward-campaigns/ - all active reward campaigns
|
||||
path(
|
||||
route="rss/reward-campaigns/",
|
||||
|
|
@ -120,6 +129,11 @@ urlpatterns: list[URLPattern | URLResolver] = [
|
|||
view=OrganizationAtomFeed(),
|
||||
name="organization_feed_atom",
|
||||
),
|
||||
path(
|
||||
route="atom/games/<str:twitch_id>/rewards/",
|
||||
view=GameRewardCampaignAtomFeed(),
|
||||
name="game_reward_feed_atom",
|
||||
),
|
||||
path(
|
||||
route="atom/reward-campaigns/",
|
||||
view=RewardCampaignAtomFeed(),
|
||||
|
|
@ -146,6 +160,11 @@ urlpatterns: list[URLPattern | URLResolver] = [
|
|||
view=OrganizationDiscordFeed(),
|
||||
name="organization_feed_discord",
|
||||
),
|
||||
path(
|
||||
route="discord/games/<str:twitch_id>/rewards/",
|
||||
view=GameRewardCampaignDiscordFeed(),
|
||||
name="game_reward_feed_discord",
|
||||
),
|
||||
path(
|
||||
route="discord/reward-campaigns/",
|
||||
view=RewardCampaignDiscordFeed(),
|
||||
|
|
|
|||
|
|
@ -263,7 +263,8 @@
|
|||
<br />
|
||||
CC0; Information wants to be free.
|
||||
<br />
|
||||
Data retrival possible because of <a href="https://github.com/DevilXD/TwitchDropsMiner">DevilXD/TwitchDropsMiner</a>.
|
||||
Data retrival possible because of <a href="https://github.com/DevilXD/TwitchDropsMiner">DevilXD/TwitchDropsMiner</a>
|
||||
and <a href="https://github.com/SunkwiBOT/twitch-drops-api">SunkwiBOT/twitch-drops-api</a>.
|
||||
<br />
|
||||
Data fetched at :01, :16, :31, and :46.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -38,72 +38,81 @@
|
|||
</div>
|
||||
<!-- Campaign description -->
|
||||
<p>{{ campaign.description|linebreaksbr }}</p>
|
||||
<div>
|
||||
Published
|
||||
{% if campaign.scraped_at %}
|
||||
<time datetime="{{ campaign.scraped_at|date:'c' }}"
|
||||
title="{{ campaign.scraped_at|date:'DATETIME_FORMAT' }}">{{ campaign.scraped_at|date:"M d, Y H:i" }}</time> ({{ campaign.scraped_at|timesince }} ago)
|
||||
{% elif campaign.start_date %}
|
||||
<time datetime="{{ campaign.start_date|date:'c' }}"
|
||||
title="{{ campaign.start_date|date:'DATETIME_FORMAT' }}">{{ campaign.start_date|date:"M d, Y H:i" }}</time> ({{ campaign.start_date|timesince }} ago)
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
Last updated
|
||||
{% if campaign.scraped_at %}
|
||||
<time datetime="{{ campaign.scraped_at|date:'c' }}"
|
||||
title="{{ campaign.scraped_at|date:'DATETIME_FORMAT' }}">{{ campaign.scraped_at|date:"M d, Y H:i" }}</time> ({{ campaign.scraped_at|timesince }} ago)
|
||||
{% elif campaign.updated_at %}
|
||||
<time datetime="{{ campaign.updated_at|date:'c' }}"
|
||||
title="{{ campaign.updated_at|date:'DATETIME_FORMAT' }}">{{ campaign.updated_at|date:"M d, Y H:i" }}</time> ({{ campaign.updated_at|timesince }} ago)
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Campaign end times -->
|
||||
<div>
|
||||
{% if campaign.end_date %}
|
||||
{% if campaign.end_date < now %}
|
||||
Ended
|
||||
<time datetime="{{ campaign.end_date|date:'c' }}"
|
||||
title="{{ campaign.end_date|date:'DATETIME_FORMAT' }}">{{ campaign.end_date|date:"M d, Y H:i" }}</time> ({{ campaign.end_date|timesince }} ago)
|
||||
{% else %}
|
||||
Ends in
|
||||
<time datetime="{{ campaign.end_date|date:'c' }}"
|
||||
title="{{ campaign.end_date|date:'DATETIME_FORMAT' }}">{{ campaign.end_date|date:"M d, Y H:i" }}</time> (in {{ campaign.end_date|timeuntil }})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Ends unknown
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Campaign start times -->
|
||||
<div>
|
||||
{% if campaign.start_date %}
|
||||
{% if campaign.start_date > now %}
|
||||
Starts in
|
||||
<time datetime="{{ campaign.start_date|date:'c' }}"
|
||||
title="{{ campaign.start_date|date:'DATETIME_FORMAT' }}">{{ campaign.start_date|date:"M d, Y H:i" }}</time> (in {{ campaign.start_date|timeuntil }})
|
||||
{% else %}
|
||||
Started
|
||||
<dl>
|
||||
<dt>Published</dt>
|
||||
<dd>
|
||||
{% if campaign.scraped_at %}
|
||||
<time datetime="{{ campaign.scraped_at|date:'c' }}"
|
||||
title="{{ campaign.scraped_at|date:'DATETIME_FORMAT' }}">{{ campaign.scraped_at|date:"M d, Y H:i" }}</time> ({{ campaign.scraped_at|timesince }} ago)
|
||||
{% elif campaign.start_date %}
|
||||
<time datetime="{{ campaign.start_date|date:'c' }}"
|
||||
title="{{ campaign.start_date|date:'DATETIME_FORMAT' }}">{{ campaign.start_date|date:"M d, Y H:i" }}</time> ({{ campaign.start_date|timesince }} ago)
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Starts unknown
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Campaign duration -->
|
||||
<div>
|
||||
Duration is
|
||||
{% if campaign.start_date and campaign.end_date %}
|
||||
<time datetime="P{{ campaign.end_date|date:'Y' }}-{{ campaign.end_date|date:'m' }}-{{ campaign.end_date|date:'d' }}T{{ campaign.end_date|date:'H' }}:00:00"
|
||||
title="{{ campaign.start_date|date:'DATETIME_FORMAT' }} to {{ campaign.end_date|date:'DATETIME_FORMAT' }}">{{ campaign.end_date|timeuntil:campaign.start_date }}</time>
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</div>
|
||||
</dd>
|
||||
<dt>Last updated</dt>
|
||||
<dd>
|
||||
{% if campaign.scraped_at %}
|
||||
<time datetime="{{ campaign.scraped_at|date:'c' }}"
|
||||
title="{{ campaign.scraped_at|date:'DATETIME_FORMAT' }}">{{ campaign.scraped_at|date:"M d, Y H:i" }}</time> ({{ campaign.scraped_at|timesince }} ago)
|
||||
{% elif campaign.updated_at %}
|
||||
<time datetime="{{ campaign.updated_at|date:'c' }}"
|
||||
title="{{ campaign.updated_at|date:'DATETIME_FORMAT' }}">{{ campaign.updated_at|date:"M d, Y H:i" }}</time> ({{ campaign.updated_at|timesince }} ago)
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>
|
||||
{% if campaign.end_date and campaign.end_date < now %}
|
||||
Ended
|
||||
{% else %}
|
||||
Ends
|
||||
{% endif %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if campaign.end_date %}
|
||||
<time datetime="{{ campaign.end_date|date:'c' }}"
|
||||
title="{{ campaign.end_date|date:'DATETIME_FORMAT' }}">{{ campaign.end_date|date:"M d, Y H:i" }}</time>
|
||||
{% if campaign.end_date < now %}
|
||||
({{ campaign.end_date|timesince }} ago)
|
||||
{% else %}
|
||||
(in {{ campaign.end_date|timeuntil }})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>
|
||||
{% if campaign.start_date and campaign.start_date > now %}
|
||||
Starts
|
||||
{% else %}
|
||||
Started
|
||||
{% endif %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if campaign.start_date %}
|
||||
<time datetime="{{ campaign.start_date|date:'c' }}"
|
||||
title="{{ campaign.start_date|date:'DATETIME_FORMAT' }}">{{ campaign.start_date|date:"M d, Y H:i" }}</time>
|
||||
{% if campaign.start_date > now %}
|
||||
(in {{ campaign.start_date|timeuntil }})
|
||||
{% else %}
|
||||
({{ campaign.start_date|timesince }} ago)
|
||||
{% endif %}
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>Duration</dt>
|
||||
<dd>
|
||||
{% if campaign.start_date and campaign.end_date %}
|
||||
<time datetime="P{{ campaign.end_date|date:'Y' }}-{{ campaign.end_date|date:'m' }}-{{ campaign.end_date|date:'d' }}T{{ campaign.end_date|date:'H' }}:00:00"
|
||||
title="{{ campaign.start_date|date:'DATETIME_FORMAT' }} to {{ campaign.end_date|date:'DATETIME_FORMAT' }}">{{ campaign.end_date|timeuntil:campaign.start_date }}</time>
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
<!-- Buttons -->
|
||||
<div>
|
||||
{% if campaign.pc_link_url %}<a href="{{ campaign.pc_link_url }}" target="_blank">[details]</a>{% endif %}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,10 @@
|
|||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<main>
|
||||
<h1>Active Drops Dashboard</h1>
|
||||
<h1>Open Drop Campaigns</h1>
|
||||
<p>
|
||||
A combined overview of currently active Twitch and Kick drops campaigns.
|
||||
<br />
|
||||
Click any campaign to open details.
|
||||
Some Drops campaigns may not be available in your region.
|
||||
<a href="https://help.twitch.tv/s/article/mission-based-drops">Learn more about Drops</a>.
|
||||
</p>
|
||||
<hr />
|
||||
<section id="twitch-campaigns-section">
|
||||
|
|
@ -173,32 +172,39 @@
|
|||
padding-top: 1rem">
|
||||
<header style="margin-bottom: 1rem;">
|
||||
<h2 style="margin: 0 0 0.5rem 0;">
|
||||
<a href="{% url 'twitch:reward_campaign_list' %}">Twitch Reward Campaigns (Quest Rewards)</a>
|
||||
<a href="{% url 'twitch:reward_campaign_list' %}">Open Reward Campaigns</a>
|
||||
</h2>
|
||||
<p>
|
||||
Rewards are limited-time offers from top brands that you can unlock by
|
||||
supporting your favorite Twitch creators. Visit <a href="https://www.twitch.tv/drops/inventory">your inventory</a>
|
||||
to access unlocked rewards.
|
||||
</p>
|
||||
</header>
|
||||
<div style="display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
|
||||
<ul>
|
||||
{% for campaign in active_reward_campaigns %}
|
||||
<article id="reward-campaign-{{ campaign.twitch_id }}"
|
||||
style="border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1rem">
|
||||
<h3 style="margin: 0 0 0.5rem 0;">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}">
|
||||
{% if campaign.brand %}
|
||||
{{ campaign.brand }}: {{ campaign.name }}
|
||||
{% else %}
|
||||
{{ campaign.name }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</h3>
|
||||
{% if campaign.summary %}
|
||||
<p style="font-size: 0.9rem; color: #555; margin: 0.5rem 0;">{{ campaign.summary }}</p>
|
||||
{% endif %}
|
||||
<div style="font-size: 0.85rem; color: #666;">
|
||||
<li id="reward-campaign-{{ campaign.twitch_id }}"
|
||||
style="display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: flex-start">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}"
|
||||
style="flex-shrink: 0;
|
||||
display: block;
|
||||
line-height: 0">
|
||||
{% picture campaign.image_best_url alt="Image for "|add:campaign.name width=120 %}
|
||||
</a>
|
||||
<div>
|
||||
<h3 style="margin: 0;">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}">
|
||||
{% if campaign.brand %}
|
||||
{{ campaign.brand }}: {{ campaign.name }}
|
||||
{% else %}
|
||||
{{ campaign.name }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</h3>
|
||||
{% if campaign.ends_at %}
|
||||
<p style="margin: 0.25rem 0;">
|
||||
<p>
|
||||
<strong>Ends:</strong>
|
||||
<time datetime="{{ campaign.ends_at|date:'c' }}"
|
||||
title="{{ campaign.ends_at|date:'DATETIME_FORMAT' }}">
|
||||
|
|
@ -206,20 +212,25 @@
|
|||
</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if campaign.summary %}
|
||||
<p>{{ campaign.summary }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{% if campaign.game %}
|
||||
<p style="margin: 0.25rem 0;">
|
||||
<p>
|
||||
<strong>Game:</strong>
|
||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.display_name }}</a>
|
||||
</p>
|
||||
{% elif campaign.is_sitewide %}
|
||||
<p style="margin: 0.25rem 0;">
|
||||
<p>
|
||||
<strong>Type:</strong> Site-wide reward campaign
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
<section id="kick-campaigns-section"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@
|
|||
upstream content and cannot guarantee upstream accuracy or permanence.
|
||||
</p>
|
||||
<p>Note that some drops has missing or incomplete data due to Twitch API limitations.</p>
|
||||
<p>
|
||||
Reward campaign data is sourced from
|
||||
<a href="https://github.com/SunkwiBOT/twitch-drops-api">SunkwiBOT/twitch-drops-api</a>.
|
||||
Raw JSON exports are available in the repository:
|
||||
<a href="https://github.com/SunkwiBOT/twitch-drops-api/blob/main/drops.json">drops.json</a>
|
||||
and
|
||||
<a href="https://github.com/SunkwiBOT/twitch-drops-api/blob/main/rewards.json">rewards.json</a>.
|
||||
</p>
|
||||
<p>
|
||||
Need a special format for your workflow or research pipeline?
|
||||
<a href="https://github.com/TheLovinator1/ttvdrops/issues">Contact me via GitHub issues</a>
|
||||
|
|
|
|||
|
|
@ -127,11 +127,15 @@
|
|||
{% if game %}
|
||||
<section>
|
||||
<h2>Filtered RSS Feeds</h2>
|
||||
<p>You can subscribe to RSS feeds scoped to a specific game.</p>
|
||||
<p>
|
||||
You can subscribe to RSS feeds scoped to a specific game.
|
||||
The Rewards feed will be empty if the game has no active reward campaigns.
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Game</th>
|
||||
<th>Feed Type</th>
|
||||
<th>RSS</th>
|
||||
<th>Atom</th>
|
||||
<th>Discord</th>
|
||||
|
|
@ -139,20 +143,39 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ game.display_name }}</td>
|
||||
<td rowspan="2">{{ game.display_name }}</td>
|
||||
<td>Campaigns</td>
|
||||
<td>
|
||||
<a href="{% url 'core:game_campaign_feed' game.twitch_id %}">
|
||||
https://ttvdrops.lovinator.space/rss/games/{{ game.twitch_id }}/
|
||||
https://ttvdrops.lovinator.space/rss/games/{{ game.twitch_id }}/campaigns/
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'core:game_campaign_feed_atom' game.twitch_id %}">
|
||||
https://ttvdrops.lovinator.space/atom/games/{{ game.twitch_id }}/
|
||||
https://ttvdrops.lovinator.space/atom/games/{{ game.twitch_id }}/campaigns/
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'core:game_campaign_feed_discord' game.twitch_id %}">
|
||||
https://ttvdrops.lovinator.space/discord/games/{{ game.twitch_id }}/
|
||||
https://ttvdrops.lovinator.space/discord/games/{{ game.twitch_id }}/campaigns/
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rewards</td>
|
||||
<td>
|
||||
<a href="{% url 'core:game_reward_feed' game.twitch_id %}">
|
||||
https://ttvdrops.lovinator.space/rss/games/{{ game.twitch_id }}/rewards/
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'core:game_reward_feed_atom' game.twitch_id %}">
|
||||
https://ttvdrops.lovinator.space/atom/games/{{ game.twitch_id }}/rewards/
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'core:game_reward_feed_discord' game.twitch_id %}">
|
||||
https://ttvdrops.lovinator.space/discord/games/{{ game.twitch_id }}/rewards/
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -51,61 +51,74 @@
|
|||
<a href="{% url 'kick:campaign_list' %}">Campaigns</a> >
|
||||
{{ campaign.name }}
|
||||
</div>
|
||||
<div>
|
||||
<dl>
|
||||
{% if campaign.category %}
|
||||
Category:
|
||||
<a href="{% url 'kick:game_detail' campaign.category.kick_id %}">{{ campaign.category.name }}</a>
|
||||
<dt>Category</dt>
|
||||
<dd>
|
||||
<a href="{% url 'kick:game_detail' campaign.category.kick_id %}">{{ campaign.category.name }}</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{% if campaign.organization %}
|
||||
Organization:
|
||||
<a href="{% url 'kick:organization_detail' campaign.organization.kick_id %}">{{ campaign.organization.name }}</a>
|
||||
<dt>Organization</dt>
|
||||
<dd>
|
||||
<a href="{% url 'kick:organization_detail' campaign.organization.kick_id %}">{{ campaign.organization.name }}</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if campaign.ends_at %}
|
||||
<div>
|
||||
{% if campaign.ends_at > now %}
|
||||
Ends
|
||||
{% else %}
|
||||
Ended at
|
||||
{% endif %}
|
||||
<time datetime="{{ campaign.ends_at|date:'c' }}"
|
||||
title="{{ campaign.ends_at|date:'DATETIME_FORMAT' }}">{{ campaign.ends_at|date:"M d, Y H:i" }}</time>
|
||||
{% if campaign.ends_at < now %}
|
||||
(ended {{ campaign.ends_at|timesince }} ago)
|
||||
{% else %}
|
||||
(in {{ campaign.ends_at|timeuntil }})
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if campaign.starts_at %}
|
||||
<div>
|
||||
{% if campaign.starts_at > now %}
|
||||
Starts
|
||||
{% else %}
|
||||
Started at
|
||||
{% endif %}
|
||||
<time datetime="{{ campaign.starts_at|date:'c' }}"
|
||||
title="{{ campaign.starts_at|date:'DATETIME_FORMAT' }}">{{ campaign.starts_at|date:"M d, Y H:i" }}</time>
|
||||
{% if campaign.starts_at < now %}
|
||||
(started {{ campaign.starts_at|timesince }} ago)
|
||||
{% else %}
|
||||
(in {{ campaign.starts_at|timeuntil }})
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if campaign.duration %}<div>Duration: {{ campaign.duration }}</div>{% endif %}
|
||||
<div>
|
||||
Added <time datetime="{{ campaign.added_at|date:'c' }}"
|
||||
title="{{ campaign.added_at|date:'DATETIME_FORMAT' }}">{{ campaign.added_at|date:"M d, Y" }}</time>
|
||||
({{ campaign.added_at|timesince }} ago)
|
||||
</div>
|
||||
<div>
|
||||
Updated <time datetime="{{ campaign.updated_at|date:'c' }}"
|
||||
title="{{ campaign.updated_at|date:'DATETIME_FORMAT' }}">{{ campaign.updated_at|date:"M d, Y" }}</time>
|
||||
({{ campaign.updated_at|timesince }} ago)
|
||||
</div>
|
||||
{% if campaign.ends_at %}
|
||||
<dt>
|
||||
{% if campaign.ends_at > now %}
|
||||
Ends
|
||||
{% else %}
|
||||
Ended at
|
||||
{% endif %}
|
||||
</dt>
|
||||
<dd>
|
||||
<time datetime="{{ campaign.ends_at|date:'c' }}"
|
||||
title="{{ campaign.ends_at|date:'DATETIME_FORMAT' }}">{{ campaign.ends_at|date:"M d, Y H:i" }}</time>
|
||||
{% if campaign.ends_at < now %}
|
||||
(ended {{ campaign.ends_at|timesince }} ago)
|
||||
{% else %}
|
||||
(in {{ campaign.ends_at|timeuntil }})
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if campaign.starts_at %}
|
||||
<dt>
|
||||
{% if campaign.starts_at > now %}
|
||||
Starts
|
||||
{% else %}
|
||||
Started at
|
||||
{% endif %}
|
||||
</dt>
|
||||
<dd>
|
||||
<time datetime="{{ campaign.starts_at|date:'c' }}"
|
||||
title="{{ campaign.starts_at|date:'DATETIME_FORMAT' }}">{{ campaign.starts_at|date:"M d, Y H:i" }}</time>
|
||||
{% if campaign.starts_at < now %}
|
||||
(started {{ campaign.starts_at|timesince }} ago)
|
||||
{% else %}
|
||||
(in {{ campaign.starts_at|timeuntil }})
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if campaign.duration %}
|
||||
<dt>Duration</dt>
|
||||
<dd>
|
||||
{{ campaign.duration }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
<dt>Added</dt>
|
||||
<dd>
|
||||
<time datetime="{{ campaign.added_at|date:'c' }}"
|
||||
title="{{ campaign.added_at|date:'DATETIME_FORMAT' }}">{{ campaign.added_at|date:"M d, Y" }}</time>
|
||||
({{ campaign.added_at|timesince }} ago)
|
||||
</dd>
|
||||
<dt>Updated</dt>
|
||||
<dd>
|
||||
<time datetime="{{ campaign.updated_at|date:'c' }}"
|
||||
title="{{ campaign.updated_at|date:'DATETIME_FORMAT' }}">{{ campaign.updated_at|date:"M d, Y" }}</time>
|
||||
({{ campaign.updated_at|timesince }} ago)
|
||||
</dd>
|
||||
</dl>
|
||||
<div>
|
||||
{% if campaign.channels.all %}
|
||||
Participating channels:
|
||||
|
|
|
|||
|
|
@ -63,20 +63,36 @@
|
|||
<a href="{{ category.kick_url }}">{{ category.kick_url }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p style="margin: 0.25rem 0; font-size: 0.85rem; color: #666;">
|
||||
ID: {{ category.kick_id }}
|
||||
- Added: <time datetime="{{ category.added_at|date:'c' }}"
|
||||
title="{{ category.added_at|date:'DATETIME_FORMAT' }}">{{ category.added_at|date:"M d, Y" }}</time>
|
||||
({{ category.added_at|timesince }} ago)
|
||||
- Updated: <time datetime="{{ category.updated_at|date:'c' }}"
|
||||
title="{{ category.updated_at|date:'DATETIME_FORMAT' }}">{{ category.updated_at|date:"M d, Y" }}</time>
|
||||
({{ category.updated_at|timesince }} ago)
|
||||
</p>
|
||||
<p style="margin: 0.25rem 0; color: #666;">
|
||||
Active: {{ active_campaigns|length }}
|
||||
- Upcoming: {{ upcoming_campaigns|length }}
|
||||
- Expired: {{ expired_campaigns|length }}
|
||||
</p>
|
||||
<dl>
|
||||
<dt>ID</dt>
|
||||
<dd>
|
||||
{{ category.kick_id }}
|
||||
</dd>
|
||||
<dt>Added</dt>
|
||||
<dd>
|
||||
<time datetime="{{ category.added_at|date:'c' }}"
|
||||
title="{{ category.added_at|date:'DATETIME_FORMAT' }}">{{ category.added_at|date:"M d, Y" }}</time>
|
||||
({{ category.added_at|timesince }} ago)
|
||||
</dd>
|
||||
<dt>Updated</dt>
|
||||
<dd>
|
||||
<time datetime="{{ category.updated_at|date:'c' }}"
|
||||
title="{{ category.updated_at|date:'DATETIME_FORMAT' }}">{{ category.updated_at|date:"M d, Y" }}</time>
|
||||
({{ category.updated_at|timesince }} ago)
|
||||
</dd>
|
||||
<dt>Active</dt>
|
||||
<dd>
|
||||
{{ active_campaigns|length }}
|
||||
</dd>
|
||||
<dt>Upcoming</dt>
|
||||
<dd>
|
||||
{{ upcoming_campaigns|length }}
|
||||
</dd>
|
||||
<dt>Expired</dt>
|
||||
<dd>
|
||||
{{ expired_campaigns|length }}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
|
|
|||
|
|
@ -31,15 +31,24 @@
|
|||
<a href="{{ org.url }}">{{ org.url }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p style="margin: 0.25rem 0; font-size: 0.85rem; color: #666;">
|
||||
ID: {{ org.kick_id }}
|
||||
- Added: <time datetime="{{ org.added_at|date:'c' }}"
|
||||
title="{{ org.added_at|date:'DATETIME_FORMAT' }}">{{ org.added_at|date:"M d, Y" }}</time>
|
||||
({{ org.added_at|timesince }} ago)
|
||||
- Updated: <time datetime="{{ org.updated_at|date:'c' }}"
|
||||
title="{{ org.updated_at|date:'DATETIME_FORMAT' }}">{{ org.updated_at|date:"M d, Y" }}</time>
|
||||
({{ org.updated_at|timesince }} ago)
|
||||
</p>
|
||||
<dl>
|
||||
<dt>ID</dt>
|
||||
<dd>
|
||||
{{ org.kick_id }}
|
||||
</dd>
|
||||
<dt>Added</dt>
|
||||
<dd>
|
||||
<time datetime="{{ org.added_at|date:'c' }}"
|
||||
title="{{ org.added_at|date:'DATETIME_FORMAT' }}">{{ org.added_at|date:"M d, Y" }}</time>
|
||||
({{ org.added_at|timesince }} ago)
|
||||
</dd>
|
||||
<dt>Updated</dt>
|
||||
<dd>
|
||||
<time datetime="{{ org.updated_at|date:'c' }}"
|
||||
title="{{ org.updated_at|date:'DATETIME_FORMAT' }}">{{ org.updated_at|date:"M d, Y" }}</time>
|
||||
({{ org.updated_at|timesince }} ago)
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
|
|
|||
|
|
@ -9,23 +9,29 @@
|
|||
<div>
|
||||
<a href="{% url 'twitch:dashboard' %}">Twitch</a> > <a href="{% url 'twitch:channel_list' %}">Channels</a> > {{ channel.display_name }}
|
||||
</div>
|
||||
{% if channel.display_name != channel.name %}
|
||||
<div>
|
||||
Username: <code>{{ channel.name }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Channel Info -->
|
||||
<div>Channel ID: {{ channel.twitch_id }}</div>
|
||||
<div>
|
||||
Published
|
||||
<time datetime="{{ channel.added_at|date:'c' }}"
|
||||
title="{{ channel.added_at|date:'DATETIME_FORMAT' }}">{{ channel.added_at|date:"M d, Y H:i" }}</time> ({{ channel.added_at|timesince }} ago)
|
||||
</div>
|
||||
<div>
|
||||
Last updated
|
||||
<time datetime="{{ channel.updated_at|date:'c' }}"
|
||||
title="{{ channel.updated_at|date:'DATETIME_FORMAT' }}">{{ channel.updated_at|date:"M d, Y H:i" }}</time> ({{ channel.updated_at|timesince }} ago)
|
||||
</div>
|
||||
<!-- Channel metadata -->
|
||||
<dl>
|
||||
{% if channel.display_name != channel.name %}
|
||||
<dt>Username</dt>
|
||||
<dd>
|
||||
<code>{{ channel.name }}</code>
|
||||
</dd>
|
||||
{% endif %}
|
||||
<dt>Channel ID</dt>
|
||||
<dd>
|
||||
{{ channel.twitch_id }}
|
||||
</dd>
|
||||
<dt>Published</dt>
|
||||
<dd>
|
||||
<time datetime="{{ channel.added_at|date:'c' }}"
|
||||
title="{{ channel.added_at|date:'DATETIME_FORMAT' }}">{{ channel.added_at|date:"M d, Y H:i" }}</time> ({{ channel.added_at|timesince }} ago)
|
||||
</dd>
|
||||
<dt>Last updated</dt>
|
||||
<dd>
|
||||
<time datetime="{{ channel.updated_at|date:'c' }}"
|
||||
title="{{ channel.updated_at|date:'DATETIME_FORMAT' }}">{{ channel.updated_at|date:"M d, Y H:i" }}</time> ({{ channel.updated_at|timesince }} ago)
|
||||
</dd>
|
||||
</dl>
|
||||
<!-- Twitch Stream Embed -->
|
||||
<iframe src="https://player.twitch.tv/?channel={{ channel.name }}&parent={{ request.get_host }}&muted=false"
|
||||
height="480"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,18 @@
|
|||
type="application/atom+xml"
|
||||
title="{{ game.display_name }} campaigns (Discord)"
|
||||
href="{% url 'core:game_campaign_feed_discord' game.twitch_id %}" />
|
||||
<link rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="{{ game.display_name }} rewards (RSS)"
|
||||
href="{% url 'core:game_reward_feed' game.twitch_id %}" />
|
||||
<link rel="alternate"
|
||||
type="application/atom+xml"
|
||||
title="{{ game.display_name }} rewards (Atom)"
|
||||
href="{% url 'core:game_reward_feed_atom' game.twitch_id %}" />
|
||||
<link rel="alternate"
|
||||
type="application/atom+xml"
|
||||
title="{{ game.display_name }} rewards (Discord)"
|
||||
href="{% url 'core:game_reward_feed_discord' game.twitch_id %}" />
|
||||
{% endif %}
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
|
|
@ -37,34 +49,48 @@
|
|||
<div>
|
||||
<a href="{% url 'twitch:dashboard' %}">Twitch</a> > <a href="{% url 'twitch:games_grid' %}">Games</a> > {{ game.display_name }}
|
||||
</div>
|
||||
<!-- Game owner -->
|
||||
{% if owners %}
|
||||
<div style="font-size: 0.9em;">
|
||||
Owned by
|
||||
{% for owner in owners %}
|
||||
<a href="{% url 'twitch:organization_detail' owner.twitch_id %}">{{ owner.name }}</a>
|
||||
{% if not forloop.last %}-{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
Published
|
||||
<time datetime="{{ game.added_at|date:'c' }}"
|
||||
title="{{ game.added_at|date:'DATETIME_FORMAT' }}">{{ game.added_at|date:"M d, Y H:i" }}</time> ({{ game.added_at|timesince }} ago)
|
||||
</div>
|
||||
<div>
|
||||
Last updated
|
||||
<time datetime="{{ game.updated_at|date:'c' }}"
|
||||
title="{{ game.updated_at|date:'DATETIME_FORMAT' }}">{{ game.updated_at|date:"M d, Y H:i" }}</time> ({{ game.updated_at|timesince }} ago)
|
||||
</div>
|
||||
<!-- RSS Feeds -->
|
||||
<!-- Game metadata -->
|
||||
<dl>
|
||||
{% if owners %}
|
||||
<dt>Owned by</dt>
|
||||
<dd>
|
||||
{% for owner in owners %}
|
||||
<a href="{% url 'twitch:organization_detail' owner.twitch_id %}">{{ owner.name }}</a>
|
||||
{% if not forloop.last %}-{% endif %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
<dt>Published</dt>
|
||||
<dd>
|
||||
<time datetime="{{ game.added_at|date:'c' }}"
|
||||
title="{{ game.added_at|date:'DATETIME_FORMAT' }}">{{ game.added_at|date:"M d, Y H:i" }}</time> ({{ game.added_at|timesince }} ago)
|
||||
</dd>
|
||||
<dt>Last updated</dt>
|
||||
<dd>
|
||||
<time datetime="{{ game.updated_at|date:'c' }}"
|
||||
title="{{ game.updated_at|date:'DATETIME_FORMAT' }}">{{ game.updated_at|date:"M d, Y H:i" }}</time> ({{ game.updated_at|timesince }} ago)
|
||||
</dd>
|
||||
</dl>
|
||||
<!-- Campaign Feeds -->
|
||||
<div>
|
||||
Campaigns:
|
||||
<a href="{% url 'core:game_campaign_feed' game.twitch_id %}"
|
||||
title="RSS feed for {{ game.display_name }} campaigns">[rss]</a>
|
||||
<a href="{% url 'core:game_campaign_feed_atom' game.twitch_id %}"
|
||||
title="Atom feed for {{ game.display_name }} campaigns">[atom]</a>
|
||||
<a href="{% url 'core:game_campaign_feed_discord' game.twitch_id %}"
|
||||
title="Discord feed for {{ game.display_name }} campaigns">[discord]</a>
|
||||
</div>
|
||||
<div>
|
||||
Rewards:
|
||||
<a href="{% url 'core:game_reward_feed' game.twitch_id %}"
|
||||
title="RSS feed for {{ game.display_name }} rewards">[rss]</a>
|
||||
<a href="{% url 'core:game_reward_feed_atom' game.twitch_id %}"
|
||||
title="Atom feed for {{ game.display_name }} rewards">[atom]</a>
|
||||
<a href="{% url 'core:game_reward_feed_discord' game.twitch_id %}"
|
||||
title="Discord feed for {{ game.display_name }} rewards">[discord]</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'twitch:twitch-api-v1:get_game' game.twitch_id %}"
|
||||
title="Twitch game API">[api]</a>
|
||||
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||
|
|
@ -163,4 +189,120 @@
|
|||
{% if not active_campaigns and not upcoming_campaigns and not expired_campaigns %}
|
||||
<p id="no-campaigns-message">No campaigns found for this game.</p>
|
||||
{% endif %}
|
||||
{% if active_rewards %}
|
||||
<h5 id="active-rewards-header">Active Rewards</h5>
|
||||
<table id="active-rewards-table">
|
||||
<tbody>
|
||||
{% for reward in active_rewards %}
|
||||
<tr id="reward-row-{{ reward.twitch_id }}">
|
||||
<td style="width: 130px; vertical-align: top;">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' reward.twitch_id %}">
|
||||
{% picture reward.image_best_url alt="Image for "|add:reward.name width=120 %}
|
||||
</a>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' reward.twitch_id %}">
|
||||
{% if reward.brand %}{{ reward.brand }}:{% endif %}
|
||||
{{ reward.name }}
|
||||
</a>
|
||||
{% if reward.is_sitewide %}
|
||||
<span title="Sitewide reward">[Sitewide]</span>
|
||||
{% endif %}
|
||||
{% if reward.summary %}
|
||||
<br />
|
||||
<small>{{ reward.summary|truncatewords:30|linebreaksbr }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<span title="{{ reward.ends_at|date:'M d, Y H:i' }}">Ends in {{ reward.ends_at|timeuntil }}</span>
|
||||
</td>
|
||||
<td style="vertical-align: top; white-space: nowrap;">
|
||||
{% if reward.about_url and reward.external_url and reward.about_url == reward.external_url %}
|
||||
<a href="{{ reward.about_url }}">About / Redeem</a>
|
||||
{% else %}
|
||||
{% if reward.about_url %}<a href="{{ reward.about_url }}">About</a>{% endif %}
|
||||
{% if reward.external_url %}· <a href="{{ reward.external_url }}">Redeem</a>{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if upcoming_rewards %}
|
||||
<h5 id="upcoming-rewards-header">Upcoming Rewards</h5>
|
||||
<table id="upcoming-rewards-table">
|
||||
<tbody>
|
||||
{% for reward in upcoming_rewards %}
|
||||
<tr id="reward-row-{{ reward.twitch_id }}">
|
||||
<td style="width: 130px; vertical-align: top;">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' reward.twitch_id %}">
|
||||
{% picture reward.image_best_url alt="Image for "|add:reward.name width=120 %}
|
||||
</a>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' reward.twitch_id %}">
|
||||
{% if reward.brand %}{{ reward.brand }}:{% endif %}
|
||||
{{ reward.name }}
|
||||
</a>
|
||||
{% if reward.is_sitewide %}<span title="Sitewide reward">[Sitewide]</span>{% endif %}
|
||||
{% if reward.summary %}
|
||||
<br />
|
||||
<small>{{ reward.summary|truncatewords:30|linebreaksbr }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<span title="Starts on {{ reward.starts_at|date:'M d, Y H:i' }}">Starts in {{ reward.starts_at|timeuntil }}</span>
|
||||
</td>
|
||||
<td style="vertical-align: top; white-space: nowrap;">
|
||||
{% if reward.about_url and reward.external_url and reward.about_url == reward.external_url %}
|
||||
<a href="{{ reward.about_url }}">About / Redeem</a>
|
||||
{% else %}
|
||||
{% if reward.about_url %}<a href="{{ reward.about_url }}">About</a>{% endif %}
|
||||
{% if reward.external_url %} · <a href="{{ reward.external_url }}">Redeem</a>{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if expired_rewards %}
|
||||
<h5 id="expired-rewards-header">Past Rewards</h5>
|
||||
<table id="expired-rewards-table">
|
||||
<tbody>
|
||||
{% for reward in expired_rewards %}
|
||||
<tr id="reward-row-{{ reward.twitch_id }}">
|
||||
<td style="width: 130px; vertical-align: top;">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' reward.twitch_id %}">
|
||||
{% picture reward.image_best_url alt="Image for "|add:reward.name width=120 %}
|
||||
</a>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<a href="{% url 'twitch:reward_campaign_detail' reward.twitch_id %}">
|
||||
{% if reward.brand %}{{ reward.brand }}:{% endif %}
|
||||
{{ reward.name }}
|
||||
</a>
|
||||
{% if reward.is_sitewide %}<span title="Sitewide reward">[Sitewide]</span>{% endif %}
|
||||
{% if reward.summary %}
|
||||
<br />
|
||||
<small>{{ reward.summary|truncatewords:30|linebreaksbr }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<span title="Ended on {{ reward.ends_at|date:'M d, Y H:i' }}">{{ reward.ends_at|timesince }} ago</span>
|
||||
</td>
|
||||
<td style="vertical-align: top; white-space: nowrap;">
|
||||
{% if reward.about_url and reward.external_url and reward.about_url == reward.external_url %}
|
||||
<a href="{{ reward.about_url }}">About / Redeem</a>
|
||||
{% else %}
|
||||
{% if reward.about_url %}<a href="{{ reward.about_url }}">About</a>{% endif %}
|
||||
{% if reward.external_url %} · <a href="{{ reward.external_url }}">Redeem</a>{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
|
|
|||
|
|
@ -26,16 +26,18 @@
|
|||
<div>
|
||||
<a href="{% url 'core:dashboard' %}">Dashboard</a> > <a href="{% url 'twitch:org_list' %}">Organizations</a> > {{ organization.name }}
|
||||
</div>
|
||||
<div>
|
||||
Published
|
||||
<time datetime="{{ organization.added_at|date:'c' }}"
|
||||
title="{{ organization.added_at|date:'DATETIME_FORMAT' }}">{{ organization.added_at|date:"M d, Y H:i" }}</time> ({{ organization.added_at|timesince }} ago)
|
||||
</div>
|
||||
<div>
|
||||
Last updated
|
||||
<time datetime="{{ organization.updated_at|date:'c' }}"
|
||||
title="{{ organization.updated_at|date:'DATETIME_FORMAT' }}">{{ organization.updated_at|date:"M d, Y H:i" }}</time> ({{ organization.updated_at|timesince }} ago)
|
||||
</div>
|
||||
<dl>
|
||||
<dt>Published</dt>
|
||||
<dd>
|
||||
<time datetime="{{ organization.added_at|date:'c' }}"
|
||||
title="{{ organization.added_at|date:'DATETIME_FORMAT' }}">{{ organization.added_at|date:"M d, Y H:i" }}</time> ({{ organization.added_at|timesince }} ago)
|
||||
</dd>
|
||||
<dt>Last updated</dt>
|
||||
<dd>
|
||||
<time datetime="{{ organization.updated_at|date:'c' }}"
|
||||
title="{{ organization.updated_at|date:'DATETIME_FORMAT' }}">{{ organization.updated_at|date:"M d, Y H:i" }}</time> ({{ organization.updated_at|timesince }} ago)
|
||||
</dd>
|
||||
</dl>
|
||||
<header>
|
||||
<h2>Games by {{ organization.name }}</h2>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ class V1GameDetailSchema(V1GameSchema):
|
|||
"""Twitch game detail response."""
|
||||
|
||||
campaigns: list[V1DropCampaignSummarySchema]
|
||||
reward_campaigns: list[V1RewardCampaignSchema]
|
||||
|
||||
|
||||
class V1ChannelDetailSchema(V1ChannelSchema):
|
||||
|
|
@ -642,6 +643,13 @@ def get_game(request: HttpRequest, twitch_id: str) -> V1GameDetailSchema:
|
|||
DropCampaign.objects.filter(game=game),
|
||||
).order_by("-end_at")
|
||||
now = timezone.now()
|
||||
reward_campaigns = list(
|
||||
RewardCampaign.objects
|
||||
.filter(game=game)
|
||||
.select_related("game")
|
||||
.prefetch_related("game__owners")
|
||||
.order_by("-starts_at"),
|
||||
)
|
||||
return V1GameDetailSchema(
|
||||
twitch_id=game.twitch_id,
|
||||
slug=game.slug,
|
||||
|
|
@ -659,6 +667,9 @@ def get_game(request: HttpRequest, twitch_id: str) -> V1GameDetailSchema:
|
|||
campaigns=[
|
||||
_serialize_campaign_summary(campaign, now) for campaign in campaigns
|
||||
],
|
||||
reward_campaigns=[
|
||||
_serialize_reward_campaign(rc, now) for rc in reward_campaigns
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
292
twitch/feeds.py
292
twitch/feeds.py
|
|
@ -1695,3 +1695,295 @@ class RewardCampaignDiscordFeed(TTVDropsAtomBaseFeed, RewardCampaignFeed):
|
|||
def feed_url(self) -> str:
|
||||
"""Return the URL to the Discord feed itself."""
|
||||
return reverse("core:reward_campaign_feed_discord")
|
||||
|
||||
|
||||
# MARK: /rss/games/<twitch_id>/rewards/
|
||||
class GameRewardCampaignFeed(TTVDropsBaseFeed):
|
||||
"""RSS feed for the latest reward campaigns of a specific game."""
|
||||
|
||||
item_guid_is_permalink = True
|
||||
_limit: int | None = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
*args: str | int,
|
||||
**kwargs: str | int,
|
||||
) -> HttpResponse:
|
||||
"""Override to capture limit parameter from request.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming HTTP request, potentially containing a 'limit' query parameter.
|
||||
*args: Additional positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The HTTP response generated by the parent Feed class after processing the request.
|
||||
"""
|
||||
if request.GET.get("limit"):
|
||||
try:
|
||||
self._limit = int(request.GET.get("limit", 200))
|
||||
except ValueError, TypeError:
|
||||
self._limit = None
|
||||
return super().__call__(request, *args, **kwargs)
|
||||
|
||||
def get_object(self, request: HttpRequest, twitch_id: str) -> Game: # noqa: ARG002
|
||||
"""Retrieve the Game instance for the given Twitch ID.
|
||||
|
||||
Returns:
|
||||
Game: The corresponding Game object.
|
||||
"""
|
||||
return Game.objects.get(twitch_id=twitch_id)
|
||||
|
||||
def item_link(self, item: RewardCampaign) -> str:
|
||||
"""Return the link to the reward campaign detail."""
|
||||
return reverse("twitch:reward_campaign_detail", args=[item.twitch_id])
|
||||
|
||||
def title(self, obj: Game) -> str:
|
||||
"""Return the feed title for the game reward campaigns."""
|
||||
return f"TTVDrops: {obj.display_name} Rewards"
|
||||
|
||||
def link(self, obj: Game) -> str:
|
||||
"""Return the absolute URL to the game detail page."""
|
||||
return reverse("twitch:game_detail", args=[obj.twitch_id])
|
||||
|
||||
def description(self, obj: Game) -> str:
|
||||
"""Return a description for the feed."""
|
||||
return f"Latest reward campaigns for {obj.display_name}"
|
||||
|
||||
def items(self, obj: Game) -> list[RewardCampaign]:
|
||||
"""Return latest active reward campaigns for this game."""
|
||||
limit: int = self._limit if self._limit is not None else 200
|
||||
queryset: QuerySet[RewardCampaign] = _active_reward_campaigns(
|
||||
RewardCampaign.objects
|
||||
.filter(game=obj)
|
||||
.select_related("game")
|
||||
.order_by("-starts_at"),
|
||||
)
|
||||
return list(queryset[:limit])
|
||||
|
||||
def item_title(self, item: RewardCampaign) -> SafeText:
|
||||
"""Return the reward campaign name as the item title."""
|
||||
if item.brand:
|
||||
return SafeText(f"{item.brand}: {item.name}")
|
||||
return SafeText(item.name)
|
||||
|
||||
def item_description(self, item: RewardCampaign) -> SafeText:
|
||||
"""Return a description of the reward campaign."""
|
||||
parts: list = []
|
||||
|
||||
if item.summary:
|
||||
parts.append(format_html("<p>{}</p>", item.summary))
|
||||
|
||||
if item.starts_at or item.ends_at:
|
||||
start_part = (
|
||||
format_html(
|
||||
"Starts: {} ({})",
|
||||
item.starts_at.strftime("%Y-%m-%d %H:%M %Z"),
|
||||
naturaltime(item.starts_at),
|
||||
)
|
||||
if item.starts_at
|
||||
else ""
|
||||
)
|
||||
end_part = (
|
||||
format_html(
|
||||
"Ends: {} ({})",
|
||||
item.ends_at.strftime("%Y-%m-%d %H:%M %Z"),
|
||||
naturaltime(item.ends_at),
|
||||
)
|
||||
if item.ends_at
|
||||
else ""
|
||||
)
|
||||
if start_part and end_part:
|
||||
parts.append(format_html("<p>{}<br />{}</p>", start_part, end_part))
|
||||
elif start_part:
|
||||
parts.append(format_html("<p>{}</p>", start_part))
|
||||
elif end_part:
|
||||
parts.append(format_html("<p>{}</p>", end_part))
|
||||
|
||||
if item.is_sitewide:
|
||||
parts.append(
|
||||
SafeText("<p><strong>This is a sitewide reward campaign</strong></p>"),
|
||||
)
|
||||
elif item.game:
|
||||
parts.append(
|
||||
format_html(
|
||||
"<p>Game: {}</p>",
|
||||
item.game.display_name or item.game.name,
|
||||
),
|
||||
)
|
||||
|
||||
if item.about_url:
|
||||
parts.append(
|
||||
format_html('<p><a href="{}">Learn more</a></p>', item.about_url),
|
||||
)
|
||||
|
||||
if item.external_url:
|
||||
parts.append(
|
||||
format_html(
|
||||
'<p><a href="{}">Redeem reward</a></p>',
|
||||
item.external_url,
|
||||
),
|
||||
)
|
||||
|
||||
return SafeText("".join(str(p) for p in parts))
|
||||
|
||||
def item_pubdate(self, item: RewardCampaign) -> datetime.datetime:
|
||||
"""Returns the publication date to the feed item.
|
||||
|
||||
Uses starts_at (when the reward starts). Fallback to added_at or now if missing.
|
||||
"""
|
||||
if item.starts_at:
|
||||
return item.starts_at
|
||||
if item.added_at:
|
||||
return item.added_at
|
||||
return timezone.now()
|
||||
|
||||
def item_updateddate(self, item: RewardCampaign) -> datetime.datetime:
|
||||
"""Returns the reward campaign's last update time."""
|
||||
return item.updated_at
|
||||
|
||||
def item_categories(self, item: RewardCampaign) -> tuple[str, ...]:
|
||||
"""Returns the associated game's name and brand as categories."""
|
||||
categories: list[str] = ["twitch", "rewards", "quests"]
|
||||
|
||||
if item.brand:
|
||||
categories.append(item.brand)
|
||||
|
||||
if item.game:
|
||||
categories.append(item.game.get_game_name)
|
||||
|
||||
return tuple(categories)
|
||||
|
||||
def item_guid(self, item: RewardCampaign) -> str:
|
||||
"""Return a unique identifier for each reward campaign."""
|
||||
return self._absolute_url(
|
||||
reverse("twitch:reward_campaign_detail", args=[item.twitch_id]),
|
||||
)
|
||||
|
||||
def item_author_name(self, item: RewardCampaign) -> str:
|
||||
"""Return the author name for the reward campaign."""
|
||||
if item.brand:
|
||||
return item.brand
|
||||
|
||||
if item.game and item.game.display_name:
|
||||
return item.game.display_name
|
||||
|
||||
return "Twitch"
|
||||
|
||||
def author_name(self, obj: Game) -> str:
|
||||
"""Return the author name for the game, typically the owner organization name."""
|
||||
return obj.display_name or "Twitch"
|
||||
|
||||
def item_enclosures(self, item: RewardCampaign) -> list[feedgenerator.Enclosure]:
|
||||
"""Return a list of enclosures for the reward campaign, if available.
|
||||
|
||||
Args:
|
||||
item (RewardCampaign): The reward campaign item.
|
||||
|
||||
Returns:
|
||||
list[feedgenerator.Enclosure]: A list of Enclosure objects if an image URL is
|
||||
available, otherwise an empty list.
|
||||
"""
|
||||
image_url: str = getattr(item, "image_best_url", "")
|
||||
if image_url:
|
||||
try:
|
||||
size: int | None = getattr(item, "image_size_bytes", None)
|
||||
length: int = int(size) if size is not None else 0
|
||||
except TypeError, ValueError:
|
||||
length = 0
|
||||
|
||||
if not length:
|
||||
return []
|
||||
|
||||
mime: str = getattr(item, "image_mime_type", "")
|
||||
mime_type: str = mime or "image/jpeg"
|
||||
|
||||
return [
|
||||
feedgenerator.Enclosure(
|
||||
self._absolute_url(image_url),
|
||||
str(length),
|
||||
mime_type,
|
||||
),
|
||||
]
|
||||
return []
|
||||
|
||||
def feed_url(self, obj: Game) -> str:
|
||||
"""Return the URL to the RSS feed itself."""
|
||||
return reverse("core:game_reward_feed", args=[obj.twitch_id])
|
||||
|
||||
|
||||
class GameRewardCampaignAtomFeed(TTVDropsAtomBaseFeed, GameRewardCampaignFeed):
|
||||
"""Atom feed for latest reward campaigns for a specific game (reuses GameRewardCampaignFeed)."""
|
||||
|
||||
def feed_url(self, obj: Game) -> str:
|
||||
"""Return the URL to the Atom feed itself."""
|
||||
return reverse("core:game_reward_feed_atom", args=[obj.twitch_id])
|
||||
|
||||
|
||||
class GameRewardCampaignDiscordFeed(TTVDropsAtomBaseFeed, GameRewardCampaignFeed):
|
||||
"""Discord feed for latest reward campaigns for a specific game with Discord relative timestamps."""
|
||||
|
||||
def item_description(self, item: RewardCampaign) -> SafeText:
|
||||
"""Return a description of the reward campaign with Discord timestamps."""
|
||||
parts: list = []
|
||||
|
||||
if item.summary:
|
||||
parts.append(format_html("<p>{}</p>", item.summary))
|
||||
|
||||
if item.starts_at or item.ends_at:
|
||||
start_part = (
|
||||
format_html(
|
||||
"Starts: {} ({})",
|
||||
item.starts_at.strftime("%Y-%m-%d %H:%M %Z"),
|
||||
discord_timestamp(item.starts_at),
|
||||
)
|
||||
if item.starts_at
|
||||
else ""
|
||||
)
|
||||
end_part = (
|
||||
format_html(
|
||||
"Ends: {} ({})",
|
||||
item.ends_at.strftime("%Y-%m-%d %H:%M %Z"),
|
||||
discord_timestamp(item.ends_at),
|
||||
)
|
||||
if item.ends_at
|
||||
else ""
|
||||
)
|
||||
if start_part and end_part:
|
||||
parts.append(format_html("<p>{}<br />{}</p>", start_part, end_part))
|
||||
elif start_part:
|
||||
parts.append(format_html("<p>{}</p>", start_part))
|
||||
elif end_part:
|
||||
parts.append(format_html("<p>{}</p>", end_part))
|
||||
|
||||
if item.is_sitewide:
|
||||
parts.append(
|
||||
SafeText("<p><strong>This is a sitewide reward campaign</strong></p>"),
|
||||
)
|
||||
elif item.game:
|
||||
parts.append(
|
||||
format_html(
|
||||
"<p>Game: {}</p>",
|
||||
item.game.display_name or item.game.name,
|
||||
),
|
||||
)
|
||||
|
||||
if item.about_url:
|
||||
parts.append(
|
||||
format_html('<p><a href="{}">Learn more</a></p>', item.about_url),
|
||||
)
|
||||
|
||||
if item.external_url:
|
||||
parts.append(
|
||||
format_html(
|
||||
'<p><a href="{}">Redeem reward</a></p>',
|
||||
item.external_url,
|
||||
),
|
||||
)
|
||||
|
||||
return SafeText("".join(str(p) for p in parts))
|
||||
|
||||
def feed_url(self, obj: Game) -> str:
|
||||
"""Return the URL to the Discord feed itself."""
|
||||
return reverse("core:game_reward_feed_discord", args=[obj.twitch_id])
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from twitch.feeds import RSS_STYLESHEETS
|
|||
from twitch.feeds import DropCampaignFeed
|
||||
from twitch.feeds import GameCampaignFeed
|
||||
from twitch.feeds import GameFeed
|
||||
from twitch.feeds import GameRewardCampaignFeed
|
||||
from twitch.feeds import OrganizationRSSFeed
|
||||
from twitch.feeds import RewardCampaignFeed
|
||||
from twitch.feeds import TTVDropsBaseFeed
|
||||
|
|
@ -297,6 +298,11 @@ class RSSFeedTestCase(TestCase):
|
|||
{},
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:game_reward_feed_atom",
|
||||
{"twitch_id": self.game.twitch_id},
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
|
||||
),
|
||||
]
|
||||
|
||||
for url_name, kwargs, expected_entry_id in atom_feed_cases:
|
||||
|
|
@ -347,6 +353,7 @@ class RSSFeedTestCase(TestCase):
|
|||
reverse("core:campaign_feed_atom"),
|
||||
reverse("core:game_feed_atom"),
|
||||
reverse("core:game_campaign_feed_atom", args=[self.game.twitch_id]),
|
||||
reverse("core:game_reward_feed_atom", args=[self.game.twitch_id]),
|
||||
reverse("core:organization_feed_atom"),
|
||||
reverse("core:reward_campaign_feed_atom"),
|
||||
]
|
||||
|
|
@ -425,10 +432,12 @@ class RSSFeedTestCase(TestCase):
|
|||
reverse("core:game_feed"),
|
||||
reverse("core:campaign_feed"),
|
||||
reverse("core:game_campaign_feed", args=[self.game.twitch_id]),
|
||||
reverse("core:game_reward_feed", args=[self.game.twitch_id]),
|
||||
reverse("core:reward_campaign_feed"),
|
||||
reverse("core:game_feed_atom"),
|
||||
reverse("core:campaign_feed_atom"),
|
||||
reverse("core:game_campaign_feed_atom", args=[self.game.twitch_id]),
|
||||
reverse("core:game_reward_feed_atom", args=[self.game.twitch_id]),
|
||||
reverse("core:reward_campaign_feed_atom"),
|
||||
]
|
||||
|
||||
|
|
@ -484,6 +493,7 @@ class RSSFeedTestCase(TestCase):
|
|||
reverse("core:campaign_feed"),
|
||||
reverse("core:game_feed"),
|
||||
reverse("core:game_campaign_feed", args=[self.game.twitch_id]),
|
||||
reverse("core:game_reward_feed", args=[self.game.twitch_id]),
|
||||
reverse("core:organization_feed"),
|
||||
reverse("core:reward_campaign_feed"),
|
||||
]
|
||||
|
|
@ -516,12 +526,14 @@ class RSSFeedTestCase(TestCase):
|
|||
GameFeed,
|
||||
DropCampaignFeed,
|
||||
GameCampaignFeed,
|
||||
GameRewardCampaignFeed,
|
||||
RewardCampaignFeed,
|
||||
):
|
||||
feed: (
|
||||
DropCampaignFeed
|
||||
| GameCampaignFeed
|
||||
| GameFeed
|
||||
| GameRewardCampaignFeed
|
||||
| OrganizationRSSFeed
|
||||
| RewardCampaignFeed
|
||||
) = feed_class()
|
||||
|
|
@ -535,6 +547,7 @@ class RSSFeedTestCase(TestCase):
|
|||
reverse("core:campaign_feed"),
|
||||
reverse("core:game_feed"),
|
||||
reverse("core:game_campaign_feed", args=[self.game.twitch_id]),
|
||||
reverse("core:game_reward_feed", args=[self.game.twitch_id]),
|
||||
reverse("core:organization_feed"),
|
||||
reverse("core:reward_campaign_feed"),
|
||||
]
|
||||
|
|
@ -799,6 +812,89 @@ class RSSFeedTestCase(TestCase):
|
|||
assert "Past Reward Campaign" not in content
|
||||
assert "Upcoming Reward Campaign" not in content
|
||||
|
||||
def test_game_reward_campaign_feed(self) -> None:
|
||||
"""Test game-specific reward campaign feed returns 200."""
|
||||
url: str = reverse("core:game_reward_feed", args=[self.game.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/xml; charset=utf-8"
|
||||
assert response["Content-Disposition"] == "inline"
|
||||
content: str = response.content.decode("utf-8")
|
||||
assert "Test Game" in content
|
||||
assert "Test Brand: Test Reward Campaign" in content
|
||||
|
||||
def test_game_reward_campaign_feed_filters_correctly(self) -> None:
|
||||
"""Test game reward feed only shows rewards for that game."""
|
||||
other_game: Game = Game.objects.create(
|
||||
twitch_id="other-reward-game-123",
|
||||
slug="other-reward-game",
|
||||
name="Other Game Rewards",
|
||||
display_name="Other Game Rewards",
|
||||
)
|
||||
other_game.owners.add(self.org)
|
||||
RewardCampaign.objects.create(
|
||||
twitch_id="other-reward-123",
|
||||
name="Other Reward Campaign",
|
||||
brand="Other Brand",
|
||||
starts_at=timezone.now() - timedelta(days=1),
|
||||
ends_at=timezone.now() + timedelta(days=7),
|
||||
status="ACTIVE",
|
||||
summary="Other reward summary",
|
||||
instructions="Do other things",
|
||||
external_url="https://example.com/other-reward",
|
||||
about_url="https://example.com/about-other-reward",
|
||||
is_sitewide=False,
|
||||
game=other_game,
|
||||
)
|
||||
|
||||
url: str = reverse("core:game_reward_feed", args=[self.game.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
content: str = response.content.decode("utf-8")
|
||||
|
||||
assert "Test Brand: Test Reward Campaign" in content
|
||||
assert "Other Brand: Other Reward Campaign" not in content
|
||||
|
||||
def test_game_reward_campaign_feed_only_includes_active_campaigns(self) -> None:
|
||||
"""Game reward campaign feed should exclude old and upcoming campaigns."""
|
||||
now: datetime.datetime = timezone.now()
|
||||
RewardCampaign.objects.create(
|
||||
twitch_id="game-reward-past-123",
|
||||
name="Game Past Reward",
|
||||
brand="Test Brand",
|
||||
starts_at=now - timedelta(days=10),
|
||||
ends_at=now - timedelta(days=1),
|
||||
status="EXPIRED",
|
||||
summary="Past reward",
|
||||
instructions="Was active",
|
||||
external_url="https://example.com/past-reward",
|
||||
about_url="https://example.com/about-past-reward",
|
||||
is_sitewide=False,
|
||||
game=self.game,
|
||||
)
|
||||
RewardCampaign.objects.create(
|
||||
twitch_id="game-reward-upcoming-123",
|
||||
name="Game Upcoming Reward",
|
||||
brand="Test Brand",
|
||||
starts_at=now + timedelta(days=1),
|
||||
ends_at=now + timedelta(days=10),
|
||||
status="UPCOMING",
|
||||
summary="Upcoming reward",
|
||||
instructions="Wait",
|
||||
external_url="https://example.com/upcoming-reward",
|
||||
about_url="https://example.com/about-upcoming-reward",
|
||||
is_sitewide=False,
|
||||
game=self.game,
|
||||
)
|
||||
|
||||
url: str = reverse("core:game_reward_feed", args=[self.game.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
content: str = response.content.decode("utf-8")
|
||||
|
||||
assert "Test Reward Campaign" in content
|
||||
assert "Game Past Reward" not in content
|
||||
assert "Game Upcoming Reward" not in content
|
||||
|
||||
def test_game_campaign_feed_enclosure_helpers(self) -> None:
|
||||
"""GameCampaignFeed helper methods should pull from the model fields."""
|
||||
feed = GameCampaignFeed()
|
||||
|
|
@ -1106,6 +1202,35 @@ def test_game_campaign_feed_queries_do_not_scale_with_items(
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_game_reward_campaign_feed_queries_bounded(
|
||||
client: Client,
|
||||
django_assert_num_queries: QueryAsserter,
|
||||
) -> None:
|
||||
"""Game reward campaign feed should stay within a modest query budget."""
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="test-org-game-reward-queries",
|
||||
name="Query Org Game Rewards",
|
||||
)
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="test-game-reward-queries",
|
||||
slug="query-game-reward",
|
||||
name="Query Game Rewards",
|
||||
display_name="Query Game Rewards",
|
||||
)
|
||||
game.owners.add(org)
|
||||
|
||||
for i in range(3):
|
||||
_build_reward_campaign(game, i)
|
||||
|
||||
url: str = reverse("core:game_reward_feed", args=[game.twitch_id])
|
||||
|
||||
with django_assert_num_queries(2, exact=True):
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_feed_queries_bounded(
|
||||
client: Client,
|
||||
|
|
@ -1235,6 +1360,7 @@ URL_NAMES: list[tuple[str, dict[str, str]]] = [
|
|||
("core:campaign_feed", {}),
|
||||
("core:game_feed", {}),
|
||||
("core:game_campaign_feed", {"twitch_id": "test-game-123"}),
|
||||
("core:game_reward_feed", {"twitch_id": "test-game-123"}),
|
||||
("core:organization_feed", {}),
|
||||
("core:reward_campaign_feed", {}),
|
||||
]
|
||||
|
|
@ -1465,6 +1591,11 @@ class DiscordFeedTestCase(TestCase):
|
|||
{},
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:game_reward_feed_discord",
|
||||
{"twitch_id": self.game.twitch_id},
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
|
||||
),
|
||||
]
|
||||
|
||||
for url_name, kwargs, expected_entry_id in discord_feed_cases:
|
||||
|
|
@ -1490,6 +1621,7 @@ class DiscordFeedTestCase(TestCase):
|
|||
reverse("core:campaign_feed_discord"),
|
||||
reverse("core:game_feed_discord"),
|
||||
reverse("core:game_campaign_feed_discord", args=[self.game.twitch_id]),
|
||||
reverse("core:game_reward_feed_discord", args=[self.game.twitch_id]),
|
||||
reverse("core:organization_feed_discord"),
|
||||
reverse("core:reward_campaign_feed_discord"),
|
||||
]
|
||||
|
|
@ -1518,6 +1650,19 @@ class DiscordFeedTestCase(TestCase):
|
|||
)
|
||||
assert "()" not in content
|
||||
|
||||
def test_game_reward_campaign_discord_feed(self) -> None:
|
||||
"""Test game-specific reward campaign Discord feed returns 200."""
|
||||
url: str = reverse(
|
||||
"core:game_reward_feed_discord",
|
||||
args=[self.game.twitch_id],
|
||||
)
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/xml; charset=utf-8"
|
||||
content: str = response.content.decode("utf-8")
|
||||
assert "<feed" in content
|
||||
assert "Test Game Discord" in content
|
||||
|
||||
def test_discord_reward_campaign_feed_contains_discord_timestamps(self) -> None:
|
||||
"""Discord reward campaign feed should contain Discord relative timestamps."""
|
||||
url: str = reverse("core:reward_campaign_feed_discord")
|
||||
|
|
@ -1532,6 +1677,25 @@ class DiscordFeedTestCase(TestCase):
|
|||
)
|
||||
assert "()" not in content
|
||||
|
||||
def test_discord_game_reward_campaign_feed_contains_discord_timestamps(
|
||||
self,
|
||||
) -> None:
|
||||
"""Discord game reward campaign feed should contain Discord relative timestamps."""
|
||||
url: str = reverse(
|
||||
"core:game_reward_feed_discord",
|
||||
args=[self.game.twitch_id],
|
||||
)
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
content: str = response.content.decode("utf-8")
|
||||
|
||||
# Should contain Discord timestamp format (double-escaped in XML payload)
|
||||
discord_pattern: re.Pattern[str] = re.compile(r"&lt;t:\d+:R&gt;")
|
||||
assert discord_pattern.search(content), (
|
||||
f"Expected Discord timestamp format &lt;t:UNIX_TIMESTAMP:R&gt; in content, got: {content}"
|
||||
)
|
||||
assert "()" not in content
|
||||
|
||||
def test_feed_links_return_200(self) -> None:
|
||||
"""Test that all links in the feeds return 200 OK."""
|
||||
feed_urls: list[str] = [
|
||||
|
|
|
|||
|
|
@ -721,6 +721,27 @@ class GameDetailView(DetailView):
|
|||
active_campaigns, upcoming_campaigns, expired_campaigns = (
|
||||
DropCampaign.split_for_channel_detail(campaigns_list, now)
|
||||
)
|
||||
|
||||
# Fetch reward campaigns for this game
|
||||
all_reward_campaigns: list[RewardCampaign] = list(
|
||||
RewardCampaign.objects
|
||||
.filter(game=game)
|
||||
.select_related("game")
|
||||
.order_by("-starts_at"),
|
||||
)
|
||||
active_rewards: list[RewardCampaign] = []
|
||||
upcoming_rewards: list[RewardCampaign] = []
|
||||
expired_rewards: list[RewardCampaign] = []
|
||||
for rc in all_reward_campaigns:
|
||||
if rc.starts_at is None or rc.ends_at is None:
|
||||
continue
|
||||
if rc.starts_at <= now <= rc.ends_at:
|
||||
active_rewards.append(rc)
|
||||
elif rc.starts_at > now:
|
||||
upcoming_rewards.append(rc)
|
||||
elif rc.ends_at < now:
|
||||
expired_rewards.append(rc)
|
||||
|
||||
owners: list[Organization] = list(getattr(game, "owners_for_detail", []))
|
||||
|
||||
game_name: str = game.get_game_name
|
||||
|
|
@ -802,6 +823,9 @@ class GameDetailView(DetailView):
|
|||
"active_campaigns": active_campaigns,
|
||||
"upcoming_campaigns": upcoming_campaigns,
|
||||
"expired_campaigns": expired_campaigns,
|
||||
"active_rewards": active_rewards,
|
||||
"upcoming_rewards": upcoming_rewards,
|
||||
"expired_rewards": expired_rewards,
|
||||
"owner": owners[0] if owners else None,
|
||||
"owners": owners,
|
||||
"now": now,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue