From 6b936f4cf7ff86b94fd00fa368009531095e1149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Mon, 9 Mar 2026 05:56:57 +0100 Subject: [PATCH] Remove /rss/organizations//campaigns/ --- templates/twitch/game_detail.html | 6 - templates/twitch/organization_detail.html | 6 - twitch/feeds.py | 160 +--------------------- twitch/tests/test_feeds.py | 108 --------------- twitch/urls.py | 20 ++- twitch/views.py | 19 --- 6 files changed, 17 insertions(+), 302 deletions(-) diff --git a/templates/twitch/game_detail.html b/templates/twitch/game_detail.html index d8423e4..398a549 100644 --- a/templates/twitch/game_detail.html +++ b/templates/twitch/game_detail.html @@ -13,12 +13,6 @@
RSS feed for {{ game.display_name }} campaigns - {% if owners %} - {% for owner in owners %} - RSS feed for {{ owner.name }} campaigns - {% endfor %} - {% endif %} RSS feed for all campaigns
diff --git a/templates/twitch/organization_detail.html b/templates/twitch/organization_detail.html index 6b8a511..ac1390b 100644 --- a/templates/twitch/organization_detail.html +++ b/templates/twitch/organization_detail.html @@ -4,12 +4,6 @@ {% endblock title %} {% block content %}

{{ organization.name }}

- -
- RSS feed for {{ organization.name }} campaigns -

Games by {{ organization.name }}

diff --git a/twitch/feeds.py b/twitch/feeds.py index 4828521..e92bfc0 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -306,9 +306,8 @@ def _construct_drops_summary( class OrganizationRSSFeed(Feed): """RSS feed for latest organizations.""" - # Spec: https://cyber.harvard.edu/rss/rss.html feed_type = feedgenerator.Rss201rev2Feed - title: str = "TTVDrops Organizations" + title: str = "TTVDrops Twitch Organizations" link: str = "/organizations/" description: str = "Latest organizations on TTVDrops" feed_copyright: str = "Information wants to be free." @@ -378,11 +377,11 @@ class OrganizationRSSFeed(Feed): # MARK: /rss/games/ class GameFeed(Feed): - """RSS feed for latest games.""" + """RSS feed for newly added games.""" title: str = "Games - TTVDrops" link: str = "/games/" - description: str = "Latest games on TTVDrops" + description: str = "Newly added games on TTVDrops" feed_copyright: str = "Information wants to be free." _limit: int | None = None @@ -810,159 +809,6 @@ class GameCampaignFeed(Feed): return "image/jpeg" -# MARK: /rss/organizations//campaigns/ -class OrganizationCampaignFeed(Feed): - """RSS feed for campaigns of a specific organization.""" - - _limit: int | None = None - - def __call__( - self, - request: HttpRequest, - *args: object, - **kwargs: object, - ) -> 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) -> Organization: # noqa: ARG002 - """Retrieve the Organization instance for the given Twitch ID. - - Returns: - Organization: The corresponding Organization object. - """ - return Organization.objects.get(twitch_id=twitch_id) - - def item_link(self, item: DropCampaign) -> str: - """Return the link to the campaign detail.""" - return reverse("twitch:campaign_detail", args=[item.twitch_id]) - - def title(self, obj: Organization) -> str: - """Return the feed title for the organization's campaigns.""" - return f"TTVDrops: {obj.name} Campaigns" - - def link(self, obj: Organization) -> str: - """Return the absolute URL to the organization detail page.""" - return reverse("twitch:organization_detail", args=[obj.twitch_id]) - - def description(self, obj: Organization) -> str: - """Return a description for the feed.""" - return f"Latest drop campaigns for organization {obj.name}" - - def items(self, obj: Organization) -> list[DropCampaign]: - """Return the latest drop campaigns for this organization, ordered by most recent start date (default 200, or limited by ?limit query param).""" - limit: int = self._limit if self._limit is not None else 200 - queryset: QuerySet[DropCampaign] = DropCampaign.objects.filter( - game__owners=obj, - ).order_by("-start_at") - return list(_with_campaign_related(queryset)[:limit]) - - def item_author_name(self, item: DropCampaign) -> str: - """Return the author name for the campaign, typically the game name.""" - return item.get_feed_author_name() - - def item_guid(self, item: DropCampaign) -> str: - """Return a unique identifier for each campaign.""" - return item.get_feed_guid() - - def item_enclosure_url(self, item: DropCampaign) -> str: - """Returns the URL of the campaign image for enclosure.""" - return item.get_feed_enclosure_url() - - def item_enclosure_length(self, item: DropCampaign) -> int: # noqa: ARG002 - """Returns the length of the enclosure.""" - # TODO(TheLovinator): Track image size for proper length # noqa: TD003 - - return 0 - - def item_enclosure_mime_type(self, item: DropCampaign) -> str: # noqa: ARG002 - """Returns the MIME type of the enclosure.""" - # TODO(TheLovinator): Determine actual MIME type if needed # noqa: TD003 - return "image/jpeg" - - def item_categories(self, item: DropCampaign) -> tuple[str, ...]: - """Returns the associated game's name as a category.""" - return item.get_feed_categories() - - def item_updateddate(self, item: DropCampaign) -> datetime.datetime: - """Returns the campaign's last update time.""" - return item.updated_at - - def item_pubdate(self, item: DropCampaign) -> datetime.datetime: - """Returns the publication date to the feed item. - - Uses start_at (when the drop starts). Fallback to added_at or now if missing. - """ - if item.start_at: - return item.start_at - if item.added_at: - return item.added_at - return timezone.now() - - def item_description(self, item: DropCampaign) -> SafeText: - """Return a description of the campaign.""" - drops_data: list[dict] = [] - channels: list[Channel] | None = getattr(item, "channels_ordered", None) - channel_name: str | None = channels[0].name if channels else None - - drops: QuerySet[TimeBasedDrop] | None = getattr(item, "time_based_drops", None) - if drops: - drops_data = _build_drops_data(drops.all()) - - parts: list[SafeText] = [] - - image_url: str | None = getattr(item, "image_url", None) - if image_url: - item_name: str = getattr(item, "name", str(object=item)) - parts.append( - format_html( - '{}', - image_url, - item_name, - ), - ) - - desc_text: str | None = getattr(item, "description", None) - if desc_text: - parts.append(format_html("

{}

", desc_text)) - - # Insert start and end date info - insert_date_info(item, parts) - - if drops_data: - parts.append( - format_html( - "

{}

", - _construct_drops_summary(drops_data, channel_name=channel_name), - ), - ) - - # Only show channels if drop is not subscription only - if not getattr(item, "is_subscription_only", False) and channels is not None: - game: Game | None = getattr(item, "game", None) - parts.append(_build_channels_html(channels, game=game)) - - details_url: str | None = getattr(item, "details_url", None) - if details_url: - parts.append(format_html('About', details_url)) - - return SafeText("".join(str(p) for p in parts)) - - # MARK: /rss/reward-campaigns/ class RewardCampaignFeed(Feed): """RSS feed for latest reward campaigns (Quest rewards).""" diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py index 7492691..9cfaea5 100644 --- a/twitch/tests/test_feeds.py +++ b/twitch/tests/test_feeds.py @@ -115,19 +115,6 @@ class RSSFeedTestCase(TestCase): content: str = response.content.decode("utf-8") assert "Test Game" in content - def test_organization_campaign_feed(self) -> None: - """Test organization-specific campaign feed returns 200.""" - url: str = reverse( - "twitch:organization_campaign_feed", - args=[self.org.twitch_id], - ) - response: _MonkeyPatchedWSGIResponse = self.client.get(url) - assert response.status_code == 200 - assert response["Content-Type"] == "application/rss+xml; charset=utf-8" - # Verify the organization name is in the feed - content: str = response.content.decode("utf-8") - assert "Test Organization" in content - def test_game_campaign_feed_filters_correctly(self) -> None: """Test game campaign feed only shows campaigns for that game.""" # Create another game with a campaign @@ -157,42 +144,6 @@ class RSSFeedTestCase(TestCase): # Should NOT contain other campaign assert "Other Campaign" not in content - def test_organization_campaign_feed_filters_correctly(self) -> None: - """Test organization campaign feed only shows campaigns for that organization.""" - # Create another organization with a game and campaign - other_org = Organization.objects.create( - twitch_id="other-org-123", - name="Other Organization", - ) - other_game = Game.objects.create( - twitch_id="other-game-456", - slug="other-game-2", - name="Other Game 2", - display_name="Other Game 2", - ) - other_game.owners.add(other_org) - DropCampaign.objects.create( - twitch_id="other-campaign-456", - name="Other Campaign 2", - game=other_game, - start_at=timezone.now(), - end_at=timezone.now() + timedelta(days=7), - operation_names=["DropCampaignDetails"], - ) - - # Get feed for first organization - url: str = reverse( - "twitch:organization_campaign_feed", - args=[self.org.twitch_id], - ) - response: _MonkeyPatchedWSGIResponse = self.client.get(url) - content: str = response.content.decode("utf-8") - - # Should contain first campaign - assert "Test Campaign" in content - # Should NOT contain other campaign - assert "Other Campaign 2" not in content - QueryAsserter = Callable[..., AbstractContextManager[object]] @@ -447,64 +398,6 @@ def test_game_feed_queries_bounded( assert response.status_code == 200 -@pytest.mark.django_db -def test_organization_campaign_feed_queries_bounded( - client: Client, - django_assert_num_queries: QueryAsserter, -) -> None: - """Organization campaign feed should not regress in query count.""" - org: Organization = Organization.objects.create( - twitch_id="org-campaign-feed", - name="Org Campaign Feed", - ) - game: Game = Game.objects.create( - twitch_id="org-campaign-game", - slug="org-campaign-game", - name="Org Campaign Game", - display_name="Org Campaign Game", - ) - game.owners.add(org) - - for i in range(3): - _build_campaign(game, i) - - url: str = reverse("twitch:organization_campaign_feed", args=[org.twitch_id]) - # TODO(TheLovinator): 12 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: TD003 - with django_assert_num_queries(12, exact=False): - response: _MonkeyPatchedWSGIResponse = client.get(url) - - assert response.status_code == 200 - - -@pytest.mark.django_db -def test_organization_campaign_feed_queries_do_not_scale_with_items( - client: Client, - django_assert_num_queries: QueryAsserter, -) -> None: - """Organization campaign RSS feed query count should remain bounded as item count grows.""" - org: Organization = Organization.objects.create( - twitch_id="test-org-org-scale-queries", - name="Org Scale Query Org", - ) - game: Game = Game.objects.create( - twitch_id="test-game-org-scale-queries", - slug="org-scale-game", - name="Org Scale Game", - display_name="Org Scale Game", - ) - game.owners.add(org) - - for i in range(50): - _build_campaign(game, i) - - url: str = reverse("twitch:organization_campaign_feed", args=[org.twitch_id]) - - with django_assert_num_queries(15, exact=False): - response: _MonkeyPatchedWSGIResponse = client.get(url) - - assert response.status_code == 200 - - @pytest.mark.django_db def test_reward_campaign_feed_queries_bounded( client: Client, @@ -591,7 +484,6 @@ URL_NAMES: list[tuple[str, dict[str, str]]] = [ ("twitch:game_feed", {}), ("twitch:game_campaign_feed", {"twitch_id": "test-game-123"}), ("twitch:organization_feed", {}), - ("twitch:organization_campaign_feed", {"twitch_id": "test-org-123"}), ("twitch:reward_campaign_feed", {}), ] diff --git a/twitch/urls.py b/twitch/urls.py index f414bb5..45543de 100644 --- a/twitch/urls.py +++ b/twitch/urls.py @@ -6,7 +6,6 @@ from twitch import views from twitch.feeds import DropCampaignFeed from twitch.feeds import GameCampaignFeed from twitch.feeds import GameFeed -from twitch.feeds import OrganizationCampaignFeed from twitch.feeds import OrganizationRSSFeed from twitch.feeds import RewardCampaignFeed @@ -83,18 +82,27 @@ urlpatterns: list[URLPattern] = [ views.export_organizations_json, name="export_organizations_json", ), + # RSS feeds + # /rss/campaigns/ - all active campaigns path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"), + # /rss/games/ - newly added games path("rss/games/", GameFeed(), name="game_feed"), + # /rss/games//campaigns/ - active campaigns for a specific game path( "rss/games//campaigns/", GameCampaignFeed(), name="game_campaign_feed", ), - path("rss/organizations/", OrganizationRSSFeed(), name="organization_feed"), + # /rss/organizations/ - newly added organizations path( - "rss/organizations//campaigns/", - OrganizationCampaignFeed(), - name="organization_campaign_feed", + "rss/organizations/", + OrganizationRSSFeed(), + name="organization_feed", + ), + # /rss/reward-campaigns/ - all active reward campaigns + path( + "rss/reward-campaigns/", + RewardCampaignFeed(), + name="reward_campaign_feed", ), - path("rss/reward-campaigns/", RewardCampaignFeed(), name="reward_campaign_feed"), ] diff --git a/twitch/views.py b/twitch/views.py index 4d46e64..f031b07 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -41,7 +41,6 @@ from pygments.lexers.data import JsonLexer from twitch.feeds import DropCampaignFeed from twitch.feeds import GameCampaignFeed from twitch.feeds import GameFeed -from twitch.feeds import OrganizationCampaignFeed from twitch.feeds import OrganizationRSSFeed from twitch.feeds import RewardCampaignFeed from twitch.models import Channel @@ -1857,24 +1856,6 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse: if sample_game else "", }, - { - "title": "Campaigns for an Organization", - "description": "Drop campaigns across games owned by one organization.", - "url": ( - absolute( - reverse( - "twitch:organization_campaign_feed", - args=[sample_org.twitch_id], - ), - ) - if sample_org - else absolute("/rss/organizations//campaigns/") - ), - "has_sample": bool(sample_org), - "example_xml": render_feed(OrganizationCampaignFeed(), sample_org.twitch_id) - if sample_org - else "", - }, ] seo_context: dict[str, Any] = _build_seo_context(