Newsfeed templates

WebHelpers2 doesn’t include WebHelpers’ feedgenerator module, so here’s a simple alternative using Mako templates. It’s based on the output of feedgenerator converted to Mako templates. This example uses GeoRSS so each item has a latitude/longitude. The templates may not support all of feedgenerator’s features but it’s sufficient for basic sites.

To run the example:

  1. Create a directory and chdir to it.

  2. Download “atom.mako”, “rss.mako”, and “newsfeeds.py” below. The templates must be in the current directory.

  3. Install Mako.

  4. Run “python newsfeeds.py”.

It will put the output files in the current directory: “news.atom” and “news.mako”.

If you use the “–debug” option it will also write the compiled templates to files: “news.atom.py” and “news.rss.py”.

Atom template

Download: atom.mako

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:georss="http://www.georss.org/georss">
  <title>${title}</title>
  <link href="${site_url}" rel="alternate"></link>
  <link href="${newsfeed_url}" rel="self"></link>
  <id>${newsfeed_url}</id>
  <updated>${update_date}</updated>
  <rights>${copyright}</rights>
% for it in items:
  <entry>
    <title>${it['title']}</title>
    <link href="${it['url']}" rel="alternate"></link>
    <updated>${it['update_date']}</updated>
    <published>${it['date']}</published>
    <author><name>${it['author']}</name></author>
    <id>${it['guid']}</id>
    <summary type="html">${it['description']}</summary>
% if lat is not None and lon is not None:
    <georss:point>${it['lat']} ${it['lon']}</georss:point>
% endif   lat lon
  </entry>
% endfor   incident
</feed>

RSS template

Download: rss.mako

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:georss="http://www.georss.org/georss">
  <channel>
    <title>${title}</title>
    <link>${site_url}</link>
    <description>${description}</description>
    <copyright>${copyright}</copyright>
    <lastBuildDate>${update_date}</lastBuildDate>
% for it in items:
    <item>
      <title>${it['title']}</title>
      <link>${it['url']}</link>
      <description>${it['description']}</description>
      <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">${it['author']}</dc:creator>
      <pubDate>${it['date']}</pubDate>
      <guid>${it['guid']}</guid>
% if it['lat'] is not None and it['lon'] is not None:
      <georss:point>${it['lat']} ${it['lon']}</georss:point>
% endif   lat lon
    </item>
% endfor it
  </channel>
</rss>

The script

Download newsfeeds.py

from __future__ import absolute_import, print_function, unicode_literals
import copy
import datetime
import os
import sys

from mako.template import Template
from six.moves.urllib import parse

COLLECTION = {
    "title": "My Collection",
    "site_url": "http://example.com/",
    "description": "A bunch of articles.",
    "copyright": "Public domain.",
    "update_date": datetime.date(2014, 8, 12),
    "records": [
        {
            "path": "articles/2",   # Relative to SITE_URL.
            "title": "Article 2",
            "author": "Me",
            "date": datetime.date(2014, 8, 12),
            "update_date": datetime.date(2014, 8, 10),
            "lat": 40.56,
            "lon": -90.23,
            "description": "My article.",
        },
        {
            "path": "articles/1",
            "title": "Article 1",
            "author": "Me",
            "date": datetime.date(2014, 7, 26),
            "update_date": datetime.date(2014, 7, 26),
            "lat": 41.17,
            "lon": -71.51,
            "description": "My earlier article.",
        },
        ]
    }


def make_guid(site, date, path):
    guid_fmt = "tag:{},{}-{}-{}:{}"
    return guid_fmt.format(site, date.year, date.month, date.day, path)


class AtomFeedGenerator(object):
    content_type = b"application/atom+xml"
    date_fmt = "%Y-%m-%dT00:00:00Z" # ISO format except "Z" instead of "UTC".
    output_encoding = "utf-8"
    template = "atom.mako"
    
    def __init__(self, site, site_url, newsfeed_url, debug=False):
        self.site = site
        self.site_url = site_url
        self.newsfeed_url = newsfeed_url
        self.debug = debug

    def render(self, content, output_file, debug=False):
        render = self.get_renderer()
        template_vars = self.get_template_vars(content)
        xml = render(**template_vars)
        f = open(output_file, "w")
        f.write(xml)
        f.close()

    def get_renderer(self):
        kw = {
            "filename": self.template,
            "output_encoding": self.output_encoding,
            }
        if self.debug:
            kw["module_directory"] = os.curdir
        tpl = Template(**kw)
        return tpl.render

    def get_template_vars(self, content):
        update_date = self.get_update_date(content)
        items = self.make_news_items(content)
        ret = {
            "title": content["title"],
            "site_url": self.site_url,
            "newsfeed_url": self.newsfeed_url,
            "update_date": update_date,
            "copyright": content["copyright"],
            "description": content["description"],
            "items": items,
            }
        return ret
        
    def make_news_items(self, content):
        items = []
        for r in content["records"]:
            r = r.copy()
            r["url"] = parse.urljoin(self.site_url, r["path"])
            r["guid"] = make_guid(self.site, r["date"], r["path"])
            r["date"] = r["date"].strftime(self.date_fmt)
            r["update_date"] = r["update_date"].strftime(self.date_fmt)
            items.append(r)
        return items

    def get_update_date(self, content):
        if not content["records"]:
            return None
        return content["records"][0]["date"].strftime(self.date_fmt)


class RSSFeedGenerator(AtomFeedGenerator):
    content_type = b"application/rss+xml"
    date_fmt = "%a, %d %b %Y 00:00:00 -0000"
    template = "rss.mako"


def main():
    debug = "--debug" in sys.argv[1:]
    site = "example.com"
    site_url = "http://example.com/"
    newsfeed_url = site_url + "atom"
    feed = AtomFeedGenerator(site, site_url, newsfeed_url, debug)
    feed.render(COLLECTION, "news.atom")
    newsfeed_url = site_url + "rss"
    feed = RSSFeedGenerator(site, site_url, newsfeed_url, debug)
    feed.render(COLLECTION, "news.rss")

if __name__ == "__main__":  main()