diff --git a/apps/blogs/migrations/0005_blog_feed_url_rss_xml.py b/apps/blogs/migrations/0005_blog_feed_url_rss_xml.py new file mode 100644 index 000000000..70ce2f885 --- /dev/null +++ b/apps/blogs/migrations/0005_blog_feed_url_rss_xml.py @@ -0,0 +1,51 @@ +"""Point the Python Insider feed at https://blog.python.org/rss.xml. + +Python Insider moved from Blogger to GitHub Pages. The legacy Blogger +feed path ``feeds/posts/default?alt=rss`` is still served as a +compatibility copy, but it has no file extension so GitHub Pages sends +it as ``application/octet-stream``, which makes Safari and other +browsers download it instead of opening a feed reader. The blog's +canonical feed is ``/rss.xml``, served as ``application/xml``. + +``update_blog_supernav`` looks up the Feed row by matching +``settings.PYTHON_BLOG_FEED_URL`` exactly, so the stored Feed row has to +change together with the setting. The ``blogs-subscriptions`` sidebar +box links to the feed too, so rewrite both its raw and rendered content. +""" + +from django.db import migrations +from django.db.models import Value +from django.db.models.functions import Replace + +OLD_FEED_URL = "https://blog.python.org/feeds/posts/default?alt=rss" +NEW_FEED_URL = "https://blog.python.org/rss.xml" + + +def _replace_feed_url(apps, old, new): + Feed = apps.get_model("blogs", "Feed") + Feed.objects.filter(feed_url=old).update(feed_url=new) + + Box = apps.get_model("boxes", "Box") + Box.objects.filter(label="blogs-subscriptions", content__contains=old).update( + content=Replace("content", Value(old), Value(new)), + _content_rendered=Replace("_content_rendered", Value(old), Value(new)), + ) + + +def forwards(apps, schema_editor): + _replace_feed_url(apps, OLD_FEED_URL, NEW_FEED_URL) + + +def backwards(apps, schema_editor): + _replace_feed_url(apps, NEW_FEED_URL, OLD_FEED_URL) + + +class Migration(migrations.Migration): + dependencies = [ + ("blogs", "0004_normalize_blogentry_urls"), + ("boxes", "0004_alter_box_creator_alter_box_last_modified_by"), + ] + + operations = [ + migrations.RunPython(forwards, backwards), + ] diff --git a/apps/blogs/tests/test_migrations.py b/apps/blogs/tests/test_migrations.py new file mode 100644 index 000000000..7688b4f9e --- /dev/null +++ b/apps/blogs/tests/test_migrations.py @@ -0,0 +1,61 @@ +"""Tests for blogs data migrations.""" + +from django.db import connection +from django.db.migrations.executor import MigrationExecutor +from django.test import TransactionTestCase + +OLD_FEED_URL = "https://blog.python.org/feeds/posts/default?alt=rss" +NEW_FEED_URL = "https://blog.python.org/rss.xml" +BEFORE = [ + ("blogs", "0004_normalize_blogentry_urls"), + ("boxes", "0004_alter_box_creator_alter_box_last_modified_by"), +] +AFTER = [("blogs", "0005_blog_feed_url_rss_xml")] + + +class BlogFeedUrlRssXmlMigrationTests(TransactionTestCase): + """The 0005 migration repoints the Feed row and sidebar box at rss.xml.""" + + def setUp(self): + self.executor = MigrationExecutor(connection) + self.executor.migrate(BEFORE) + apps = self.executor.loader.project_state(BEFORE).apps + self.Feed = apps.get_model("blogs", "Feed") + self.Box = apps.get_model("boxes", "Box") + + def tearDown(self): + self.executor.loader.build_graph() + self.executor.migrate(self.executor.loader.graph.leaf_nodes()) + + def _migrate(self, targets): + self.executor.loader.build_graph() + self.executor.migrate(targets) + return self.executor.loader.project_state(targets).apps + + def test_forwards_rewrites_feed_and_box(self): + self.Feed.objects.create(name="Python Insider", website_url="https://blog.python.org", feed_url=OLD_FEED_URL) + self.Feed.objects.create(name="Other", website_url="https://example.org", feed_url="https://example.org/feed") + self.Box.objects.create( + label="blogs-subscriptions", + content=f'RSS', + content_markup_type="html", + ) + + apps = self._migrate(AFTER) + + Feed = apps.get_model("blogs", "Feed") + self.assertEqual(Feed.objects.get(name="Python Insider").feed_url, NEW_FEED_URL) + self.assertEqual(Feed.objects.get(name="Other").feed_url, "https://example.org/feed") + box = apps.get_model("boxes", "Box").objects.get(label="blogs-subscriptions") + self.assertEqual(box.content.raw, f'RSS') + self.assertIn(NEW_FEED_URL, box.content.rendered) + self.assertNotIn(OLD_FEED_URL, box.content.rendered) + + def test_backwards_restores_old_url(self): + self.Feed.objects.create(name="Python Insider", website_url="https://blog.python.org", feed_url=OLD_FEED_URL) + self._migrate(AFTER) + + apps = self._migrate(BEFORE) + + Feed = apps.get_model("blogs", "Feed") + self.assertEqual(Feed.objects.get(name="Python Insider").feed_url, OLD_FEED_URL) diff --git a/fixtures/boxes.json b/fixtures/boxes.json index b903f7b23..457271f62 100644 --- a/fixtures/boxes.json +++ b/fixtures/boxes.json @@ -474,9 +474,9 @@ "created": "2014-02-13T18:27:24.143Z", "updated": "2025-04-21T17:45:00.000Z", "label": "blogs-subscriptions", - "content": "
Subscribe to Python Insider via:
\r\nAlso check out the Discussions on Python.org
", + "content": "Subscribe to Python Insider via:
\r\nAlso check out the Discussions on Python.org
", "content_markup_type": "html", - "_content_rendered": "Subscribe to Python Insider via:
\r\nAlso check out the Discussions on Python.org
" + "_content_rendered": "Subscribe to Python Insider via:
\r\nAlso check out the Discussions on Python.org
" } }, { diff --git a/pydotorg/settings/base.py b/pydotorg/settings/base.py index 6077d677a..e174aff44 100644 --- a/pydotorg/settings/base.py +++ b/pydotorg/settings/base.py @@ -293,7 +293,7 @@ HAYSTACK_CUSTOM_HIGHLIGHTER = "pydotorg.search.Highlighter" ### Blog Feed URL -PYTHON_BLOG_FEED_URL = "https://blog.python.org/feeds/posts/default?alt=rss" +PYTHON_BLOG_FEED_URL = "https://blog.python.org/rss.xml" PYTHON_BLOG_URL = "https://blog.python.org" ### Registration mailing lists diff --git a/templates/base.html b/templates/base.html index d26422b01..8b6c84c35 100644 --- a/templates/base.html +++ b/templates/base.html @@ -71,7 +71,7 @@ + href="https://blog.python.org/rss.xml">