From 2d941d21e131a4cd228b436706ded084d537b9c8 Mon Sep 17 00:00:00 2001 From: Ben Sturmfels Date: Mon, 15 Mar 2021 23:24:44 +1100 Subject: Convert atom feeds to use feedgenerator library. Issue is that Werkzeug > 1.0.0 has removed werkzeug.contrib.atom.AtomFeed, making it difficult to use a distribution-packaged version of werkzeug. To solve this, I've replaced use of werkzeug.contrib.atom.AtomFeed with feedgenerator.Atom1Feed. After the change, the only major difference between the feeds before and after is that they use instead of . Minor differences include no longer adding 'type="text/html"' on some elements and no "xml:base" attribute on elements. I don't think these differences will have any noticable effect. Tested on Liferea feed reader. --- mediagoblin/listings/views.py | 60 ++++++++++++++++++++--------------------- mediagoblin/tests/test_feed.py | 19 +++++++++++++ mediagoblin/tools/feeds.py | 38 ++++++++++++++++++++++++++ mediagoblin/user_pages/views.py | 56 +++++++++++++++++++------------------- 4 files changed, 113 insertions(+), 60 deletions(-) create mode 100644 mediagoblin/tests/test_feed.py create mode 100644 mediagoblin/tools/feeds.py (limited to 'mediagoblin') diff --git a/mediagoblin/listings/views.py b/mediagoblin/listings/views.py index 11d480d3..8e55d7d0 100644 --- a/mediagoblin/listings/views.py +++ b/mediagoblin/listings/views.py @@ -19,11 +19,12 @@ from mediagoblin.db.models import MediaEntry from mediagoblin.db.util import media_entries_for_tag_slug from mediagoblin.decorators import uses_pagination from mediagoblin.plugins.api.tools import get_media_file_paths +from mediagoblin.tools.feeds import AtomFeedWithLinks from mediagoblin.tools.pagination import Pagination from mediagoblin.tools.response import render_to_response from mediagoblin.tools.translate import pass_to_ugettext as _ -from werkzeug.contrib.atom import AtomFeed +from werkzeug.wrappers import Response def _get_tag_name_from_entries(media_entries, tag_slug): @@ -76,35 +77,32 @@ def atom_feed(request): if tag_slug: feed_title += " for tag '%s'" % tag_slug link = request.urlgen('mediagoblin.listings.tags_listing', - qualified=True, tag=tag_slug ) + qualified=True, tag=tag_slug) cursor = media_entries_for_tag_slug(request.db, tag_slug) - else: # all recent item feed + else: # all recent item feed feed_title += " for all recent items" link = request.urlgen('index', qualified=True) cursor = MediaEntry.query.filter_by(state='processed') cursor = cursor.order_by(MediaEntry.created.desc()) cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) - """ ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI) """ - atomlinks = [{ - 'href': link, - 'rel': 'alternate', - 'type': 'text/html'}] - + atomlinks = [] if mg_globals.app_config["push_urls"]: for push_url in mg_globals.app_config["push_urls"]: atomlinks.append({ 'rel': 'hub', 'href': push_url}) - feed = AtomFeed( - feed_title, + feed = AtomFeedWithLinks( + title=feed_title, + link=link, + description='', feed_url=request.url, - id=link, - links=atomlinks) + links=atomlinks, + ) for entry in cursor: # Include a thumbnail image in content. @@ -115,25 +113,25 @@ def atom_feed(request): else: content = entry.description_html - feed.add( + feed.add_item( # AtomFeed requires a non-blank title. This situation can occur if # you edit a media item and blank out the existing title. - entry.get('title') or _('Untitled'), - content, - id=entry.url_for_self(request.urlgen, qualified=True), - content_type='html', - author={ - 'name': entry.get_actor.username, - 'uri': request.urlgen( + title=entry.get('title') or _('Untitled'), + link=entry.url_for_self( + request.urlgen, + qualified=True), + description=content, + unique_id=entry.url_for_self(request.urlgen, qualified=True), + author_name=entry.get_actor.username, + author_link=request.urlgen( 'mediagoblin.user_pages.user_home', qualified=True, - user=entry.get_actor.username)}, - updated=entry.get('created'), - links=[{ - 'href': entry.url_for_self( - request.urlgen, - qualified=True), - 'rel': 'alternate', - 'type': 'text/html'}]) - - return feed.get_response() + user=entry.get_actor.username), + updateddate=entry.get('created'), + ) + + response = Response( + feed.writeString(encoding='utf-8'), + mimetype='application/atom+xml' + ) + return response diff --git a/mediagoblin/tests/test_feed.py b/mediagoblin/tests/test_feed.py new file mode 100644 index 00000000..93b8531f --- /dev/null +++ b/mediagoblin/tests/test_feed.py @@ -0,0 +1,19 @@ +from mediagoblin.tests.tools import fixture_add_user, fixture_media_entry + + +class TestFeeds: + def setup(self): + self.user = fixture_add_user(username='terence', privileges=['active']) + self.media_entry = fixture_media_entry( + uploader=self.user.id, + state='processed') + + def test_site_feed(self, test_app): + res = test_app.get('/atom/') + assert res.status_int == 200 + assert res.content_type == 'application/atom+xml' + + def test_user_feed(self, test_app): + res = test_app.get('/u/terence/atom/') + assert res.status_int == 200 + assert res.content_type == 'application/atom+xml' diff --git a/mediagoblin/tools/feeds.py b/mediagoblin/tools/feeds.py new file mode 100644 index 00000000..c62478c4 --- /dev/null +++ b/mediagoblin/tools/feeds.py @@ -0,0 +1,38 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2021 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from feedgenerator.django.utils import feedgenerator + + +class AtomFeedWithLinks(feedgenerator.Atom1Feed): + """Custom AtomFeed that adds additional top-level links. + + This is used in MediaGoblin for adding pubsubhubub "hub" links to the feed + via the "push_urls" config. We're porting the feed across to feedgenerator + due to deprecation of werkzeug.contrib.atom.AtomFeed, so while I've never + seen this feature in use, but this class allows us to continue to support + it. + + """ + def __init__(self, *args, links=None, **kwargs): + super().__init__(*args, **kwargs) + links = [] if links is None else links + self.links = links + + def add_root_elements(self, handler): + super().add_root_elements(handler) + for link in self.links: + handler.addQuickElement('link', '', link) diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 2993c6e6..f50d52f1 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -29,6 +29,7 @@ from mediagoblin.tools.text import cleaned_markdown_conversion from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.pagination import Pagination from mediagoblin.tools.federation import create_activity +from mediagoblin.tools.feeds import AtomFeedWithLinks from mediagoblin.user_pages import forms as user_forms from mediagoblin.user_pages.lib import (send_comment_email, add_media_to_collection, build_report_object) @@ -42,7 +43,6 @@ from mediagoblin.decorators import (uses_pagination, get_user_media_entry, get_user_collection, get_user_collection_item, active_user_from_url, get_optional_media_comment_by_id, allow_reporting) -from werkzeug.contrib.atom import AtomFeed from werkzeug.exceptions import MethodNotAllowed from werkzeug.wrappers import Response @@ -541,7 +541,7 @@ def atom_feed(request): generates the atom feed with the newest images """ user = LocalUser.query.filter_by( - username = request.matchdict['user']).first() + username=request.matchdict['user']).first() if not user or not user.has_privilege('active'): return render_404(request) feed_title = "MediaGoblin Feed for user '%s'" % request.matchdict['user'] @@ -551,29 +551,27 @@ def atom_feed(request): cursor = cursor.order_by(MediaEntry.created.desc()) cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) - """ ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI) """ - atomlinks = [{ - 'href': link, - 'rel': 'alternate', - 'type': 'text/html'}] - + atomlinks = [] if mg_globals.app_config["push_urls"]: for push_url in mg_globals.app_config["push_urls"]: atomlinks.append({ 'rel': 'hub', 'href': push_url}) - feed = AtomFeed( - feed_title, - feed_url=request.url, + feed = AtomFeedWithLinks( + title=feed_title, + link=link, + description='', id='tag:{host},{year}:gallery.user-{user}'.format( host=request.host, year=datetime.datetime.today().strftime('%Y'), user=request.matchdict['user']), - links=atomlinks) + feed_url=request.url, + links=atomlinks, + ) for entry in cursor: # Include a thumbnail image in content. @@ -584,26 +582,26 @@ def atom_feed(request): else: content = entry.description_html - feed.add( - entry.get('title'), - content, - id=entry.url_for_self(request.urlgen, qualified=True), - content_type='html', - author={ - 'name': entry.get_actor.username, - 'uri': request.urlgen( - 'mediagoblin.user_pages.user_home', - qualified=True, - user=entry.get_actor.username)}, - updated=entry.get('created'), - links=[{ - 'href': entry.url_for_self( + feed.add_item( + title=entry.get('title'), + link=entry.url_for_self( request.urlgen, qualified=True), - 'rel': 'alternate', - 'type': 'text/html'}]) + description=content, + unique_id=entry.url_for_self(request.urlgen, qualified=True), + author_name=entry.get_actor.username, + author_link=request.urlgen( + 'mediagoblin.user_pages.user_home', + qualified=True, + user=entry.get_actor.username), + updateddate=entry.get('created'), + ) - return feed.get_response() + response = Response( + feed.writeString(encoding='utf-8'), + mimetype='application/atom+xml' + ) + return response def collection_atom_feed(request): -- cgit v1.2.3