M main/denylist.py => main/denylist.py +1 -0
@@ 8,6 8,7 @@ DISALLOWED_USERNAMES = [
"administration",
"administrator",
"api",
+ "atom",
"auth",
"authentication",
"billing",
M main/feeds.py => main/feeds.py +16 -2
@@ 7,7 7,7 @@ from django.utils import timezone
from main import models
from main.util import reading_time
-from django.utils.feedgenerator import Rss201rev2Feed
+from django.utils.feedgenerator import Rss201rev2Feed, Atom1Feed
class RSLRSSFeed(Rss201rev2Feed):
def root_attributes(self):
@@ 60,7 60,6 @@ class RSLRSSFeed(Rss201rev2Feed):
handler.endElement("rsl:license")
handler.endElement("rsl:content")
-
class RSSBlogFeed(Feed):
feed_type = RSLRSSFeed
title = ""
@@ 120,6 119,21 @@ class RSSBlogFeed(Feed):
return {"rsl_content": rsl_content}
return {}
+
+class AtomBlogFeed(RSSBlogFeed):
+ feed_type = Atom1Feed
+ subtitle = RSSBlogFeed.description
+
+ def __call__(self, request, *args, **kwargs):
+ if not hasattr(request, "subdomain"):
+ raise Http404()
+
+ user = models.User.objects.get(username=request.subdomain)
+
+ models.AnalyticPage.objects.create(user=user, path="atom")
+
+ return super().__call__(request, *args, **kwargs)
+
# Helper to parse RSL XML
RSL_NS = {"rsl": "https://rslstandard.org/rsl"}
M main/templates/main/analytic_list.html => main/templates/main/analytic_list.html +1 -0
@@ 11,6 11,7 @@
<ul>
<li><a href="{% url 'analytic_page_detail' 'index' %}">index</a></li>
<li><a href="{% url 'analytic_page_detail' 'rss' %}">rss</a></li>
+ <li><a href="{% url 'analytic_page_detail' 'atom' %}">atom</a></li>
{% for page in page_list %}
<li><a href="{% url 'analytic_page_detail' page.slug %}">{{ page.slug }}</a></li>
{% endfor %}
M main/templates/main/blog_index.html => main/templates/main/blog_index.html +1 -0
@@ 6,6 6,7 @@
{% block head_extra %}
<link rel="alternate" type="application/rss+xml" title="RSS" href="{% url 'rss_feed' %}">
+<link rel="alternate" type="application/atom+xml" title="Atom" href="{% url 'atom_feed' %}">
{% if blog_user.blog_byline %}
<meta name="description" content="{{ blog_user.blog_byline_as_text }}">
{% endif %}
M main/tests/test_analytics.py => main/tests/test_analytics.py +30 -14
@@ 120,20 120,20 @@ class PageAnalyticIndexTestCase(TestCase):
self.assertEqual(models.AnalyticPage.objects.filter(path="index").count(), 1)
-class PageAnalyticRSSTestCase(TestCase):
+class PageAnalyticFeedTestCase(TestCase):
"""Test 'rss' special page analytics."""
def setUp(self):
self.user = models.User.objects.create(username="alice")
- def test_rss_analytic(self):
- response = self.client.get(
- reverse("rss_feed"),
- # needs HTTP_HOST because we need to request it on the subdomain
- HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
- )
- self.assertEqual(response.status_code, 200)
- self.assertEqual(models.AnalyticPage.objects.filter(path="rss").count(), 1)
+ def test_feed_analytic(self):
+ for feed in ["rss", "atom"]:
+ response = self.client.get(
+ reverse(f"{feed}_feed"),
+ HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(models.AnalyticPage.objects.filter(path=feed).count(), 1)
class AnalyticListTestCase(TestCase):
@@ 264,26 264,28 @@ class PageAnalyticDetailIndexTestCase(TestCase):
self.assertContains(response, "1 hits")
-class PageAnalyticDetailRSSTestCase(TestCase):
+class PageAnalyticDetailFeedTestCase(TestCase):
"""Test analytic detail for 'rss' special page."""
- def setUp(self):
+ def setup_feed_analytic_detail(self, feed):
self.user = models.User.objects.create(username="alice")
self.client.force_login(self.user)
# logout so that analytic is counted
self.client.logout()
- # register one sample rss page analytic
+ # register one sample feed page analytic
self.client.get(
- reverse("rss_feed"),
+ reverse(feed),
HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
)
# login again to access analytic page detail dashboard page
self.client.force_login(self.user)
- def test_page_analytic_detail(self):
+ def test_rss_page_analytic_detail(self):
+ self.setup_feed_analytic_detail("rss_feed")
+
response = self.client.get(
reverse("analytic_page_detail", args=("rss",)),
)
@@ 294,3 296,17 @@ class PageAnalyticDetailRSSTestCase(TestCase):
'<svg version="1.1" viewBox="0 0 500 192" xmlns="http://www.w3.org/2000/svg">',
)
self.assertContains(response, "1 hits")
+
+ def test_atom_page_analytic_detail(self):
+ self.setup_feed_analytic_detail("atom_feed")
+
+ response = self.client.get(
+ reverse("analytic_page_detail", args=("atom",)),
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '<div class="analytics-chart">')
+ self.assertContains(
+ response,
+ '<svg version="1.1" viewBox="0 0 500 192" xmlns="http://www.w3.org/2000/svg">',
+ )
+ self.assertContains(response, "1 hits")<
\ No newline at end of file
M main/tests/test_feeds.py => main/tests/test_feeds.py +59 -62
@@ 8,7 8,7 @@ from main import models
from mataroa import settings
-class RSSFeedTestCase(TestCase):
+class FeedTestCase(TestCase):
def setUp(self):
self.user = models.User.objects.create(username="alice")
self.client.force_login(self.user)
@@ 21,22 21,23 @@ class RSSFeedTestCase(TestCase):
self.post = models.Post.objects.create(owner=self.user, **self.data)
def test_rss_feed(self):
- response = self.client.get(
- reverse("rss_feed"),
- HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
- )
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response["Content-Type"], "application/rss+xml; charset=utf-8")
- self.assertContains(response, self.data["title"])
- self.assertContains(response, self.data["slug"])
- self.assertContains(response, self.data["body"])
- self.assertContains(
- response,
- f"//{self.user.username}.{settings.CANONICAL_HOST}/blog/{self.data['slug']}/</link>",
- )
+ for feed in ["rss", "atom"]:
+ response = self.client.get(
+ reverse(f"{feed}_feed"),
+ HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response["Content-Type"], f"application/{feed}+xml; charset=utf-8")
+ self.assertContains(response, self.data["title"])
+ self.assertContains(response, self.data["slug"])
+ self.assertContains(response, self.data["body"])
+ self.assertContains(
+ response,
+ f"//{self.user.username}.{settings.CANONICAL_HOST}/blog/{self.data['slug']}/",
+ )
-class RSSFeedDraftsTestCase(TestCase):
+class FeedDraftsTestCase(TestCase):
"""Tests draft posts do not appear in the RSS feed."""
def setUp(self):
@@ 58,25 59,26 @@ class RSSFeedDraftsTestCase(TestCase):
models.Post.objects.create(owner=self.user, **self.post_draft)
def test_rss_feed(self):
- response = self.client.get(
- reverse("rss_feed"),
- HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
- )
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response["Content-Type"], "application/rss+xml; charset=utf-8")
- self.assertContains(response, self.post_published["title"])
- self.assertContains(response, self.post_published["slug"])
- self.assertContains(response, self.post_published["body"])
- self.assertNotContains(response, self.post_draft["title"])
- self.assertNotContains(response, self.post_draft["slug"])
- self.assertNotContains(response, self.post_draft["body"])
- self.assertNotContains(
- response,
- f"//{self.user.username}.{settings.CANONICAL_HOST}/blog/{self.post_draft['slug']}/</link>",
- )
+ for feed in ["rss", "atom"]:
+ response = self.client.get(
+ reverse(f"{feed}_feed"),
+ HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response["Content-Type"], f"application/{feed}+xml; charset=utf-8")
+ self.assertContains(response, self.post_published["title"])
+ self.assertContains(response, self.post_published["slug"])
+ self.assertContains(response, self.post_published["body"])
+ self.assertNotContains(response, self.post_draft["title"])
+ self.assertNotContains(response, self.post_draft["slug"])
+ self.assertNotContains(response, self.post_draft["body"])
+ self.assertNotContains(
+ response,
+ f"//{self.user.username}.{settings.CANONICAL_HOST}/blog/{self.post_draft['slug']}/</link>",
+ )
-class RSSFeedFuturePostTestCase(TestCase):
+class FeedFuturePostTestCase(TestCase):
def setUp(self):
self.user = models.User.objects.create(username="alice")
self.client.force_login(self.user)
@@ 89,41 91,36 @@ class RSSFeedFuturePostTestCase(TestCase):
self.post = models.Post.objects.create(owner=self.user, **self.data)
def test_future_post_hidden(self):
- response = self.client.get(
- reverse("rss_feed"),
- # needs HTTP_HOST because we need to request it on the subdomain
- HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
- )
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response["Content-Type"], "application/rss+xml; charset=utf-8")
- self.assertNotContains(response, self.data["title"])
- self.assertNotContains(response, self.data["slug"])
- self.assertNotContains(response, self.data["body"])
- self.assertNotContains(
- response,
- f"//{self.user.username}.{settings.CANONICAL_HOST}/blog/{self.data['slug']}/</link>",
- )
+ for feed in ["rss", "atom"]:
+ response = self.client.get(
+ reverse(f"{feed}_feed"),
+ # needs HTTP_HOST because we need to request it on the subdomain
+ HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response["Content-Type"], f"application/{feed}+xml; charset=utf-8")
+ self.assertNotContains(response, self.data["title"])
+ self.assertNotContains(response, self.data["slug"])
+ self.assertNotContains(response, self.data["body"])
+ self.assertNotContains(
+ response,
+ f"//{self.user.username}.{settings.CANONICAL_HOST}/blog/{self.data['slug']}/</link>",
+ )
-class RSSFeedFormatTestCase(TestCase):
+class FeedFormatTestCase(TestCase):
def setUp(self):
self.user = models.User.objects.create(
username="alice", blog_title="test title", blog_byline="test about text"
)
def test_feed_valid(self):
- response = self.client.get(
- reverse("rss_feed"),
- # needs HTTP_HOST because we need to request it on the subdomain
- HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
- )
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, '<?xml version="1.0" encoding="utf-8"?>')
- self.assertContains(
- response,
- '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
- )
- self.assertContains(response, f"<title>{self.user.blog_title}</title>")
- self.assertContains(
- response, f"<description>{self.user.blog_byline}</description>"
- )
+ for feed in ["rss", "atom"]:
+ response = self.client.get(
+ reverse(f"{feed}_feed"),
+ # needs HTTP_HOST because we need to request it on the subdomain
+ HTTP_HOST=self.user.username + "." + settings.CANONICAL_HOST,
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '<?xml version="1.0" encoding="utf-8"?>')
+ self.assertContains(response, f"<title>{self.user.blog_title}</title>")<
\ No newline at end of file
M main/urls.py => main/urls.py +4 -1
@@ 103,10 103,13 @@ urlpatterns += [
# blog extras
urlpatterns += [
path("rss/", feeds.RSSBlogFeed(), name="rss_feed"),
- path("feed/", feeds.RSSBlogFeed(), name="rss_feed"),
+ path("atom/", feeds.AtomBlogFeed(), name="atom_feed"),
+ path("feed/", feeds.RSSBlogFeed()),
path("feed/rss/", feeds.RSSBlogFeed()),
+ path("feed/atom/", feeds.AtomBlogFeed()),
path("feed.xml", feeds.RSSBlogFeed()),
path("rss.xml", feeds.RSSBlogFeed()),
+ path("atom.xml", feeds.AtomBlogFeed()),
path("index.xml", feeds.RSSBlogFeed()),
# really simple licensing