Enhance RSS feed documentation with example XML and filtered feeds
This commit is contained in:
parent
f4dd987f73
commit
2f9c5a9328
6 changed files with 130 additions and 72 deletions
|
|
@ -18,6 +18,10 @@
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ feed.url }}">Subscribe to {{ feed.title }} RSS Feed</a>
|
<a href="{{ feed.url }}">Subscribe to {{ feed.title }} RSS Feed</a>
|
||||||
</p>
|
</p>
|
||||||
|
<details>
|
||||||
|
<summary>Example XML</summary>
|
||||||
|
<pre><code class="language-xml">{{ feed.example_xml|escape }}</code></pre>
|
||||||
|
</details>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -25,26 +29,31 @@
|
||||||
<section style="margin-top: 2rem;">
|
<section style="margin-top: 2rem;">
|
||||||
<h2 id="filtered-feeds-header">Filtered RSS Feeds</h2>
|
<h2 id="filtered-feeds-header">Filtered RSS Feeds</h2>
|
||||||
<p>
|
<p>
|
||||||
You can also subscribe to RSS feeds for specific games or organizations. These feeds are available on each game or organization detail page.
|
You can subscribe to RSS feeds scoped to a specific game or organization. When available, links below point to live examples; otherwise use the endpoint template.
|
||||||
</p>
|
</p>
|
||||||
<h3>Game-Specific Campaign Feeds</h3>
|
<ul id="filtered-feeds-list">
|
||||||
|
{% for feed in filtered_feeds %}
|
||||||
|
<li id="filtered-feed-{{ forloop.counter }}">
|
||||||
|
<h3>{{ feed.title }}</h3>
|
||||||
|
<p>{{ feed.description }}</p>
|
||||||
<p>
|
<p>
|
||||||
Subscribe to campaigns for a specific game using: <code>/rss/games/<game_id>/campaigns/</code>
|
Endpoint: <code>{{ feed.url }}</code>
|
||||||
</p>
|
</p>
|
||||||
{% if sample_game %}
|
{% if feed.has_sample %}
|
||||||
<p>
|
<p>
|
||||||
Example: <a href="{% url 'twitch:game_campaign_feed' sample_game.twitch_id %}">{{ sample_game.display_name }} Campaigns RSS Feed</a>
|
<a href="{{ feed.url }}">View a live example</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h3>Organization-Specific Campaign Feeds</h3>
|
<details>
|
||||||
|
<summary>Example XML</summary>
|
||||||
|
<pre><code class="language-xml">{{ feed.example_xml|escape }}</code></pre>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
Subscribe to campaigns for a specific organization using: <code>/rss/organizations/<org_id>/campaigns/</code>
|
Versioned paths under <code>/rss/v1/</code> are available and return the same XML structure.
|
||||||
</p>
|
</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>
|
||||||
<section style="margin-top: 2rem;">
|
<section style="margin-top: 2rem;">
|
||||||
<h2 id="usage-header">How to Use RSS Feeds</h2>
|
<h2 id="usage-header">How to Use RSS Feeds</h2>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||||
from django.contrib.syndication.views import Feed
|
from django.contrib.syndication.views import Feed
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import feedgenerator
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.html import format_html_join
|
from django.utils.html import format_html_join
|
||||||
|
|
@ -283,55 +284,33 @@ def _construct_drops_summary(drops_data: list[dict]) -> SafeText:
|
||||||
|
|
||||||
|
|
||||||
# MARK: /rss/organizations/
|
# MARK: /rss/organizations/
|
||||||
class OrganizationFeed(Feed):
|
class OrganizationRSSFeed(Feed):
|
||||||
"""RSS feed for latest organizations."""
|
"""RSS feed for latest organizations."""
|
||||||
|
|
||||||
|
# Spec: https://cyber.harvard.edu/rss/rss.html
|
||||||
|
feed_type = feedgenerator.Rss201rev2Feed
|
||||||
title: str = "TTVDrops Organizations"
|
title: str = "TTVDrops Organizations"
|
||||||
link: str = "/organizations/"
|
link: str = "/organizations/"
|
||||||
description: str = "Latest organizations on TTVDrops"
|
description: str = "Latest organizations on TTVDrops"
|
||||||
feed_copyright: str = "Information wants to be free."
|
feed_copyright: str = "Information wants to be free."
|
||||||
|
|
||||||
def items(self) -> list[Organization]:
|
def items(self) -> QuerySet[Organization, Organization]:
|
||||||
"""Return the latest 200 organizations."""
|
"""Return the latest 200 organizations."""
|
||||||
return list(Organization.objects.order_by("-added_at")[:200])
|
return Organization.objects.order_by("-added_at")[:200]
|
||||||
|
|
||||||
def item_title(self, item: Model) -> SafeText:
|
def item_title(self, item: Organization) -> SafeText:
|
||||||
"""Return the organization name as the item title."""
|
"""Return the organization name as the item title."""
|
||||||
if not isinstance(item, Organization):
|
|
||||||
logger.error("item_title called with non-Organization item: %s", type(item))
|
|
||||||
return SafeText("New Twitch organization added")
|
|
||||||
|
|
||||||
return SafeText(getattr(item, "name", str(item)))
|
return SafeText(getattr(item, "name", str(item)))
|
||||||
|
|
||||||
def item_description(self, item: Model) -> SafeText:
|
def item_description(self, item: Organization) -> SafeText:
|
||||||
"""Return a description of the organization."""
|
"""Return a description of the organization."""
|
||||||
if not isinstance(item, Organization):
|
return SafeText(item.feed_description)
|
||||||
logger.error("item_description called with non-Organization item: %s", type(item))
|
|
||||||
return SafeText("No description available.")
|
|
||||||
|
|
||||||
description_parts: list[SafeText] = []
|
def item_link(self, item: Organization) -> str:
|
||||||
|
|
||||||
name: str = getattr(item, "name", "")
|
|
||||||
twitch_id: str = getattr(item, "twitch_id", "")
|
|
||||||
|
|
||||||
# Link to ttvdrops organization page
|
|
||||||
description_parts.extend((
|
|
||||||
SafeText("<p>New Twitch organization added to TTVDrops:</p>"),
|
|
||||||
SafeText(
|
|
||||||
f"<p><a href='{reverse('twitch:organization_detail', args=[twitch_id])}' target='_blank' rel='noopener noreferrer'>{name}</a></p>", # noqa: E501
|
|
||||||
),
|
|
||||||
))
|
|
||||||
return SafeText("".join(str(part) for part in description_parts))
|
|
||||||
|
|
||||||
def item_link(self, item: Model) -> str:
|
|
||||||
"""Return the link to the organization detail."""
|
"""Return the link to the organization detail."""
|
||||||
if not isinstance(item, Organization):
|
|
||||||
logger.error("item_link called with non-Organization item: %s", type(item))
|
|
||||||
return reverse("twitch:dashboard")
|
|
||||||
|
|
||||||
return reverse("twitch:organization_detail", args=[item.twitch_id])
|
return reverse("twitch:organization_detail", args=[item.twitch_id])
|
||||||
|
|
||||||
def item_pubdate(self, item: Model) -> datetime.datetime:
|
def item_pubdate(self, item: Organization) -> datetime.datetime:
|
||||||
"""Returns the publication date to the feed item.
|
"""Returns the publication date to the feed item.
|
||||||
|
|
||||||
Fallback to added_at or now if missing.
|
Fallback to added_at or now if missing.
|
||||||
|
|
@ -341,24 +320,15 @@ class OrganizationFeed(Feed):
|
||||||
return added_at
|
return added_at
|
||||||
return timezone.now()
|
return timezone.now()
|
||||||
|
|
||||||
def item_updateddate(self, item: Model) -> datetime.datetime:
|
def item_updateddate(self, item: Organization) -> datetime.datetime:
|
||||||
"""Returns the organization's last update time."""
|
"""Returns the organization's last update time."""
|
||||||
updated_at: datetime.datetime | None = getattr(item, "updated_at", None)
|
updated_at: datetime.datetime | None = getattr(item, "updated_at", None)
|
||||||
if updated_at:
|
if updated_at:
|
||||||
return updated_at
|
return updated_at
|
||||||
return timezone.now()
|
return timezone.now()
|
||||||
|
|
||||||
def item_guid(self, item: Model) -> str:
|
def item_author_name(self, item: Organization) -> str:
|
||||||
"""Return a unique identifier for each organization."""
|
|
||||||
twitch_id: str = getattr(item, "twitch_id", "unknown")
|
|
||||||
return twitch_id + "@ttvdrops.com"
|
|
||||||
|
|
||||||
def item_author_name(self, item: Model) -> str:
|
|
||||||
"""Return the author name for the organization."""
|
"""Return the author name for the organization."""
|
||||||
if not isinstance(item, Organization):
|
|
||||||
logger.error("item_author_name called with non-Organization item: %s", type(item))
|
|
||||||
return "Twitch"
|
|
||||||
|
|
||||||
return getattr(item, "name", "Twitch")
|
return getattr(item, "name", "Twitch")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ from typing import TYPE_CHECKING
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
logger: logging.Logger = logging.getLogger("ttvdrops")
|
logger: logging.Logger = logging.getLogger("ttvdrops")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -55,6 +57,18 @@ class Organization(models.Model):
|
||||||
"""Return a string representation of the organization."""
|
"""Return a string representation of the organization."""
|
||||||
return self.name or self.twitch_id
|
return self.name or self.twitch_id
|
||||||
|
|
||||||
|
def feed_description(self: Organization) -> str:
|
||||||
|
"""Return a description of the organization for RSS feeds."""
|
||||||
|
name: str = self.name or "Unknown Organization"
|
||||||
|
url: str = reverse("twitch:organization_detail", args=[self.twitch_id])
|
||||||
|
|
||||||
|
return format_html(
|
||||||
|
"<p>New Twitch organization added to TTVDrops:</p>\n"
|
||||||
|
'<p><a href="{}" target="_blank" rel="noopener noreferrer">{}</a></p>',
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# MARK: Game
|
# MARK: Game
|
||||||
class Game(models.Model):
|
class Game(models.Model):
|
||||||
|
|
|
||||||
|
|
@ -573,3 +573,7 @@ class TestChannelListView:
|
||||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:docs_rss"))
|
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:docs_rss"))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "feeds" in response.context
|
assert "feeds" in response.context
|
||||||
|
assert "filtered_feeds" in response.context
|
||||||
|
assert response.context["feeds"][0]["example_xml"]
|
||||||
|
html: str = response.content.decode()
|
||||||
|
assert '<code class="language-xml">' in html
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from twitch.feeds import DropCampaignFeed
|
||||||
from twitch.feeds import GameCampaignFeed
|
from twitch.feeds import GameCampaignFeed
|
||||||
from twitch.feeds import GameFeed
|
from twitch.feeds import GameFeed
|
||||||
from twitch.feeds import OrganizationCampaignFeed
|
from twitch.feeds import OrganizationCampaignFeed
|
||||||
from twitch.feeds import OrganizationFeed
|
from twitch.feeds import OrganizationRSSFeed
|
||||||
from twitch.feeds import RewardCampaignFeed
|
from twitch.feeds import RewardCampaignFeed
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -24,7 +24,7 @@ rss_feeds_latest: list[URLPattern] = [
|
||||||
path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"),
|
path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"),
|
||||||
path("rss/games/", GameFeed(), name="game_feed"),
|
path("rss/games/", GameFeed(), name="game_feed"),
|
||||||
path("rss/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed"),
|
path("rss/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed"),
|
||||||
path("rss/organizations/", OrganizationFeed(), name="organization_feed"),
|
path("rss/organizations/", OrganizationRSSFeed(), name="organization_feed"),
|
||||||
path("rss/organizations/<str:twitch_id>/campaigns/", OrganizationCampaignFeed(), name="organization_campaign_feed"),
|
path("rss/organizations/<str:twitch_id>/campaigns/", OrganizationCampaignFeed(), name="organization_campaign_feed"),
|
||||||
path("rss/reward-campaigns/", RewardCampaignFeed(), name="reward_campaign_feed"),
|
path("rss/reward-campaigns/", RewardCampaignFeed(), name="reward_campaign_feed"),
|
||||||
]
|
]
|
||||||
|
|
@ -33,7 +33,7 @@ v1_rss_feeds: list[URLPattern] = [
|
||||||
path("rss/v1/campaigns/", DropCampaignFeed(), name="campaign_feed_v1"),
|
path("rss/v1/campaigns/", DropCampaignFeed(), name="campaign_feed_v1"),
|
||||||
path("rss/v1/games/", GameFeed(), name="game_feed_v1"),
|
path("rss/v1/games/", GameFeed(), name="game_feed_v1"),
|
||||||
path("rss/v1/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed_v1"),
|
path("rss/v1/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed_v1"),
|
||||||
path("rss/v1/organizations/", OrganizationFeed(), name="organization_feed_v1"),
|
path("rss/v1/organizations/", OrganizationRSSFeed(), name="organization_feed_v1"),
|
||||||
path(
|
path(
|
||||||
"rss/v1/organizations/<str:twitch_id>/campaigns/",
|
"rss/v1/organizations/<str:twitch_id>/campaigns/",
|
||||||
OrganizationCampaignFeed(),
|
OrganizationCampaignFeed(),
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ from django.db.models import Prefetch
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models import Subquery
|
from django.db.models import Subquery
|
||||||
from django.db.models.functions import Trim
|
from django.db.models.functions import Trim
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views.generic import DetailView
|
from django.views.generic import DetailView
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
@ -33,6 +33,12 @@ from pygments import highlight
|
||||||
from pygments.formatters import HtmlFormatter
|
from pygments.formatters import HtmlFormatter
|
||||||
from pygments.lexers.data import JsonLexer
|
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
|
from twitch.models import Channel
|
||||||
from twitch.models import ChatBadge
|
from twitch.models import ChatBadge
|
||||||
from twitch.models import ChatBadgeSet
|
from twitch.models import ChatBadgeSet
|
||||||
|
|
@ -44,9 +50,9 @@ from twitch.models import RewardCampaign
|
||||||
from twitch.models import TimeBasedDrop
|
from twitch.models import TimeBasedDrop
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.db.models import QuerySet
|
from collections.abc import Callable
|
||||||
from django.http import HttpRequest
|
|
||||||
from django.http import HttpResponse
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
logger: logging.Logger = logging.getLogger("ttvdrops.views")
|
logger: logging.Logger = logging.getLogger("ttvdrops.views")
|
||||||
|
|
||||||
|
|
@ -507,6 +513,7 @@ class GamesGridView(ListView):
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
|
.prefetch_related("owners")
|
||||||
.annotate(
|
.annotate(
|
||||||
campaign_count=Count("drop_campaigns", distinct=True),
|
campaign_count=Count("drop_campaigns", distinct=True),
|
||||||
active_count=Count(
|
active_count=Count(
|
||||||
|
|
@ -1009,38 +1016,92 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
|
||||||
Returns:
|
Returns:
|
||||||
Rendered HTML response with list of RSS feeds.
|
Rendered HTML response with list of RSS feeds.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _pretty_example(xml_str: str, max_items: int = 1) -> str:
|
||||||
|
try:
|
||||||
|
trimmed = xml_str.strip()
|
||||||
|
first_item = trimmed.find("<item")
|
||||||
|
if first_item != -1 and max_items == 1:
|
||||||
|
second_item = trimmed.find("<item", first_item + 5)
|
||||||
|
if second_item != -1:
|
||||||
|
end_channel = trimmed.find("</channel>", second_item)
|
||||||
|
if end_channel != -1:
|
||||||
|
trimmed = trimmed[:second_item] + trimmed[end_channel:]
|
||||||
|
formatted = trimmed.replace("><", ">\n<")
|
||||||
|
return "\n".join(line for line in formatted.splitlines() if line.strip())
|
||||||
|
except Exception: # pragma: no cover - defensive formatting for docs only
|
||||||
|
logger.exception("Failed to pretty-print RSS example")
|
||||||
|
return xml_str
|
||||||
|
|
||||||
|
def render_feed(feed_view: Callable[..., HttpResponse], *args: object) -> str:
|
||||||
|
try:
|
||||||
|
response: HttpResponse = feed_view(request, *args)
|
||||||
|
return _pretty_example(response.content.decode("utf-8"))
|
||||||
|
except Exception: # pragma: no cover - defensive logging for docs only
|
||||||
|
logger.exception("Failed to render %s for RSS docs", feed_view.__class__.__name__)
|
||||||
|
return ""
|
||||||
|
|
||||||
feeds: list[dict[str, str]] = [
|
feeds: list[dict[str, str]] = [
|
||||||
{
|
{
|
||||||
"title": "All Organizations",
|
"title": "All Organizations",
|
||||||
"description": "Latest organizations added to TTVDrops",
|
"description": "Latest organizations added to TTVDrops",
|
||||||
"url": "/rss/organizations/",
|
"url": reverse("twitch:organization_feed"),
|
||||||
|
"example_xml": render_feed(OrganizationRSSFeed()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "All Games",
|
"title": "All Games",
|
||||||
"description": "Latest games added to TTVDrops",
|
"description": "Latest games added to TTVDrops",
|
||||||
"url": "/rss/games/",
|
"url": reverse("twitch:game_feed"),
|
||||||
|
"example_xml": render_feed(GameFeed()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "All Drop Campaigns",
|
"title": "All Drop Campaigns",
|
||||||
"description": "Latest drop campaigns across all games",
|
"description": "Latest drop campaigns across all games",
|
||||||
"url": "/rss/campaigns/",
|
"url": reverse("twitch:campaign_feed"),
|
||||||
|
"example_xml": render_feed(DropCampaignFeed()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "All Reward Campaigns",
|
"title": "All Reward Campaigns",
|
||||||
"description": "Latest reward campaigns (Quest rewards) on Twitch",
|
"description": "Latest reward campaigns (Quest rewards) on Twitch",
|
||||||
"url": "/rss/reward-campaigns/",
|
"url": reverse("twitch:reward_campaign_feed"),
|
||||||
|
"example_xml": render_feed(RewardCampaignFeed()),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get sample game and organization for examples
|
|
||||||
sample_game: Game | None = Game.objects.first()
|
sample_game: Game | None = Game.objects.first()
|
||||||
sample_org: Organization | None = Organization.objects.first()
|
sample_org: Organization | None = Organization.objects.first()
|
||||||
|
|
||||||
|
filtered_feeds: list[dict[str, str | bool]] = [
|
||||||
|
{
|
||||||
|
"title": "Campaigns for a Single Game",
|
||||||
|
"description": "Latest drop campaigns for one game.",
|
||||||
|
"url": (
|
||||||
|
reverse("twitch:game_campaign_feed", args=[sample_game.twitch_id])
|
||||||
|
if sample_game
|
||||||
|
else "/rss/games/<game_id>/campaigns/"
|
||||||
|
),
|
||||||
|
"has_sample": bool(sample_game),
|
||||||
|
"example_xml": render_feed(GameCampaignFeed(), sample_game.twitch_id) if sample_game else "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Campaigns for an Organization",
|
||||||
|
"description": "Drop campaigns across games owned by one organization.",
|
||||||
|
"url": (
|
||||||
|
reverse("twitch:organization_campaign_feed", args=[sample_org.twitch_id])
|
||||||
|
if sample_org
|
||||||
|
else "/rss/organizations/<org_id>/campaigns/"
|
||||||
|
),
|
||||||
|
"has_sample": bool(sample_org),
|
||||||
|
"example_xml": render_feed(OrganizationCampaignFeed(), sample_org.twitch_id) if sample_org else "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"twitch/docs_rss.html",
|
"twitch/docs_rss.html",
|
||||||
{
|
{
|
||||||
"feeds": feeds,
|
"feeds": feeds,
|
||||||
|
"filtered_feeds": filtered_feeds,
|
||||||
"sample_game": sample_game,
|
"sample_game": sample_game,
|
||||||
"sample_org": sample_org,
|
"sample_org": sample_org,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue