From e2283eb9202c9d7f82a3b7ee7912e8cd24cfbc24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Fri, 28 Nov 2025 17:21:45 +0100 Subject: [PATCH] Fix type errors --- twitch/feeds.py | 232 ++++++++++++++++++++++++------------------------ twitch/views.py | 63 +++++++------ 2 files changed, 149 insertions(+), 146 deletions(-) diff --git a/twitch/feeds.py b/twitch/feeds.py index c11f36a..9f593f8 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -3,36 +3,41 @@ from __future__ import annotations from typing import TYPE_CHECKING from django.contrib.syndication.views import Feed +from django.db.models.query import QuerySet from django.urls import reverse +from django.utils import timezone from django.utils.html import format_html +from django.utils.safestring import SafeText -from twitch.models import DropCampaign, Game, Organization +from twitch.models import DropCampaign, Game, Organization, TimeBasedDrop if TYPE_CHECKING: import datetime + from django.db.models import Model, QuerySet + # MARK: /rss/organizations/ class OrganizationFeed(Feed): """RSS feed for latest organizations.""" - title = "TTVDrops Organizations" - link = "/organizations/" - description = "Latest organizations on TTVDrops" + title: str = "TTVDrops Organizations" + link: str = "/organizations/" + description: str = "Latest organizations on TTVDrops" def items(self) -> list[Organization]: """Return the latest 100 organizations.""" return list(Organization.objects.order_by("-id")[:100]) - def item_title(self, item: Organization) -> str: + def item_title(self, item: Model) -> SafeText: """Return the organization name as the item title.""" - return item.name + return SafeText(getattr(item, "name", str(item))) - def item_description(self, item: Organization) -> str: + def item_description(self, item: Model) -> SafeText: """Return a description of the organization.""" - return f"Organization {item.name}" + return SafeText(f"Organization {getattr(item, 'name', str(item))}") - def item_link(self, item: Organization) -> str: + def item_link(self, item: Model) -> str: """Return the link to the organization detail.""" return reverse("twitch:organization_detail", args=[item.pk]) @@ -41,23 +46,23 @@ class OrganizationFeed(Feed): class GameFeed(Feed): """RSS feed for latest games.""" - title = "TTVDrops Games" - link = "/games/" - description = "Latest games on TTVDrops" + title: str = "TTVDrops Games" + link: str = "/games/" + description: str = "Latest games on TTVDrops" def items(self) -> list[Game]: """Return the latest 100 games.""" return list(Game.objects.order_by("-id")[:100]) - def item_title(self, item: Game) -> str: - """Return the game name as the item title.""" - return str(item) + def item_title(self, item: Model) -> SafeText: + """Return the game name as the item title (SafeText for RSS).""" + return SafeText(str(item)) - def item_description(self, item: Game) -> str: + def item_description(self, item: Model) -> SafeText: """Return a description of the game.""" - return f"Game {item.display_name}" + return SafeText(f"Game {getattr(item, 'display_name', str(item))}") - def item_link(self, item: Game) -> str: + def item_link(self, item: Model) -> str: """Return the link to the game detail.""" return reverse("twitch:game_detail", args=[item.pk]) @@ -66,117 +71,116 @@ class GameFeed(Feed): class DropCampaignFeed(Feed): """RSS feed for latest drop campaigns.""" - title = "Twitch Drop Campaigns" - link = "/campaigns/" - description = "Latest Twitch drop campaigns" - feed_url = "/rss/campaigns/" - feed_copyright = "Information wants to be free." + title: str = "Twitch Drop Campaigns" + link: str = "/campaigns/" + description: str = "Latest Twitch drop campaigns" + feed_url: str = "/rss/campaigns/" + feed_copyright: str = "Information wants to be free." def items(self) -> list[DropCampaign]: """Return the latest 100 drop campaigns.""" return list(DropCampaign.objects.select_related("game").order_by("-added_at")[:100]) - def item_title(self, item: DropCampaign) -> str: - """Return the campaign name as the item title.""" - return f"{item.game.display_name}: {item.clean_name}" + def item_title(self, item: Model) -> SafeText: + """Return the campaign name as the item title (SafeText for RSS).""" + game: Game | None = getattr(item, "game", None) + game_name: str = getattr(game, "display_name", str(game)) if game else "" + clean_name: str = getattr(item, "clean_name", str(item)) + return SafeText(f"{game_name}: {clean_name}") - def item_description(self, item: DropCampaign) -> str: + def item_description(self, item: Model) -> SafeText: # noqa: PLR0915 """Return a description of the campaign.""" - description = "" - - # Include the campaign image if available - if item.image_url: + description: str = "" + image_url: str | None = getattr(item, "image_url", None) + name: str = getattr(item, "name", str(item)) + if image_url: description += format_html( '{}

', - item.image_url, - item.name, + image_url, + name, ) - - # Add the campaign description text - description += format_html("

{}

", item.description) if item.description else "" - - # Add start and end dates for clarity - if item.start_at: - description += f"

Starts: {item.start_at.strftime('%Y-%m-%d %H:%M %Z')}

" - if item.end_at: - description += f"

Ends: {item.end_at.strftime('%Y-%m-%d %H:%M %Z')}

" - - # Add information about the drops in this campaign - drops = item.time_based_drops.select_related().prefetch_related("benefits").all() # type: ignore[attr-defined] + desc_text: str | None = getattr(item, "description", None) + if desc_text: + description += format_html("

{}

", desc_text) + start_at: datetime.datetime | None = getattr(item, "start_at", None) + end_at: datetime.datetime | None = getattr(item, "end_at", None) + if start_at: + description += f"

Starts: {start_at.strftime('%Y-%m-%d %H:%M %Z')}

" + if end_at: + description += f"

Ends: {end_at.strftime('%Y-%m-%d %H:%M %Z')}

" + drops: QuerySet[TimeBasedDrop] | None = getattr(item, "time_based_drops", None) if drops: - description += "

Drops in this campaign:

" - table_header = ( - '' - "" - '' - '' - '' - '' - "" - ) - description += table_header + drops_qs: QuerySet[TimeBasedDrop] = drops.select_related().prefetch_related("benefits").all() + if drops_qs: + description += "

Drops in this campaign:

" + table_header = ( + '
BenefitsDrop NameRequirementsPeriod
' + "" + '' + '' + '' + '' + "" + ) + description += table_header + for drop in drops_qs: + description += "" + description += '" + description += f'' + requirements: str = "" + if getattr(drop, "required_minutes_watched", None): + requirements = f"{drop.required_minutes_watched} minutes watched" + if getattr(drop, "required_subs", 0) > 0: + if requirements: + requirements += f" and {drop.required_subs} subscriptions required" + else: + requirements = f"{drop.required_subs} subscriptions required" + description += f'' + period: str = "" + start_at = getattr(drop, "start_at", None) + end_at = getattr(drop, "end_at", None) + if start_at is not None: + period += start_at.strftime("%Y-%m-%d %H:%M %Z") + if end_at is not None: + if period: + period += " - " + end_at.strftime("%Y-%m-%d %H:%M %Z") + else: + period = end_at.strftime("%Y-%m-%d %H:%M %Z") + description += f'' + description += "" + description += "
BenefitsDrop NameRequirementsPeriod
' + for benefit in drop.benefits.all(): + if getattr(benefit, "image_asset_url", None): + description += format_html( + '{}', + benefit.image_asset_url, + benefit.name, + ) + else: + placeholder_img = ( + 'No Image Available' + ) + description += placeholder_img + description += "{getattr(drop, "name", str(drop))}{requirements}{period}

" + details_url: str | None = getattr(item, "details_url", None) + if details_url: + description += format_html('

About this drop

', details_url) + return SafeText(description) - for drop in drops: - description += "" - # Benefits column with images - description += '' - for benefit in drop.benefits.all(): - if benefit.image_asset_url: - description += format_html( - '{}', - benefit.image_asset_url, - benefit.name, - ) - else: - placeholder_img = ( - 'No Image Available' - ) - description += placeholder_img - description += "" - - # Drop name - description += f'{drop.name}' - - # Requirements - requirements = "" - if drop.required_minutes_watched: - requirements = f"{drop.required_minutes_watched} minutes watched" - if drop.required_subs > 0: - if requirements: - requirements += f" and {drop.required_subs} subscriptions required" - else: - requirements = f"{drop.required_subs} subscriptions required" - description += f'{requirements}' - - # Period - period = "" - if drop.start_at: - period += drop.start_at.strftime("%Y-%m-%d %H:%M %Z") - if drop.end_at: - if period: - period += " - " + drop.end_at.strftime("%Y-%m-%d %H:%M %Z") - else: - period = drop.end_at.strftime("%Y-%m-%d %H:%M %Z") - description += f'{period}' - - description += "" - - description += "
" - - # Add a clear link to the campaign details page - if item.details_url: - description += format_html('

About this drop

', item.details_url) - - return f"{description}" - - def item_link(self, item: DropCampaign) -> str: + def item_link(self, item: Model) -> str: """Return the link to the campaign detail.""" return reverse("twitch:campaign_detail", args=[item.pk]) - def item_pubdate(self, item: DropCampaign) -> datetime.datetime: - """Returns the publication date to the feed item.""" - return item.start_at + def item_pubdate(self, item: Model) -> datetime.datetime: + """Returns the publication date to the feed item. Fallback to updated_at or now if missing.""" + start_at: datetime.datetime | None = getattr(item, "start_at", None) + if start_at: + return start_at + updated_at: datetime.datetime | None = getattr(item, "updated_at", None) + if updated_at: + return updated_at + return timezone.now() def item_updateddate(self, item: DropCampaign) -> datetime.datetime: """Returns the campaign's last update time.""" diff --git a/twitch/views.py b/twitch/views.py index e77f40d..7b527ef 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector from django.core.serializers import serialize -from django.db.models import Count, F, Prefetch, Q +from django.db.models import BaseManager, Count, F, Model, Prefetch, Q from django.db.models.functions import Trim from django.db.models.query import QuerySet from django.http import HttpRequest, HttpResponse @@ -118,28 +118,27 @@ class OrgDetailView(DetailView): Returns: dict: Context data. """ - context = super().get_context_data(**kwargs) - organization: Organization = self.object + context: dict[str, Any] = super().get_context_data(**kwargs) + organization: Organization = self.get_object() # pyright: ignore[reportAssignmentType] + games: QuerySet[Game] = organization.games.all() # pyright: ignore[reportAttributeAccessIssue] - games: QuerySet[Game, Game] = organization.games.all() # pyright: ignore[reportAttributeAccessIssue] - - serialized_org = serialize( + serialized_org: str = serialize( "json", [organization], fields=("name",), ) - org_data = json.loads(serialized_org) + org_data: list[dict] = json.loads(serialized_org) if games.exists(): - serialized_games = serialize( + serialized_games: str = serialize( "json", games, fields=("slug", "name", "display_name", "box_art"), ) - games_data = json.loads(serialized_games) + games_data: list[dict] = json.loads(serialized_games) org_data[0]["fields"]["games"] = games_data - pretty_org_data = json.dumps(org_data[0], indent=4) + pretty_org_data: str = json.dumps(org_data[0], indent=4) context.update({ "games": games, @@ -186,9 +185,9 @@ class DropCampaignListView(ListView): context["games"] = Game.objects.all().order_by("display_name") context["status_options"] = ["active", "upcoming", "expired"] context["now"] = timezone.now() - context["selected_game"] = str(self.request.GET.get(key="game", default="")) + context["selected_game"] = self.request.GET.get("game", "") context["selected_per_page"] = self.paginate_by - context["selected_status"] = self.request.GET.get(key="status", default="") + context["selected_status"] = self.request.GET.get("status", "") return context @@ -214,7 +213,7 @@ class DropCampaignDetailView(DetailView): template_name = "twitch/campaign_detail.html" context_object_name = "campaign" - def get_object(self, queryset: QuerySet[DropCampaign] | None = None) -> DropCampaign: + def get_object(self, queryset: QuerySet[DropCampaign] | None = None) -> Model: """Get the campaign object with related data prefetched. Args: @@ -240,8 +239,8 @@ class DropCampaignDetailView(DetailView): dict: Context data. """ context: dict[str, Any] = super().get_context_data(**kwargs) - campaign = context["campaign"] - drops: QuerySet[TimeBasedDrop, TimeBasedDrop] = ( + campaign: DropCampaign = context["campaign"] + drops: BaseManager[TimeBasedDrop] = ( TimeBasedDrop.objects.filter(campaign=campaign).select_related("campaign").prefetch_related("benefits").order_by("required_minutes_watched") ) @@ -276,11 +275,11 @@ class DropCampaignDetailView(DetailView): "end_at", ), ) - drops_data = json.loads(serialized_drops) + drops_data: list[dict[str, Any]] = json.loads(serialized_drops) for i, drop in enumerate(drops): - benefits = drop.benefits.all() - if benefits.exists(): + benefits: list[DropBenefit] = list(drop.benefits.all()) + if benefits: serialized_benefits = serialize( "json", benefits, @@ -292,11 +291,11 @@ class DropCampaignDetailView(DetailView): campaign_data[0]["fields"]["drops"] = drops_data # Enhance drops with additional context data - enhanced_drops = [] + enhanced_drops: list[dict[str, TimeBasedDrop | datetime.datetime | str | None]] = [] now: datetime.datetime = timezone.now() for drop in drops: # Ensure benefits are loaded - benefits = list(drop.benefits.all()) + benefits: list[DropBenefit] = list(drop.benefits.all()) # Calculate countdown text if drop.end_at and drop.end_at > now: @@ -318,7 +317,7 @@ class DropCampaignDetailView(DetailView): else: countdown_text = "Expired" - enhanced_drop: dict[str, str | datetime.datetime | TimeBasedDrop] = { + enhanced_drop: dict[str, TimeBasedDrop | datetime.datetime | str | None] = { "drop": drop, "local_start": drop.start_at, "local_end": drop.end_at, @@ -380,7 +379,7 @@ class GamesGridView(ListView): context: dict[str, Any] = super().get_context_data(**kwargs) now: datetime.datetime = timezone.now() - games_with_campaigns: QuerySet[Game, Game] = ( + games_with_campaigns: BaseManager[Game] = ( Game.objects.filter(drop_campaigns__isnull=False) .select_related("owner") .annotate( @@ -426,10 +425,10 @@ class GameDetailView(DetailView): Expired campaigns are filtered based on either end date or status. """ context: dict[str, Any] = super().get_context_data(**kwargs) - game: Game = self.get_object() + game: Game = self.get_object() # pyright: ignore[reportAssignmentType] now: datetime.datetime = timezone.now() - all_campaigns: QuerySet[DropCampaign, DropCampaign] = ( + all_campaigns: BaseManager[DropCampaign] = ( DropCampaign.objects.filter(game=game) .select_related("game__owner") .prefetch_related( @@ -456,14 +455,14 @@ class GameDetailView(DetailView): # Add unique sorted benefits to each campaign object for campaign in all_campaigns: - benefits_dict = {} # Use dict to track unique benefits by ID + benefits_dict: dict[int, DropBenefit] = {} # Use dict to track unique benefits by ID for drop in campaign.time_based_drops.all(): # type: ignore[attr-defined] for benefit in drop.benefits.all(): benefits_dict[benefit.id] = benefit # Sort benefits by name and attach to campaign campaign.sorted_benefits = sorted(benefits_dict.values(), key=lambda b: b.name) # type: ignore[attr-defined] - serialized_game = serialize( + serialized_game: str = serialize( "json", [game], fields=( @@ -474,7 +473,7 @@ class GameDetailView(DetailView): "owner", ), ) - game_data = json.loads(serialized_game) + game_data: list[dict[str, Any]] = json.loads(serialized_game) if all_campaigns.exists(): serialized_campaigns = serialize( @@ -491,7 +490,7 @@ class GameDetailView(DetailView): "is_account_connected", ), ) - campaigns_data = json.loads(serialized_campaigns) + campaigns_data: list[dict[str, Any]] = json.loads(serialized_campaigns) game_data[0]["fields"]["campaigns"] = campaigns_data context.update({ @@ -500,7 +499,7 @@ class GameDetailView(DetailView): "expired_campaigns": expired_campaigns, "owner": game.owner, "now": now, - "game_data": format_and_color_json(game_data[0]), + "game_data": format_and_color_json(json.dumps(game_data[0], indent=4)), }) return context @@ -718,10 +717,10 @@ class ChannelDetailView(DetailView): dict: Context data with active, upcoming, and expired campaigns for this channel. """ context: dict[str, Any] = super().get_context_data(**kwargs) - channel: Channel = self.get_object() + channel: Channel = self.get_object() # pyright: ignore[reportAssignmentType] now: datetime.datetime = timezone.now() - all_campaigns: QuerySet[DropCampaign, DropCampaign] = ( + all_campaigns: QuerySet[DropCampaign] = ( DropCampaign.objects.filter(allow_channels=channel) .select_related("game__owner") .prefetch_related( @@ -749,7 +748,7 @@ class ChannelDetailView(DetailView): # Add unique sorted benefits to each campaign object for campaign in all_campaigns: - benefits_dict = {} # Use dict to track unique benefits by ID + benefits_dict: dict[int, DropBenefit] = {} # Use dict to track unique benefits by ID for drop in campaign.time_based_drops.all(): # type: ignore[attr-defined] for benefit in drop.benefits.all(): benefits_dict[benefit.id] = benefit