from __future__ import annotations import logging import re from typing import TYPE_CHECKING from typing import Literal from django.contrib.humanize.templatetags.humanize import naturaltime from django.contrib.syndication.views import Feed from django.db.models import Prefetch from django.db.models.query import QuerySet from django.urls import reverse from django.utils import feedgenerator from django.utils import timezone from django.utils.html import format_html from django.utils.html import format_html_join from django.utils.safestring import SafeString from django.utils.safestring import SafeText from twitch.models import Channel from twitch.models import ChatBadge from twitch.models import DropBenefit from twitch.models import DropCampaign from twitch.models import Game from twitch.models import Organization from twitch.models import RewardCampaign from twitch.models import TimeBasedDrop if TYPE_CHECKING: import datetime from django.db.models import Model from django.db.models import QuerySet from django.http import HttpRequest from django.http import HttpResponse logger: logging.Logger = logging.getLogger("ttvdrops") def _with_campaign_related(queryset: QuerySet[DropCampaign]) -> QuerySet[DropCampaign]: """Apply related-selects/prefetches needed by feed rendering to avoid N+1 queries. Returns: QuerySet[DropCampaign]: Queryset with related data preloaded for feed rendering. """ drops_prefetch: Prefetch = Prefetch( "time_based_drops", queryset=TimeBasedDrop.objects.prefetch_related("benefits"), ) return queryset.select_related("game").prefetch_related("game__owners", "allow_channels", drops_prefetch) def insert_date_info(item: Model, parts: list[SafeText]) -> None: """Insert start and end date information into parts list. Args: item (Model): The campaign item containing start_at and end_at. parts (list[SafeText]): The list of HTML parts to append to. """ end_at: datetime.datetime | None = getattr(item, "end_at", None) start_at: datetime.datetime | None = getattr(item, "start_at", None) if start_at or end_at: start_part: SafeString = ( format_html("Starts: {} ({})", start_at.strftime("%Y-%m-%d %H:%M %Z"), naturaltime(start_at)) if start_at else SafeText("") ) end_part: SafeString = ( format_html("Ends: {} ({})", end_at.strftime("%Y-%m-%d %H:%M %Z"), naturaltime(end_at)) if end_at else SafeText("") ) # Start date and end date separated by a line break if both present if start_part and end_part: parts.append(format_html("

{}
{}

", start_part, end_part)) elif start_part: parts.append(format_html("

{}

", start_part)) elif end_part: parts.append(format_html("

{}

", end_part)) def _build_drops_data(drops_qs: QuerySet[TimeBasedDrop]) -> list[dict]: """Build a simplified data structure for rendering drops in a template. Returns: list[dict]: A list of dictionaries each containing `name`, `benefits`, `requirements`, and `period` for a drop, suitable for template rendering. """ drops_data: list[dict] = [] for drop in drops_qs: requirements: str = "" required_minutes: int | None = getattr(drop, "required_minutes_watched", None) required_subs: int = getattr(drop, "required_subs", 0) or 0 if required_minutes: requirements = f"{required_minutes} minutes watched" if required_subs > 0: sub_word: Literal["subs", "sub"] = "subs" if required_subs > 1 else "sub" if requirements: requirements += f" and {required_subs} {sub_word} required" else: requirements = f"{required_subs} {sub_word} required" period: str = "" drop_start: datetime.datetime | None = getattr(drop, "start_at", None) drop_end: datetime.datetime | None = getattr(drop, "end_at", None) if drop_start is not None: period += drop_start.strftime("%Y-%m-%d %H:%M %Z") if drop_end is not None: if period: period += " - " + drop_end.strftime("%Y-%m-%d %H:%M %Z") else: period = drop_end.strftime("%Y-%m-%d %H:%M %Z") drops_data.append({ "name": getattr(drop, "name", str(drop)), "benefits": list(drop.benefits.all()), "requirements": requirements, "period": period, }) return drops_data def _build_channels_html(channels: QuerySet[Channel], game: Game | None) -> SafeText: """Render up to max_links channel links as
  • , then a count of additional channels, or fallback to game category link. If only one channel and drop_requirements is '1 subscriptions required', merge the Twitch link with the '1 subs' row. Args: channels (QuerySet[Channel]): The queryset of channels. game (Game | None): The game object for fallback link. Returns: SafeText: HTML