Add RSS feed links in templates
This commit is contained in:
parent
da923f82da
commit
6a62eaa885
13 changed files with 349 additions and 39 deletions
|
|
@ -17,6 +17,19 @@
|
|||
<a href="{% url 'twitch:organization_detail' owner.twitch_id %}">{{ owner.name }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
{% if campaign.game %}
|
||||
<a href="{% url 'twitch:game_campaign_feed' campaign.game.twitch_id %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for {{ campaign.game.display_name }} campaigns">RSS feed for {{ campaign.game.display_name }} campaigns</a>
|
||||
{% endif %}
|
||||
{% if owner %}
|
||||
<a href="{% url 'twitch:organization_campaign_feed' owner.twitch_id %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for {{ owner.name }} campaigns">RSS feed for {{ owner.name }} campaigns</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Campaign image -->
|
||||
{% if campaign.image_best_url or campaign.image_url %}
|
||||
<img id="campaign-image"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
<header>
|
||||
<h1 id="page-title">Drop Campaigns</h1>
|
||||
<p>Browse all available drop campaigns</p>
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{% url 'twitch:campaign_feed' %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for all campaigns">RSS feed for all campaigns</a>
|
||||
</div>
|
||||
</header>
|
||||
<form id="filter-form"
|
||||
method="get"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@
|
|||
Drops are sorted alphabetically by organization and game. Click on a campaign or game title to see more details.
|
||||
Hover over the end time to see the exact date and time.
|
||||
</pre>
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{% url 'twitch:campaign_feed' %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for all campaigns">RSS feed for campaigns</a>
|
||||
</div>
|
||||
{% if campaigns_by_org_game %}
|
||||
{% for org_id, org_data in campaigns_by_org_game.items %}
|
||||
<section id="org-section-{{ org_id }}">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
<h1 id="page-title">RSS Feeds Documentation</h1>
|
||||
<p>This page lists all available RSS feeds for TTVDrops.</p>
|
||||
<section>
|
||||
<h2 id="available-feeds-header">Available RSS Feeds</h2>
|
||||
<h2 id="available-feeds-header">Global RSS Feeds</h2>
|
||||
<p>These feeds contain all items across the entire site:</p>
|
||||
<ul id="feeds-list">
|
||||
{% for feed in feeds %}
|
||||
<li id="feed-{{ forloop.counter }}">
|
||||
|
|
@ -21,5 +22,40 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
<section style="margin-top: 2rem;">
|
||||
<h2 id="filtered-feeds-header">Filtered RSS Feeds</h2>
|
||||
<p>
|
||||
You can also subscribe to RSS feeds for specific games or organizations. These feeds are available on each game or organization detail page.
|
||||
</p>
|
||||
<h3>Game-Specific Campaign Feeds</h3>
|
||||
<p>
|
||||
Subscribe to campaigns for a specific game using: <code>/rss/games/<game_id>/campaigns/</code>
|
||||
</p>
|
||||
{% if sample_game %}
|
||||
<p>
|
||||
Example: <a href="{% url 'twitch:game_campaign_feed' sample_game.twitch_id %}">{{ sample_game.display_name }} Campaigns RSS Feed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<h3>Organization-Specific Campaign Feeds</h3>
|
||||
<p>
|
||||
Subscribe to campaigns for a specific organization using: <code>/rss/organizations/<org_id>/campaigns/</code>
|
||||
</p>
|
||||
{% if sample_org %}
|
||||
<p>
|
||||
Example: <a href="{% url 'twitch:organization_campaign_feed' sample_org.twitch_id %}">{{ sample_org.name }} Campaigns RSS Feed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<section style="margin-top: 2rem;">
|
||||
<h2 id="usage-header">How to Use RSS Feeds</h2>
|
||||
<p>
|
||||
RSS feeds allow you to stay updated with new content. You can use any RSS reader application to subscribe to these feeds.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Copy the feed URL</li>
|
||||
<li>Paste it into your favorite RSS reader (Feedly, Inoreader, NetNewsWire, etc.)</li>
|
||||
<li>Get automatic updates when new content is added</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,20 @@
|
|||
{{ game.display_name }}
|
||||
{% if game.display_name != game.name and game.name %}<small>({{ game.name }})</small>{% endif %}
|
||||
</h1>
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{% url 'twitch:game_campaign_feed' game.twitch_id %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for {{ game.display_name }} campaigns">RSS feed for {{ game.display_name }} campaigns</a>
|
||||
{% if owner %}
|
||||
<a href="{% url 'twitch:organization_campaign_feed' owner.twitch_id %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for {{ owner.name }} campaigns">RSS feed for {{ owner.name }} campaigns</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'twitch:campaign_feed' %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for all campaigns">RSS feed for all campaigns</a>
|
||||
</div>
|
||||
<!-- Game image -->
|
||||
{% if game.box_art %}
|
||||
<img id="game-image"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@
|
|||
<p>
|
||||
<a href="{% url 'twitch:game_list_simple' %}">List View</a>
|
||||
</p>
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{% url 'twitch:game_feed' %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for all games">RSS feed for all games</a>
|
||||
</div>
|
||||
</header>
|
||||
{% if games_by_org %}
|
||||
<section>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
<p>
|
||||
<a href="{% url 'twitch:game_list' %}">Grid View</a>
|
||||
</p>
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{% url 'twitch:game_feed' %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for all games">RSS feed for all games</a>
|
||||
</div>
|
||||
{% if games_by_org %}
|
||||
{% for organization, games in games_by_org.items %}
|
||||
<h2 id="org-{{ organization.twitch_id }}">{{ organization.name }}</h2>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@
|
|||
{% endblock title %}
|
||||
{% block content %}
|
||||
<h1 id="page-title">Organizations</h1>
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{% url 'twitch:organization_feed' %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for all organizations">RSS feed for organizations</a>
|
||||
</div>
|
||||
{% if orgs %}
|
||||
<ul id="org-list">
|
||||
{% for organization in orgs %}
|
||||
|
|
|
|||
|
|
@ -4,36 +4,26 @@
|
|||
{% endblock title %}
|
||||
{% block content %}
|
||||
<h1 id="org-name">{{ organization.name }}</h1>
|
||||
{% if user.is_authenticated %}
|
||||
<form id="notification-form"
|
||||
method="post"
|
||||
action="{% url 'twitch:subscribe_org_notifications' org_id=organization.twitch_id %}">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<input type="checkbox"
|
||||
id="found"
|
||||
name="notify_found"
|
||||
{% if subscription and subscription.notify_found %}checked{% endif %} />
|
||||
<label for="found">🔔 Get notified as soon as a drop for {{ organization.name }} appears on Twitch.</label>
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{% url 'twitch:organization_campaign_feed' organization.twitch_id %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for {{ organization.name }} campaigns">RSS feed for {{ organization.name }} campaigns</a>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox"
|
||||
id="live"
|
||||
name="notify_live"
|
||||
{% if subscription and subscription.notify_live %}checked{% endif %} />
|
||||
<label for="live">🎮 Get notified when the drop is live and ready to be farmed.</label>
|
||||
</div>
|
||||
<button id="save-preferences-button" type="submit">Save preferences</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p id="login-prompt">Login to subscribe!</p>
|
||||
{% endif %}
|
||||
<ul id="games-list">
|
||||
<theader>
|
||||
<h2 id="games-header">Games by {{ organization.name }}</h2>
|
||||
</theader>
|
||||
<table id="games-table">
|
||||
<tbody>
|
||||
{% for game in games %}
|
||||
<li id="game-{{ game.twitch_id }}">
|
||||
<tr id="game-row-{{ game.twitch_id }}">
|
||||
<td>
|
||||
<a href="{% url 'twitch:game_detail' game.twitch_id %}">{{ game }}</a>
|
||||
</li>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
{{ org_data|safe }}
|
||||
{% endblock content %}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
|||
|
||||
from django.db.models import Model
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpRequest
|
||||
|
||||
|
||||
# MARK: /rss/organizations/
|
||||
|
|
@ -239,3 +240,73 @@ class DropCampaignFeed(Feed):
|
|||
def item_enclosure_mime_type(self, item: DropCampaign) -> str: # noqa: ARG002
|
||||
"""Returns the MIME type of the enclosure."""
|
||||
return "image/jpeg"
|
||||
|
||||
|
||||
# MARK: /rss/games/<twitch_id>/campaigns/
|
||||
class GameCampaignFeed(DropCampaignFeed):
|
||||
"""RSS feed for campaigns of a specific game."""
|
||||
|
||||
def get_object(self, request: HttpRequest, twitch_id: str) -> Game: # noqa: ARG002
|
||||
"""Get the game object for this feed.
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
twitch_id: The Twitch ID of the game.
|
||||
|
||||
Returns:
|
||||
Game: The game object.
|
||||
"""
|
||||
return Game.objects.get(twitch_id=twitch_id)
|
||||
|
||||
def title(self, obj: Game) -> str:
|
||||
"""Return the feed title."""
|
||||
return f"TTVDrops: {obj.display_name} Campaigns"
|
||||
|
||||
def link(self, obj: Game) -> str:
|
||||
"""Return the link to the game detail."""
|
||||
return reverse("twitch:game_detail", args=[obj.twitch_id])
|
||||
|
||||
def description(self, obj: Game) -> str:
|
||||
"""Return the feed description."""
|
||||
return f"Latest drop campaigns for {obj.display_name}"
|
||||
|
||||
def items(self, obj: Game) -> list[DropCampaign]:
|
||||
"""Return the latest 100 campaigns for this game."""
|
||||
return list(
|
||||
DropCampaign.objects.filter(game=obj).select_related("game").order_by("-added_at")[:100],
|
||||
)
|
||||
|
||||
|
||||
# MARK: /rss/organizations/<twitch_id>/campaigns/
|
||||
class OrganizationCampaignFeed(DropCampaignFeed):
|
||||
"""RSS feed for campaigns of a specific organization."""
|
||||
|
||||
def get_object(self, request: HttpRequest, twitch_id: str) -> Organization: # noqa: ARG002
|
||||
"""Get the organization object for this feed.
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
twitch_id: The Twitch ID of the organization.
|
||||
|
||||
Returns:
|
||||
Organization: The organization object.
|
||||
"""
|
||||
return Organization.objects.get(twitch_id=twitch_id)
|
||||
|
||||
def title(self, obj: Organization) -> str:
|
||||
"""Return the feed title."""
|
||||
return f"TTVDrops: {obj.name} Campaigns"
|
||||
|
||||
def link(self, obj: Organization) -> str:
|
||||
"""Return the link to the organization detail."""
|
||||
return reverse("twitch:organization_detail", args=[obj.twitch_id])
|
||||
|
||||
def description(self, obj: Organization) -> str:
|
||||
"""Return the feed description."""
|
||||
return f"Latest drop campaigns for {obj.name}"
|
||||
|
||||
def items(self, obj: Organization) -> list[DropCampaign]:
|
||||
"""Return the latest 100 campaigns for this organization."""
|
||||
return list(
|
||||
DropCampaign.objects.filter(game__owner=obj).select_related("game").order_by("-added_at")[:100],
|
||||
)
|
||||
|
|
|
|||
139
twitch/tests/test_feeds.py
Normal file
139
twitch/tests/test_feeds.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""Test RSS feeds."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from twitch.models import DropCampaign
|
||||
from twitch.models import Game
|
||||
from twitch.models import Organization
|
||||
|
||||
|
||||
class RSSFeedTestCase(TestCase):
|
||||
"""Test RSS feeds."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up test fixtures."""
|
||||
self.org = Organization.objects.create(
|
||||
twitch_id="test-org-123",
|
||||
name="Test Organization",
|
||||
)
|
||||
self.game = Game.objects.create(
|
||||
twitch_id="test-game-123",
|
||||
slug="test-game",
|
||||
name="Test Game",
|
||||
display_name="Test Game",
|
||||
owner=self.org,
|
||||
)
|
||||
self.campaign = DropCampaign.objects.create(
|
||||
twitch_id="test-campaign-123",
|
||||
name="Test Campaign",
|
||||
game=self.game,
|
||||
start_at=timezone.now(),
|
||||
end_at=timezone.now() + timedelta(days=7),
|
||||
)
|
||||
|
||||
def test_organization_feed(self) -> None:
|
||||
"""Test organization feed returns 200."""
|
||||
url = reverse("twitch:organization_feed")
|
||||
response = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
|
||||
def test_game_feed(self) -> None:
|
||||
"""Test game feed returns 200."""
|
||||
url = reverse("twitch:game_feed")
|
||||
response = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
|
||||
def test_campaign_feed(self) -> None:
|
||||
"""Test campaign feed returns 200."""
|
||||
url = reverse("twitch:campaign_feed")
|
||||
response = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
|
||||
def test_game_campaign_feed(self) -> None:
|
||||
"""Test game-specific campaign feed returns 200."""
|
||||
url = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id])
|
||||
response = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
# Verify the game name is in the feed
|
||||
content = 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 = reverse("twitch:organization_campaign_feed", args=[self.org.twitch_id])
|
||||
response = 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 = 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
|
||||
other_game = Game.objects.create(
|
||||
twitch_id="other-game-123",
|
||||
slug="other-game",
|
||||
name="Other Game",
|
||||
display_name="Other Game",
|
||||
owner=self.org,
|
||||
)
|
||||
DropCampaign.objects.create(
|
||||
twitch_id="other-campaign-123",
|
||||
name="Other Campaign",
|
||||
game=other_game,
|
||||
start_at=timezone.now(),
|
||||
end_at=timezone.now() + timedelta(days=7),
|
||||
)
|
||||
|
||||
# Get feed for first game
|
||||
url = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id])
|
||||
response = self.client.get(url)
|
||||
content = response.content.decode("utf-8")
|
||||
|
||||
# Should contain first campaign
|
||||
assert "Test Campaign" in content
|
||||
# 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",
|
||||
owner=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),
|
||||
)
|
||||
|
||||
# Get feed for first organization
|
||||
url = reverse("twitch:organization_campaign_feed", args=[self.org.twitch_id])
|
||||
response = self.client.get(url)
|
||||
content = 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
|
||||
|
|
@ -6,7 +6,9 @@ from django.urls import path
|
|||
|
||||
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 OrganizationFeed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -28,7 +30,9 @@ urlpatterns: list[URLPattern] = [
|
|||
path("channels/", views.ChannelListView.as_view(), name="channel_list"),
|
||||
path("channels/<str:twitch_id>/", views.ChannelDetailView.as_view(), name="channel_detail"),
|
||||
path("rss/organizations/", OrganizationFeed(), name="organization_feed"),
|
||||
path("rss/organizations/<str:twitch_id>/campaigns/", OrganizationCampaignFeed(), name="organization_campaign_feed"),
|
||||
path("rss/games/", GameFeed(), name="game_feed"),
|
||||
path("rss/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed"),
|
||||
path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"),
|
||||
path("docs/rss/", views.docs_rss_view, name="docs_rss"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -792,22 +792,35 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
|
|||
"""
|
||||
feeds: list[dict[str, str]] = [
|
||||
{
|
||||
"title": "Organizations",
|
||||
"description": "Latest organizations",
|
||||
"title": "All Organizations",
|
||||
"description": "Latest organizations added to TTVDrops",
|
||||
"url": "/rss/organizations/",
|
||||
},
|
||||
{
|
||||
"title": "Games",
|
||||
"description": "Latest games",
|
||||
"title": "All Games",
|
||||
"description": "Latest games added to TTVDrops",
|
||||
"url": "/rss/games/",
|
||||
},
|
||||
{
|
||||
"title": "Drop Campaigns",
|
||||
"description": "Latest drop campaigns",
|
||||
"title": "All Drop Campaigns",
|
||||
"description": "Latest drop campaigns across all games",
|
||||
"url": "/rss/campaigns/",
|
||||
},
|
||||
]
|
||||
return render(request, "twitch/docs_rss.html", {"feeds": feeds})
|
||||
|
||||
# Get sample game and organization for examples
|
||||
sample_game = Game.objects.first()
|
||||
sample_org = Organization.objects.first()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"twitch/docs_rss.html",
|
||||
{
|
||||
"feeds": feeds,
|
||||
"sample_game": sample_game,
|
||||
"sample_org": sample_org,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# MARK: /channels/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue