~linuxgoose/bocpress

16ffd8e75ae0637489ad15138c74641b0317f4a7 — Jordan Robinson 2 months ago 3113af5
add atom feed
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