changeset 8019:6c9e8aa906cb

feeds: replace webhelpers.feedgenerator with simple mako templating for rendering RSS/Atom XML Most of the complexity in RSS libraries is in dynamically supporting all kinds of attributes. For our use, we have a small static set of attributes we use, and it is simpler to just use mako. Also, webhelpers is dead, and the alternatives seem quite heavy.
author Mads Kiilerich <mads@kiilerich.com>
date Mon, 09 Dec 2019 02:29:04 +0100
parents 68e802950fe4
children 18d146a04fde
files kallithea/lib/feeds.py
diffstat 1 files changed, 101 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/kallithea/lib/feeds.py	Thu Dec 19 21:23:33 2019 +0100
+++ b/kallithea/lib/feeds.py	Mon Dec 09 02:29:04 2019 +0100
@@ -22,38 +22,131 @@
 import datetime
 import re
 
-from webhelpers import feedgenerator
+import mako.template
 
 
 language = 'en-us'
 ttl = "5"
 
+
+# From ``django.utils.feedgenerator`` via webhelpers.feedgenerator
+def rfc2822_date(date):
+    # We do this ourselves to be timezone aware, email.Utils is not tz aware.
+    if getattr(date, "tzinfo", False):
+        time_str = date.strftime('%a, %d %b %Y %H:%M:%S ')
+        offset = date.tzinfo.utcoffset(date)
+        timezone = (offset.days * 24 * 60) + (offset.seconds / 60)
+        hour, minute = divmod(timezone, 60)
+        return time_str + "%+03d%02d" % (hour, minute)
+    else:
+        return date.strftime('%a, %d %b %Y %H:%M:%S -0000')
+
+# From ``django.utils.feedgenerator`` via webhelpers.feedgenerator
+def rfc3339_date(date):
+    if getattr(date, "tzinfo", False):
+        time_str = date.strftime('%Y-%m-%dT%H:%M:%S')
+        offset = date.tzinfo.utcoffset(date)
+        timezone = (offset.days * 24 * 60) + (offset.seconds / 60)
+        hour, minute = divmod(timezone, 60)
+        return time_str + "%+03d:%02d" % (hour, minute)
+    else:
+        return date.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+# From ``django.utils.feedgenerator`` via webhelpers.feedgenerator
+def get_tag_uri(url, date):
+    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
+    tag = re.sub('^http://', '', url)
+    if date is not None:
+        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
+    tag = re.sub('#', '/', tag)
+    return u'tag:' + tag
+
+
+class Attributes(object):
+    """Simple namespace for attribute dict access in mako and elsewhere"""
+    def __init__(self, a_dict):
+        self.__dict__ = a_dict
+
+
 class _Feeder(object):
 
     content_type = None
-    feed_factory = None  # a webhelpers.feedgenerator
+    template = None  # subclass must provide a mako.template.Template
 
     @classmethod
     def render(cls, header, entries):
-        feed = cls.feed_factory(
+        try:
+            latest_pubdate = max(
+                pubdate for pubdate in (e.get('pubdate') for e in entries)
+                if pubdate
+            )
+        except ValueError:  # max() arg is an empty sequence ... or worse
+            latest_pubdate = datetime.datetime.now()
+
+        return cls.template.render(
             language=language,
             ttl=ttl,  # rss only
+            latest_pubdate=latest_pubdate,
+            rfc2822_date=rfc2822_date,  # for RSS
+            rfc3339_date=rfc3339_date,  # for Atom
+            get_tag_uri=get_tag_uri,
+            entries=[Attributes(e) for e in entries],
             **header
         )
-        for e in entries:
-            feed.add_item(**e)
-        return feed.writeString('utf-8')
 
 
 class AtomFeed(_Feeder):
 
     content_type = 'application/atom+xml'
 
-    feed_factory = feedgenerator.Atom1Feed
+    template = mako.template.Template('''\
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="${language}">
+  <title>${title}</title>
+  <link href="${link}" rel="alternate"></link>
+  <id>${link}</id>
+  <updated>${rfc3339_date(latest_pubdate)}</updated>
+  % for entry in entries:
+  <entry>
+    <title>${entry.title}</title>
+    <link href="${entry.link}" rel="alternate"></link>
+    <updated>${rfc3339_date(entry.pubdate)}</updated>
+    <published>${rfc3339_date(entry.pubdate)}</published>
+    <author>
+      <name>${entry.author_name}</name>
+      <email>${entry.author_email}</email>
+    </author>
+    <id>${get_tag_uri(entry.link, entry.pubdate)}</id>
+    <summary type="html">${entry.description}</summary>
+  </entry>
+  % endfor
+</feed>
+''', default_filters=['x'], output_encoding='utf-8', encoding_errors='replace')
 
 
 class RssFeed(_Feeder):
 
     content_type = 'application/rss+xml'
 
-    feed_factory = feedgenerator.Rss201rev2Feed
+    template = mako.template.Template('''\
+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0">
+  <channel>
+    <title>${title}</title>
+    <link>${link}</link>
+    <description>${description}</description>
+    <language>${language}</language>
+    <lastBuildDate>${rfc2822_date(latest_pubdate)}</lastBuildDate>
+    <ttl>${ttl}</ttl>
+    % for entry in entries:
+    <item>
+      <title>${entry.title}</title>
+      <link>${entry.link}</link>
+      <description>${entry.description}</description>
+      <author>${entry.author_email} (${entry.author_name})</author>
+      <pubDate>${rfc2822_date(entry.pubdate)}</pubDate>
+    </item>
+    % endfor
+  </channel>
+</rss>
+''', default_filters=['x'], output_encoding='utf-8', encoding_errors='replace')