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 @@
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 }}
-
-
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(