diff --git a/twitch/feeds.py b/twitch/feeds.py index e92bfc0..0f54931 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -409,12 +409,14 @@ class GameFeed(Feed): return super().__call__(request, *args, **kwargs) def items(self) -> list[Game]: - """Return the latest games (default 200, or limited by ?limit query param).""" - limit: int = self._limit if self._limit is not None else 200 - return list(Game.objects.order_by("-added_at")[:limit]) + """Return the latest games (default 20, or limited by ?limit query param).""" + limit: int = self._limit if self._limit is not None else 20 + return list( + Game.objects.prefetch_related("owners").order_by("-added_at")[:limit], + ) def item_title(self, item: Game) -> SafeText: - """Return the game name as the item title (SafeText for RSS).""" + """Return the game name as the item title.""" return SafeText(item.get_game_name) def item_description(self, item: Game) -> SafeText: @@ -424,7 +426,7 @@ class GameFeed(Feed): name: str = getattr(item, "name", "") display_name: str = getattr(item, "display_name", "") box_art: str = item.box_art_best_url - owner: Organization | None = getattr(item, "owner", None) + owner: Organization | None = item.owners.first() description_parts: list[SafeText] = [] @@ -438,17 +440,28 @@ class GameFeed(Feed): ), ) + # Get the full URL for TTVDrops game detail page + game_url: str = reverse("twitch:game_detail", args=[twitch_id]) + rss_feed_url: str = reverse("twitch:game_campaign_feed", args=[twitch_id]) + twitch_directory_url: str = getattr(item, "twitch_directory_url", "") if slug: description_parts.append( SafeText( - f"
", + f"New game has been added to ttvdrops.lovinator.space: {game_name} by {game_owner}\n" + f"Game Details\n" + f"Twitch\n" + f"RSS feed\n
", ), ) else: - description_parts.append(SafeText(f"{game_name} by {game_owner}
")) - - if twitch_id: - description_parts.append(SafeText(f"Twitch ID: {twitch_id}")) + description_parts.append( + SafeText( + f"A new game has been added to ttvdrops.lovinator.space: {game_name} by {game_owner}\n" + f"Game Details\n" + f"Twitch\n" + f"RSS feed\n
", + ), + ) return SafeText("".join(str(part) for part in description_parts)) @@ -480,7 +493,7 @@ class GameFeed(Feed): def item_author_name(self, item: Game) -> str: """Return the author name for the game, typically the owner organization name.""" - owner: Organization | None = getattr(item, "owner", None) + owner: Organization | None = item.owners.first() if owner and owner.name: return owner.name diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py index 9cfaea5..ed4d7f0 100644 --- a/twitch/tests/test_feeds.py +++ b/twitch/tests/test_feeds.py @@ -64,6 +64,14 @@ class RSSFeedTestCase(TestCase): response: _MonkeyPatchedWSGIResponse = self.client.get(url) assert response.status_code == 200 assert response["Content-Type"] == "application/rss+xml; charset=utf-8" + content: str = response.content.decode("utf-8") + assert "Test Game by Test Organization" in content + + expected_rss_link: str = reverse( + "twitch:game_campaign_feed", + args=[self.game.twitch_id], + ) + assert expected_rss_link in content def test_campaign_feed(self) -> None: """Test campaign feed returns 200.""" @@ -392,7 +400,8 @@ def test_game_feed_queries_bounded( game.owners.add(org) url: str = reverse("twitch:game_feed") - with django_assert_num_queries(1, exact=True): + # One query for games + one prefetch query for owners. + with django_assert_num_queries(2, exact=True): response: _MonkeyPatchedWSGIResponse = client.get(url) assert response.status_code == 200 diff --git a/twitch/tests/test_views.py b/twitch/tests/test_views.py index 2cc5c80..1f32860 100644 --- a/twitch/tests/test_views.py +++ b/twitch/tests/test_views.py @@ -930,6 +930,36 @@ class TestChannelListView: assert response.status_code == 200 assert "game" in response.context + @pytest.mark.django_db + def test_game_detail_view_serializes_owners_field( + self, + client: Client, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Game detail JSON payload should use `owners` (M2M), not stale `owner`.""" + org: Organization = Organization.objects.create( + twitch_id="org-game-detail", + name="Org Game Detail", + ) + game: Game = Game.objects.create( + twitch_id="g2-owners", + name="Game2 Owners", + display_name="Game2 Owners", + ) + game.owners.add(org) + + monkeypatch.setattr("twitch.views.format_and_color_json", lambda data: data) + + url: str = reverse("twitch:game_detail", args=[game.twitch_id]) + response: _MonkeyPatchedWSGIResponse = client.get(url) + assert response.status_code == 200 + + game_data: dict[str, Any] = response.context["game_data"] + fields: dict[str, Any] = game_data["fields"] + assert "owners" in fields + assert fields["owners"] == [org.pk] + assert "owner" not in fields + @pytest.mark.django_db def test_org_list_view(self, client: Client) -> None: """Test org list view returns 200 and has orgs in context.""" diff --git a/twitch/views.py b/twitch/views.py index f031b07..7e1560e 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -1216,7 +1216,7 @@ class GameDetailView(DetailView): "name", "display_name", "box_art", - "owner", + "owners", "added_at", "updated_at", ),