Add page for game-less reward campaigns with RSS/Atom/Discord feeds
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
9de0f52af8
commit
dbc36db1e0
8 changed files with 602 additions and 11 deletions
21
core/urls.py
21
core/urls.py
|
|
@ -17,6 +17,9 @@ from twitch.feeds import GameCampaignDiscordFeed
|
||||||
from twitch.feeds import GameCampaignFeed
|
from twitch.feeds import GameCampaignFeed
|
||||||
from twitch.feeds import GameDiscordFeed
|
from twitch.feeds import GameDiscordFeed
|
||||||
from twitch.feeds import GameFeed
|
from twitch.feeds import GameFeed
|
||||||
|
from twitch.feeds import GameLessRewardCampaignAtomFeed
|
||||||
|
from twitch.feeds import GameLessRewardCampaignDiscordFeed
|
||||||
|
from twitch.feeds import GameLessRewardCampaignFeed
|
||||||
from twitch.feeds import GameRewardCampaignAtomFeed
|
from twitch.feeds import GameRewardCampaignAtomFeed
|
||||||
from twitch.feeds import GameRewardCampaignDiscordFeed
|
from twitch.feeds import GameRewardCampaignDiscordFeed
|
||||||
from twitch.feeds import GameRewardCampaignFeed
|
from twitch.feeds import GameRewardCampaignFeed
|
||||||
|
|
@ -160,6 +163,24 @@ urlpatterns: list[URLPattern | URLResolver] = [
|
||||||
view=SitewideRewardCampaignDiscordFeed(),
|
view=SitewideRewardCampaignDiscordFeed(),
|
||||||
name="sitewide_reward_feed_discord",
|
name="sitewide_reward_feed_discord",
|
||||||
),
|
),
|
||||||
|
# /rss/rewards/no-game/ - reward campaigns with no game linked (brand + sitewide)
|
||||||
|
path(
|
||||||
|
route="rss/rewards/no-game/",
|
||||||
|
view=GameLessRewardCampaignFeed(),
|
||||||
|
name="game_less_reward_feed",
|
||||||
|
),
|
||||||
|
# /atom/rewards/no-game/ - same as RSS but Atom
|
||||||
|
path(
|
||||||
|
route="atom/rewards/no-game/",
|
||||||
|
view=GameLessRewardCampaignAtomFeed(),
|
||||||
|
name="game_less_reward_feed_atom",
|
||||||
|
),
|
||||||
|
# /discord/rewards/no-game/ - same as RSS but Discord timestamps
|
||||||
|
path(
|
||||||
|
route="discord/rewards/no-game/",
|
||||||
|
view=GameLessRewardCampaignDiscordFeed(),
|
||||||
|
name="game_less_reward_feed_discord",
|
||||||
|
),
|
||||||
# Discord feeds (Atom feeds with Discord relative timestamps)
|
# Discord feeds (Atom feeds with Discord relative timestamps)
|
||||||
path(
|
path(
|
||||||
route="discord/campaigns/",
|
route="discord/campaigns/",
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,18 @@
|
||||||
<a href="https://ttvdrops.lovinator.space{% url 'core:sitewide_reward_feed_discord' %}">https://ttvdrops.lovinator.space/discord/sitewide/rewards/</a>
|
<a href="https://ttvdrops.lovinator.space{% url 'core:sitewide_reward_feed_discord' %}">https://ttvdrops.lovinator.space/discord/sitewide/rewards/</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Twitch rewards with no game linked (brand + sitewide)</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space{% url 'core:game_less_reward_feed' %}">https://ttvdrops.lovinator.space/rss/rewards/no-game/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space{% url 'core:game_less_reward_feed_atom' %}">https://ttvdrops.lovinator.space/atom/rewards/no-game/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space{% url 'core:game_less_reward_feed_discord' %}">https://ttvdrops.lovinator.space/discord/rewards/no-game/</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Latest Kick drops</td>
|
<td>Latest Kick drops</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
198
templates/twitch/game_less_rewards.html
Normal file
198
templates/twitch/game_less_rewards.html
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block title %}
|
||||||
|
Brand & Site-wide Rewards
|
||||||
|
{% endblock title %}
|
||||||
|
{% block extra_head %}
|
||||||
|
<link rel="alternate"
|
||||||
|
type="application/rss+xml"
|
||||||
|
title="All game-less rewards (RSS)"
|
||||||
|
href="{% url 'core:game_less_reward_feed' %}" />
|
||||||
|
<link rel="alternate"
|
||||||
|
type="application/atom+xml"
|
||||||
|
title="All game-less rewards (Atom)"
|
||||||
|
href="{% url 'core:game_less_reward_feed_atom' %}" />
|
||||||
|
<link rel="alternate"
|
||||||
|
type="application/atom+xml"
|
||||||
|
title="All game-less rewards (Discord)"
|
||||||
|
href="{% url 'core:game_less_reward_feed_discord' %}" />
|
||||||
|
{% endblock extra_head %}
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>Brand & Site-wide Rewards</h1>
|
||||||
|
<!-- Breadcrumbs -->
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'twitch:dashboard' %}">Twitch</a> > <a href="{% url 'twitch:reward_campaign_list' %}">Reward Campaigns</a> > Brand & Site-wide
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Reward campaigns not tied to a specific Twitch game. These include
|
||||||
|
<strong>brand-specific</strong> campaigns (e.g., Minecraft, DOORS) and
|
||||||
|
truly <strong>site-wide</strong> Twitch quests.
|
||||||
|
</p>
|
||||||
|
<!-- Feeds and links -->
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'core:game_less_reward_feed' %}"
|
||||||
|
title="RSS feed for all game-less rewards">[rss]</a>
|
||||||
|
<a href="{% url 'core:game_less_reward_feed_atom' %}"
|
||||||
|
title="Atom feed for all game-less rewards">[atom]</a>
|
||||||
|
<a href="{% url 'core:game_less_reward_feed_discord' %}"
|
||||||
|
title="Discord feed for all game-less rewards">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
|
<a href="{% url 'twitch:sitewide_rewards' %}"
|
||||||
|
title="View only site-wide rewards">[sitewide only]</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% if active_campaigns %}
|
||||||
|
<section>
|
||||||
|
<h2>Active Rewards</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Campaign</th>
|
||||||
|
<th>Brand</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Ends</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for campaign in active_campaigns %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}">
|
||||||
|
{% if campaign.brand %}
|
||||||
|
{{ campaign.brand }}: {{ campaign.name }}
|
||||||
|
{% else %}
|
||||||
|
{{ campaign.name }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if campaign.summary %}
|
||||||
|
<br />
|
||||||
|
<small>{{ campaign.summary }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ campaign.brand|default:"—" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if campaign.is_sitewide %}
|
||||||
|
<strong>Site-wide</strong>
|
||||||
|
{% else %}
|
||||||
|
Brand
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<time datetime="{{ campaign.ends_at|date:'c' }}"
|
||||||
|
title="{{ campaign.ends_at|date:'DATETIME_FORMAT' }}">
|
||||||
|
Ends in {{ campaign.ends_at|timeuntil }}
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if upcoming_campaigns %}
|
||||||
|
<section>
|
||||||
|
<h2>Upcoming Rewards</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Campaign</th>
|
||||||
|
<th>Brand</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Starts</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for campaign in upcoming_campaigns %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}">
|
||||||
|
{% if campaign.brand %}
|
||||||
|
{{ campaign.brand }}: {{ campaign.name }}
|
||||||
|
{% else %}
|
||||||
|
{{ campaign.name }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if campaign.summary %}
|
||||||
|
<br />
|
||||||
|
<small>{{ campaign.summary }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ campaign.brand|default:"—" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if campaign.is_sitewide %}
|
||||||
|
<strong>Site-wide</strong>
|
||||||
|
{% else %}
|
||||||
|
Brand
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<time datetime="{{ campaign.starts_at|date:'c' }}"
|
||||||
|
title="{{ campaign.starts_at|date:'DATETIME_FORMAT' }}">
|
||||||
|
Starts in {{ campaign.starts_at|timeuntil }}
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if expired_campaigns %}
|
||||||
|
<section>
|
||||||
|
<h2>Past Rewards</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Campaign</th>
|
||||||
|
<th>Brand</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Ended</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for campaign in expired_campaigns %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}">
|
||||||
|
{% if campaign.brand %}
|
||||||
|
{{ campaign.brand }}: {{ campaign.name }}
|
||||||
|
{% else %}
|
||||||
|
{{ campaign.name }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if campaign.summary %}
|
||||||
|
<br />
|
||||||
|
<small>{{ campaign.summary }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ campaign.brand|default:"—" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if campaign.is_sitewide %}
|
||||||
|
<strong>Site-wide</strong>
|
||||||
|
{% else %}
|
||||||
|
Brand
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<time datetime="{{ campaign.ends_at|date:'c' }}"
|
||||||
|
title="{{ campaign.ends_at|date:'DATETIME_FORMAT' }}">
|
||||||
|
{{ campaign.ends_at|timesince }} ago
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if not active_campaigns and not upcoming_campaigns and not expired_campaigns %}
|
||||||
|
<section style="text-align: center; padding: 3rem 1rem;">
|
||||||
|
<div style="font-size: 4rem; margin-bottom: 1rem;">🏷️</div>
|
||||||
|
<h2 style="margin: 0 0 1rem 0;">No Rewards Found</h2>
|
||||||
|
<p style="margin: 0; font-size: 1.1rem;">No brand or site-wide reward campaigns are currently available.</p>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
{% endblock content %}
|
||||||
|
|
@ -30,17 +30,19 @@
|
||||||
</p>
|
</p>
|
||||||
<!-- RSS Feeds -->
|
<!-- RSS Feeds -->
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'core:reward_campaign_feed' %}"
|
<div>
|
||||||
title="RSS feed for all reward campaigns">[rss]</a>
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
<a href="{% url 'core:reward_campaign_feed_atom' %}"
|
<a href="{% url 'twitch:twitch-api-v1:list_reward_campaigns' %}"
|
||||||
title="Atom feed for all reward campaigns">[atom]</a>
|
title="Twitch reward campaigns API">[api]</a>
|
||||||
<a href="{% url 'core:reward_campaign_feed_discord' %}"
|
</div>
|
||||||
title="Discord feed for all reward campaigns">[discord]</a>
|
<div>
|
||||||
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
<a href="{% url 'core:reward_campaign_feed' %}"
|
||||||
<a href="{% url 'twitch:twitch-api-v1:list_reward_campaigns' %}"
|
title="RSS feed for all reward campaigns">[rss]</a>
|
||||||
title="Twitch reward campaigns API">[api]</a>
|
<a href="{% url 'core:reward_campaign_feed_atom' %}"
|
||||||
<a href="{% url 'twitch:sitewide_rewards' %}"
|
title="Atom feed for all reward campaigns">[atom]</a>
|
||||||
title="View site-wide reward campaigns">[sitewide]</a>
|
<a href="{% url 'core:reward_campaign_feed_discord' %}"
|
||||||
|
title="Discord feed for all reward campaigns">[discord]</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if reward_campaigns %}
|
{% if reward_campaigns %}
|
||||||
{% with active_campaigns=reward_campaigns|dictsortreversed:"ends_at" %}
|
{% with active_campaigns=reward_campaigns|dictsortreversed:"ends_at" %}
|
||||||
|
|
@ -68,6 +70,8 @@
|
||||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.display_name }}</a>
|
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.display_name }}</a>
|
||||||
{% elif campaign.is_sitewide %}
|
{% elif campaign.is_sitewide %}
|
||||||
<a href="{% url 'twitch:sitewide_rewards' %}">Site-wide</a>
|
<a href="{% url 'twitch:sitewide_rewards' %}">Site-wide</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'twitch:game_less_rewards' %}">[no game]</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -102,6 +106,8 @@
|
||||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.display_name }}</a>
|
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.display_name }}</a>
|
||||||
{% elif campaign.is_sitewide %}
|
{% elif campaign.is_sitewide %}
|
||||||
<a href="{% url 'twitch:sitewide_rewards' %}">Site-wide</a>
|
<a href="{% url 'twitch:sitewide_rewards' %}">Site-wide</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'twitch:game_less_rewards' %}">[no game]</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -137,6 +143,8 @@
|
||||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.display_name }}</a>
|
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.display_name }}</a>
|
||||||
{% elif campaign.is_sitewide %}
|
{% elif campaign.is_sitewide %}
|
||||||
<a href="{% url 'twitch:sitewide_rewards' %}">Site-wide</a>
|
<a href="{% url 'twitch:sitewide_rewards' %}">Site-wide</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'twitch:game_less_rewards' %}">[no game]</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,18 @@
|
||||||
type="application/atom+xml"
|
type="application/atom+xml"
|
||||||
title="Site-wide rewards (Discord)"
|
title="Site-wide rewards (Discord)"
|
||||||
href="{% url 'core:sitewide_reward_feed_discord' %}" />
|
href="{% url 'core:sitewide_reward_feed_discord' %}" />
|
||||||
|
<link rel="alternate"
|
||||||
|
type="application/rss+xml"
|
||||||
|
title="All game-less rewards (RSS)"
|
||||||
|
href="{% url 'core:game_less_reward_feed' %}" />
|
||||||
|
<link rel="alternate"
|
||||||
|
type="application/atom+xml"
|
||||||
|
title="All game-less rewards (Atom)"
|
||||||
|
href="{% url 'core:game_less_reward_feed_atom' %}" />
|
||||||
|
<link rel="alternate"
|
||||||
|
type="application/atom+xml"
|
||||||
|
title="All game-less rewards (Discord)"
|
||||||
|
href="{% url 'core:game_less_reward_feed_discord' %}" />
|
||||||
{% endblock extra_head %}
|
{% endblock extra_head %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
|
|
@ -39,6 +51,8 @@
|
||||||
<a href="{% url 'core:sitewide_reward_feed_discord' %}"
|
<a href="{% url 'core:sitewide_reward_feed_discord' %}"
|
||||||
title="Discord feed for site-wide rewards">[discord]</a>
|
title="Discord feed for site-wide rewards">[discord]</a>
|
||||||
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
|
<a href="{% url 'twitch:game_less_rewards' %}"
|
||||||
|
title="View all rewards with no game linked">[no game]</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% if active_campaigns %}
|
{% if active_campaigns %}
|
||||||
|
|
|
||||||
251
twitch/feeds.py
251
twitch/feeds.py
|
|
@ -2223,3 +2223,254 @@ class SitewideRewardCampaignDiscordFeed(
|
||||||
def feed_url(self) -> str:
|
def feed_url(self) -> str:
|
||||||
"""Return the URL to the Discord feed itself."""
|
"""Return the URL to the Discord feed itself."""
|
||||||
return reverse("core:sitewide_reward_feed_discord")
|
return reverse("core:sitewide_reward_feed_discord")
|
||||||
|
|
||||||
|
|
||||||
|
# MARK: /rss/rewards/no-game/
|
||||||
|
class GameLessRewardCampaignFeed(TTVDropsBaseFeed):
|
||||||
|
"""RSS feed for reward campaigns with no game linked (brand-specific + sitewide)."""
|
||||||
|
|
||||||
|
title: str = "TTVDrops: Game-less Rewards"
|
||||||
|
link: str = "/twitch/reward-campaigns/"
|
||||||
|
description: str = (
|
||||||
|
"Twitch reward campaigns without a linked game (brand-specific and sitewide)"
|
||||||
|
)
|
||||||
|
|
||||||
|
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: 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.
|
||||||
|
"""
|
||||||
|
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 items(self) -> list[RewardCampaign]:
|
||||||
|
"""Return reward campaigns with no game linked, ordered by newest."""
|
||||||
|
limit: int = self._limit if self._limit is not None else 200
|
||||||
|
return list(
|
||||||
|
RewardCampaign.objects
|
||||||
|
.filter(game__isnull=True)
|
||||||
|
.select_related("game")
|
||||||
|
.order_by("-starts_at")[: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.brand:
|
||||||
|
parts.append(
|
||||||
|
format_html("<p>Brand: {}</p>", item.brand),
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.is_sitewide:
|
||||||
|
parts.append(
|
||||||
|
SafeText("<p><strong>Site-wide reward campaign</strong></p>"),
|
||||||
|
)
|
||||||
|
|
||||||
|
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_link(self, item: RewardCampaign) -> str:
|
||||||
|
"""Return the link to the reward campaign detail."""
|
||||||
|
return reverse("twitch:reward_campaign_detail", args=[item.twitch_id])
|
||||||
|
|
||||||
|
def item_pubdate(self, item: RewardCampaign) -> datetime.datetime:
|
||||||
|
"""Returns the publication date to the feed item."""
|
||||||
|
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 categories for the feed item."""
|
||||||
|
categories: list[str] = ["twitch", "rewards", "quests"]
|
||||||
|
if item.brand:
|
||||||
|
categories.append(item.brand)
|
||||||
|
if item.is_sitewide:
|
||||||
|
categories.append("sitewide")
|
||||||
|
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."""
|
||||||
|
return item.brand or "Twitch"
|
||||||
|
|
||||||
|
def item_enclosures(self, item: RewardCampaign) -> list[feedgenerator.Enclosure]:
|
||||||
|
"""Return a list of enclosures for the reward campaign, if available."""
|
||||||
|
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) -> str:
|
||||||
|
"""Return the URL to the RSS feed itself."""
|
||||||
|
return reverse("core:game_less_reward_feed")
|
||||||
|
|
||||||
|
|
||||||
|
class GameLessRewardCampaignAtomFeed(TTVDropsAtomBaseFeed, GameLessRewardCampaignFeed):
|
||||||
|
"""Atom feed for reward campaigns with no game linked."""
|
||||||
|
|
||||||
|
def feed_url(self) -> str:
|
||||||
|
"""Return the URL to the Atom feed itself."""
|
||||||
|
return reverse("core:game_less_reward_feed_atom")
|
||||||
|
|
||||||
|
|
||||||
|
class GameLessRewardCampaignDiscordFeed(
|
||||||
|
TTVDropsAtomBaseFeed, GameLessRewardCampaignFeed
|
||||||
|
):
|
||||||
|
"""Discord feed for reward campaigns with no game linked, with Discord timestamps."""
|
||||||
|
|
||||||
|
def item_description(self, item: RewardCampaign) -> SafeText:
|
||||||
|
"""Return a description 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.brand:
|
||||||
|
parts.append(
|
||||||
|
format_html("<p>Brand: {}</p>", item.brand),
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.is_sitewide:
|
||||||
|
parts.append(
|
||||||
|
SafeText("<p><strong>Site-wide reward campaign</strong></p>"),
|
||||||
|
)
|
||||||
|
|
||||||
|
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) -> str:
|
||||||
|
"""Return the URL to the Discord feed itself."""
|
||||||
|
return reverse("core:game_less_reward_feed_discord")
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,12 @@ urlpatterns: list[URLPattern | URLResolver] = [
|
||||||
views.sitewide_rewards_view,
|
views.sitewide_rewards_view,
|
||||||
name="sitewide_rewards",
|
name="sitewide_rewards",
|
||||||
),
|
),
|
||||||
|
# /twitch/rewards/no-game/
|
||||||
|
path(
|
||||||
|
"rewards/no-game/",
|
||||||
|
views.game_less_rewards_view,
|
||||||
|
name="game_less_rewards",
|
||||||
|
),
|
||||||
# /twitch/export/campaigns/csv/
|
# /twitch/export/campaigns/csv/
|
||||||
path(
|
path(
|
||||||
"export/campaigns/csv/",
|
"export/campaigns/csv/",
|
||||||
|
|
|
||||||
|
|
@ -1728,3 +1728,84 @@ def sitewide_rewards_view(request: HttpRequest) -> HttpResponse:
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "twitch/sitewide_rewards.html", context)
|
return render(request, "twitch/sitewide_rewards.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
# MARK: /rewards/no-game/
|
||||||
|
def game_less_rewards_view(request: HttpRequest) -> HttpResponse:
|
||||||
|
"""View to display all reward campaigns with no game linked.
|
||||||
|
|
||||||
|
Includes both site-wide (is_sitewide=True) and brand-specific campaigns
|
||||||
|
that don't have a Twitch game associated with them.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The HTTP request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: The rendered game-less rewards page.
|
||||||
|
"""
|
||||||
|
now: datetime.datetime = timezone.now()
|
||||||
|
|
||||||
|
# Fetch all reward campaigns without a game
|
||||||
|
campaigns: QuerySet[RewardCampaign] = (
|
||||||
|
RewardCampaign.objects
|
||||||
|
.filter(game__isnull=True)
|
||||||
|
.select_related("game")
|
||||||
|
.order_by("-starts_at")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split into active, upcoming, expired
|
||||||
|
active_campaigns: list[RewardCampaign] = []
|
||||||
|
upcoming_campaigns: list[RewardCampaign] = []
|
||||||
|
expired_campaigns: list[RewardCampaign] = []
|
||||||
|
|
||||||
|
for campaign in campaigns:
|
||||||
|
if campaign.starts_at is None or campaign.ends_at is None:
|
||||||
|
continue
|
||||||
|
if campaign.starts_at <= now <= campaign.ends_at:
|
||||||
|
active_campaigns.append(campaign)
|
||||||
|
elif campaign.starts_at > now:
|
||||||
|
upcoming_campaigns.append(campaign)
|
||||||
|
elif campaign.ends_at < now:
|
||||||
|
expired_campaigns.append(campaign)
|
||||||
|
|
||||||
|
title: str = "Brand & Site-wide Rewards"
|
||||||
|
description: str = (
|
||||||
|
"Twitch reward campaigns without a linked game (brand-specific and site-wide)."
|
||||||
|
)
|
||||||
|
|
||||||
|
url: str = build_absolute_uri(reverse("twitch:game_less_rewards"))
|
||||||
|
|
||||||
|
# CollectionPage schema
|
||||||
|
collection_schema: dict[str, str] = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "CollectionPage",
|
||||||
|
"name": title,
|
||||||
|
"description": description,
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Breadcrumb schema
|
||||||
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
|
{"name": "Home", "url": build_absolute_uri("/")},
|
||||||
|
{"name": "Reward Campaigns", "url": build_absolute_uri("/reward-campaigns/")},
|
||||||
|
{"name": title, "url": url},
|
||||||
|
])
|
||||||
|
|
||||||
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
|
page_title=title,
|
||||||
|
page_description=description,
|
||||||
|
seo_meta={
|
||||||
|
"page_url": url,
|
||||||
|
"schema_data": collection_schema,
|
||||||
|
"breadcrumb_schema": breadcrumb_schema,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
context: dict[str, Any] = {
|
||||||
|
"active_campaigns": active_campaigns,
|
||||||
|
"upcoming_campaigns": upcoming_campaigns,
|
||||||
|
"expired_campaigns": expired_campaigns,
|
||||||
|
"now": now,
|
||||||
|
**seo_context,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "twitch/game_less_rewards.html", context)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue