Add rewards to more pages as we have support for them now
All checks were successful
Deploy to Server / deploy (push) Successful in 26s

This commit is contained in:
Joakim Hellsén 2026-06-14 20:48:46 +02:00
commit 37c1390100
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
16 changed files with 966 additions and 216 deletions

View file

@ -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])