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 <summary> instead of <content>. Minor differences include no longer
adding 'type="text/html"' on some <link> elements and no "xml:base" attribute on
<entry> elements. I don't think these differences will have any noticable
effect.

Tested on Liferea feed reader.
This commit is contained in:
Ben Sturmfels 2021-03-15 23:24:44 +11:00
parent 6d3d8667ae
commit 2d941d21e1
7 changed files with 132 additions and 68 deletions

View File

@ -20,6 +20,15 @@ Release Notes
This chapter has important information about our current and previous releases.
0.12.0 (work in progress)
=========================
**Improvements:**
- Switch Atom feeds from deprecated werkzeug.contrib.atom to feedgenerator,
upgrade werkzeug (Ben Sturmfels)
0.11.0
======

View File

@ -23,7 +23,7 @@
;;; WORK IN PROGRESS - UNRESOLVED ISSUES:
;;;
;;; 1. Switch MediaGoblin to using python-feedparser instead of
;;; werkzeug.contrib.atom so we can use Guix's newer version of werkzeug.
;;; werkzeug.contrib.atom so we can use Guix's newer version of werkzeug. DONE
;;;
;;; 2. Package python-soundfile.
;;;
@ -33,12 +33,13 @@
;;;
;;; 4. Fix other test suite errors.
;;;
;;; 6. H264 videos won't transcode: "GStreamer: missing H.264 decoder". Try with
;;; openh264 installed?
;;; 5. H264 videos won't transcode: "GStreamer: missing H.264 decoder".
;;;
;;; 5. Don't have NPM in this environment yet. Maybe we use it, or maybe we
;;; 6. Don't have NPM in this environment yet. Maybe we use it, or maybe we
;;; modify MediaGoblin to provide most functionality without it?
;;;
;;; 7. Haven't even looked at running celery.
;;;
;;; With `guix environment' you can use guix as kind of a universal
;;; virtualenv, except a universal virtualenv with magical time traveling
;;; properties and also, not just for Python.
@ -77,9 +78,9 @@
;;; ./bootstrap.sh
;;; ./configure --without-virtualenv
;;; make
;;; rm -rf bin include lib lib64
;;; rm -rf bin include lib lib64 pyvenv.cfg
;;; python3 -m venv --system-site-packages . && bin/python setup.py develop --no-deps
;;; bin/python -m pip install soundfile 'werkzeug<1.0.0'
;;; bin/python -m pip install soundfile
;;;
;;; ... wait whaaat, what's that venv line?! I thought you said this
;;; was a reasonable virtualenv replacement! Well it is and it will
@ -204,10 +205,11 @@
("python-docutils" ,python-docutils)
("python-sqlalchemy" ,python-sqlalchemy)
("python-unidecode" ,python-unidecode)
;; ("python-werkzeug" ,python-werkzeug) ; Broken due to missing werkzeug.contrib.atom in 1.0.0.
("python-werkzeug" ,python-werkzeug)
("python-exif-read" ,python-exif-read)
("python-wtforms" ,python-wtforms)
("python-email-validator" ,python-email-validator)))
("python-email-validator" ,python-email-validator)
("python-feedgenerator" ,python-feedgenerator)))
(home-page "http://mediagoblin.org/")
(synopsis "Web application for media publishing")
(description "MediaGoblin is a web application for publishing all kinds of

View File

@ -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'}])
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

View File

@ -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'

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -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):

View File

@ -45,7 +45,7 @@ install_requires = [
'py-bcrypt',
'pytest>=2.3.1',
'pytest-xdist',
'werkzeug>=0.7,<1.0.0',
'werkzeug>=0.7',
# Celery 4.3.0 drops the "sqlite" transport alias making our tests fail.
'celery>=3.0,<4.3.0',
# Jinja2 3.0.0 uses f-strings (Python 3.7 and above) but `pip install` on