Commit 942e5246 authored by Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss
Browse files

Added a number of callbacks to SyndicationFeed for adding custom attributes...

Added a number of callbacks to SyndicationFeed for adding custom attributes and elements to feeds. Refs #6547.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8311 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent 899ca54f
Loading
Loading
Loading
Loading
+144 −102
Original line number Diff line number Diff line
@@ -19,9 +19,10 @@ For definitions of the different versions of RSS, see:
http://diveintomark.org/archives/2004/02/04/incompatible-rss
"""

import re
import datetime
from django.utils.xmlutils import SimplerXMLGenerator
from django.utils.encoding import force_unicode, iri_to_uri
import datetime, re, time

def rfc2822_date(date):
    # We do this ourselves to be timezone aware, email.Utils is not tz aware.
@@ -56,7 +57,7 @@ class SyndicationFeed(object):
    "Base class for all syndication feeds. Subclasses should provide write()"
    def __init__(self, title, link, description, language=None, author_email=None,
            author_name=None, author_link=None, subtitle=None, categories=None,
            feed_url=None, feed_copyright=None, feed_guid=None, ttl=None):
            feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs):
        to_unicode = lambda s: force_unicode(s, strings_only=True)
        if categories:
            categories = [force_unicode(c) for c in categories]
@@ -75,11 +76,13 @@ class SyndicationFeed(object):
            'id': feed_guid or link,
            'ttl': ttl,
        }
        self.feed.update(kwargs)
        self.items = []

    def add_item(self, title, link, description, author_email=None,
        author_name=None, author_link=None, pubdate=None, comments=None,
        unique_id=None, enclosure=None, categories=(), item_copyright=None, ttl=None):
        unique_id=None, enclosure=None, categories=(), item_copyright=None, 
        ttl=None, **kwargs):
        """
        Adds an item to the feed. All args are expected to be Python Unicode
        objects except pubdate, which is a datetime.datetime object, and
@@ -88,7 +91,7 @@ class SyndicationFeed(object):
        to_unicode = lambda s: force_unicode(s, strings_only=True)
        if categories:
            categories = [to_unicode(c) for c in categories]
        self.items.append({
        item = {
            'title': to_unicode(title),
            'link': iri_to_uri(link),
            'description': to_unicode(description),
@@ -102,11 +105,39 @@ class SyndicationFeed(object):
            'categories': categories or (),
            'item_copyright': to_unicode(item_copyright),
            'ttl': ttl,
        })
        }
        item.update(kwargs)
        self.items.append(item)

    def num_items(self):
        return len(self.items)

    def root_attributes(self):
        """
        Return extra attributes to place on the root (i.e. feed/channel) element.
        Called from write().
        """
        return {}
        
    def add_root_elements(self, handler):
        """
        Add elements in the the root (i.e. feed/channel) element. Called 
        from write().
        """
        pass
        
    def item_attributes(self, item):
        """
        Return extra attributes to place on each item (i.e. item/entry) element.
        """
        return {}
        
    def add_item_elements(self, handler, item):
        """
        Add elements on each item (i.e. item/entry) element.
        """
        pass
        
    def write(self, outfile, encoding):
        """
        Outputs the feed in the given encoding to outfile, which is a file-like
@@ -148,7 +179,19 @@ class RssFeed(SyndicationFeed):
        handler = SimplerXMLGenerator(outfile, encoding)
        handler.startDocument()
        handler.startElement(u"rss", {u"version": self._version})
        handler.startElement(u"channel", {})
        handler.startElement(u"channel", self.root_attributes())
        self.add_root_elements(handler)
        self.write_items(handler)
        self.endChannelElement(handler)
        handler.endElement(u"rss")

    def write_items(self, handler):
        for item in self.items:
            handler.startElement(u'item', self.item_attributes(item))
            self.add_item_elements(handler, item)
            handler.endElement(u"item")

    def add_root_elements(self, handler):
        handler.addQuickElement(u"title", self.feed['title'])
        handler.addQuickElement(u"link", self.feed['link'])
        handler.addQuickElement(u"description", self.feed['description'])
@@ -161,30 +204,22 @@ class RssFeed(SyndicationFeed):
        handler.addQuickElement(u"lastBuildDate", rfc2822_date(self.latest_post_date()).decode('ascii'))
        if self.feed['ttl'] is not None:
            handler.addQuickElement(u"ttl", self.feed['ttl'])
        self.write_items(handler)
        self.endChannelElement(handler)
        handler.endElement(u"rss")

    def endChannelElement(self, handler):
        handler.endElement(u"channel")

class RssUserland091Feed(RssFeed):
    _version = u"0.91"
    def write_items(self, handler):
        for item in self.items:
            handler.startElement(u"item", {})
    def add_item_elements(self, handler, item):
        handler.addQuickElement(u"title", item['title'])
        handler.addQuickElement(u"link", item['link'])
        if item['description'] is not None:
            handler.addQuickElement(u"description", item['description'])
            handler.endElement(u"item")

class Rss201rev2Feed(RssFeed):
    # Spec: http://blogs.law.harvard.edu/tech/rss
    _version = u"2.0"
    def write_items(self, handler):
        for item in self.items:
            handler.startElement(u"item", {})
    def add_item_elements(self, handler, item):
        handler.addQuickElement(u"title", item['title'])
        handler.addQuickElement(u"link", item['link'])
        if item['description'] is not None:
@@ -218,19 +253,26 @@ class Rss201rev2Feed(RssFeed):
        for cat in item['categories']:
            handler.addQuickElement(u"category", cat)

            handler.endElement(u"item")

class Atom1Feed(SyndicationFeed):
    # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html
    mime_type = 'application/atom+xml'
    ns = u"http://www.w3.org/2005/Atom"

    def write(self, outfile, encoding):
        handler = SimplerXMLGenerator(outfile, encoding)
        handler.startDocument()
        handler.startElement(u'feed', self.root_attributes())
        self.add_root_elements(handler)
        self.write_items(handler)
        handler.endElement(u"feed")

    def root_element_attributes(self):
        if self.feed['language'] is not None:
            handler.startElement(u"feed", {u"xmlns": self.ns, u"xml:lang": self.feed['language']})
            return {u"xmlns": self.ns, u"xml:lang": self.feed['language']}
        else:
            handler.startElement(u"feed", {u"xmlns": self.ns})
            return {u"xmlns": self.ns}

    def add_root_elements(self, handler):
        handler.addQuickElement(u"title", self.feed['title'])
        handler.addQuickElement(u"link", "", {u"rel": u"alternate", u"href": self.feed['link']})
        if self.feed['feed_url'] is not None:
@@ -251,12 +293,14 @@ class Atom1Feed(SyndicationFeed):
            handler.addQuickElement(u"category", "", {u"term": cat})
        if self.feed['feed_copyright'] is not None:
            handler.addQuickElement(u"rights", self.feed['feed_copyright'])
        self.write_items(handler)
        handler.endElement(u"feed")
        
    def write_items(self, handler):
        for item in self.items:
            handler.startElement(u"entry", {})
            handler.startElement(u"entry", self.item_attributes(item))
            self.add_item_elements(handler, item)
            handler.endElement(u"entry")
            
    def add_item_elements(self, handler, item):
        handler.addQuickElement(u"title", item['title'])
        handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"alternate"})
        if item['pubdate'] is not None:
@@ -299,8 +343,6 @@ class Atom1Feed(SyndicationFeed):
        if item['item_copyright'] is not None:
            handler.addQuickElement(u"rights", item['item_copyright'])

            handler.endElement(u"entry")

# This isolates the decision of what the system default is, so calling code can
# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
DefaultFeed = Rss201rev2Feed
+134 −39
Original line number Diff line number Diff line
@@ -801,7 +801,12 @@ Behind the scenes, the high-level RSS framework uses a lower-level framework
for generating feeds' XML. This framework lives in a single module:
`django/utils/feedgenerator.py`_.

Feel free to use this framework on your own, for lower-level tasks.
You use this framework on your own, for lower-level feed generation. You can
also create custom feed generator subclasses for use with the ``feed_type``
``Feed`` option.

``SyndicationFeed`` classes
---------------------------

The ``feedgenerator`` module contains a base class ``SyndicationFeed`` and
several subclasses:
@@ -813,38 +818,71 @@ several subclasses:
Each of these three classes knows how to render a certain type of feed as XML.
They share this interface:

``__init__(title, link, description, language=None, author_email=None,``
``author_name=None, author_link=None, subtitle=None, categories=None,``
``feed_url=None)``
``SyndicationFeed.__init__(**kwargs)``
    Initialize the feed with the given dictionary of metadata, which applies to
    the entire feed. Required keyword arguments are:
    
Initializes the feed with the given metadata, which applies to the entire feed
(i.e., not just to a specific item in the feed).
        * ``title``
        * ``link``
        * ``description``
        
All parameters, if given, should be Unicode objects, except ``categories``,
which should be a sequence of Unicode objects.
    There's also a bunch of other optional keywords:
    
``add_item(title, link, description, author_email=None, author_name=None,``
``pubdate=None, comments=None, unique_id=None, enclosure=None, categories=())``
        * ``language``
        * ``author_email``
        * ``author_name``
        * ``author_link``
        * ``subtitle``
        * ``categories``
        * ``feed_url``
        * ``feed_copyright``
        * ``feed_guid``
        * ``ttl``
        
Add an item to the feed with the given parameters. All parameters, if given,
should be Unicode objects, except:
    Any extra keyword arguments you pass to ``__init__`` will be stored in
    ``self.feed`` for use with `custom feed generators`_.

    * ``pubdate`` should be a `Python datetime object`_.
    * ``enclosure`` should be an instance of ``feedgenerator.Enclosure``.
    * ``categories`` should be a sequence of Unicode objects.
    All parameters should be Unicode objects, except ``categories``, which
    should be a sequence of Unicode objects.

``SyndicationFeed.add_item(**kwargs)``
    Add an item to the feed with the given parameters. 

``write(outfile, encoding)``
    Required keyword arguments are:
    
Outputs the feed in the given encoding to outfile, which is a file-like object.
        * ``title``
        * ``link``
        * ``description``

``writeString(encoding)``
    Optional keyword arguments are:

Returns the feed as a string in the given encoding.
        * ``author_email``
        * ``author_name``
        * ``author_link``
        * ``pubdate``
        * ``comments``
        * ``unique_id``
        * ``enclosure``
        * ``categories``
        * ``item_copyright``
        * ``ttl``

Example usage
-------------
    Extra keyword arguments will be stored for `custom feed generators`_.

This example creates an Atom 1.0 feed and prints it to standard output::
    All parameters, if given, should be Unicode objects, except:

        * ``pubdate`` should be a `Python datetime object`_.
        * ``enclosure`` should be an instance of ``feedgenerator.Enclosure``.
        * ``categories`` should be a sequence of Unicode objects.
        
``SyndicationFeed.write(outfile, encoding)``
    Outputs the feed in the given ``encoding`` to ``outfile``, which must be a
    file-like object.

``SyndicationFeed.writeString(encoding)``
    Returns the feed as a string in the given ``encoding``.

For example, to create an Atom 1.0 feed and print it to standard output::

    >>> from django.utils import feedgenerator
    >>> f = feedgenerator.Atom1Feed(
@@ -857,12 +895,69 @@ This example creates an Atom 1.0 feed and prints it to standard output::
    ...     description=u"<p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p>")
    >>> print f.writeString('utf8')
    <?xml version="1.0" encoding="utf8"?>
    <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><title>My Weblog</title>
    <link href="http://www.example.com/"></link><id>http://www.example.com/</id>
    <updated>Sat, 12 Nov 2005 00:28:43 -0000</updated><entry><title>Hot dog today</title>
    <link>http://www.example.com/entries/1/</link><id>tag:www.example.com/entries/1/</id>
    <summary type="html">&lt;p&gt;Today I had a Vienna Beef hot dog. It was pink, plump and perfect.&lt;/p&gt;</summary>
    </entry></feed>
    <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    ...
    </feed>

.. _django/utils/feedgenerator.py: http://code.djangoproject.com/browser/django/trunk/django/utils/feedgenerator.py
.. _Python datetime object: http://www.python.org/doc/current/lib/module-datetime.html

Custom feed generators
----------------------

If you need to produce a custom feed format, you've got a couple of options.
    
If the feed format is totally custom, you'll want to subclass
``SyndicationFeed`` and completely replace the ``write()`` and
``writeString()`` methods.

However, if the feed format is a spin-off of RSS or Atom (i.e. GeoRSS_, Apple's
`iTunes podcast format`_, etc.), you've got a better choice. These types of
feeds typically add extra elements and/or attributes to the underlying format,
and there are a set of methods that ``SyndicationFeed`` calls to get these extra
attributes. Thus, you can subclass the appropriate feed generator class
(``Atom1Feed`` or ``Rss201rev2Feed``) and extend these callbacks. They are:
      
.. _georss: http://georss.org/
.. _itunes podcast format: http://www.apple.com/itunes/store/podcaststechspecs.html

``SyndicationFeed.root_attributes(self, )``
    Return a ``dict`` of attributes to add to the root feed element
    (``feed``/``channel``).
    
``SyndicationFeed.add_root_elements(self, handler)``
    Callback to add elements inside the root feed element
    (``feed``/``channel``). ``handler`` is an `XMLGenerator`_ from Python's
    built-in SAX library; you'll call methods on it to add to the XML
    document in process.
    
``SyndicationFeed.item_attributes(self, item)``
    Return a ``dict`` of attributes to add to each item (``item``/``entry``)
    element. The argument, ``item``, is a dictionary of all the data passed to
    ``SyndicationFeed.add_item()``.
    
``SyndicationFeed.add_item_elements(self, handler, item)``
    Callback to add elements to each item (``item``/``entry``) element.
    ``handler`` and ``item`` are as above.

.. warning::

    If you override any of these methods, be sure to call the superclass methods
    since they add the required elements for each feed format.

For example, you might start implementing an iTunes RSS feed generator like so::

    class iTunesFeed(Rss201rev2Feed):
        def root_attibutes(self):
            attrs = super(iTunesFeed, self).root_attibutes()
            attrs['xmlns:itunes'] = 'http://www.itunes.com/dtds/podcast-1.0.dtd
            return attrs
            
        def add_root_elements(self, handler):
            super(iTunesFeed, self).add_root_elements(handler)
            handler.addQuickElement('itunes:explicit', 'clean')

Obviously there's a lot more work to be done for a complete custom feed class,
but the above example should demonstrate the basic idea.

.. _XMLGenerator: http://docs.python.org/dev/library/xml.sax.utils.html#xml.sax.saxutils.XMLGenerator
 No newline at end of file
+25 −0
Original line number Diff line number Diff line
@@ -21,3 +21,28 @@ class TestRssFeed(feeds.Feed):

class TestAtomFeed(TestRssFeed):
    feed_type = Atom1Feed

class MyCustomAtom1Feed(Atom1Feed):
    """
    Test of a custom feed generator class.
    """    
    def root_attributes(self):
        attrs = super(MyCustomAtom1Feed, self).root_attributes()
        attrs[u'django'] = u'rocks'
        return attrs
        
    def add_root_elements(self, handler):
        super(MyCustomAtom1Feed, self).add_root_elements(handler)
        handler.addQuickElement(u'spam', u'eggs')
        
    def item_attributes(self, item):
        attrs = super(MyCustomAtom1Feed, self).item_attributes(item)
        attrs[u'bacon'] = u'yum'
        return attrs
        
    def add_item_elements(self, handler, item):
        super(MyCustomAtom1Feed, self).add_item_elements(handler, item)
        handler.addQuickElement(u'ministry', u'silly walks')
    
class TestCustomFeed(TestAtomFeed):
    feed_type = MyCustomAtom1Feed
+45 −3
Original line number Diff line number Diff line
@@ -4,21 +4,63 @@ from xml.dom import minidom
from django.test import TestCase
from django.test.client import Client
from models import Entry
try:
    set
except NameError:
    from sets import Set as set

class SyndicationFeedTest(TestCase):
    fixtures = ['feeddata.json']

    def assertChildNodes(self, elem, expected):
        actual = set([n.nodeName for n in elem.childNodes])
        expected = set(expected)
        self.assertEqual(actual, expected)

    def test_rss_feed(self):
        response = self.client.get('/syndication/feeds/rss/')
        doc = minidom.parseString(response.content)
        self.assertEqual(len(doc.getElementsByTagName('channel')), 1)
        self.assertEqual(len(doc.getElementsByTagName('item')), Entry.objects.count())

        chan = doc.getElementsByTagName('channel')[0]
        self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item'])
    
        items = chan.getElementsByTagName('item')
        self.assertEqual(len(items), Entry.objects.count())
        for item in items:
            self.assertChildNodes(item, ['title', 'link', 'description', 'guid'])
    
    def test_atom_feed(self):
        response = self.client.get('/syndication/feeds/atom/')
        doc = minidom.parseString(response.content)
        self.assertEqual(len(doc.getElementsByTagName('feed')), 1)
        self.assertEqual(len(doc.getElementsByTagName('entry')), Entry.objects.count())
        
        feed = doc.firstChild
        self.assertEqual(feed.nodeName, 'feed')
        self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry'])        
        
        entries = feed.getElementsByTagName('entry')
        self.assertEqual(len(entries), Entry.objects.count())
        for entry in entries:
            self.assertChildNodes(entry, ['title', 'link', 'id', 'summary'])
            summary = entry.getElementsByTagName('summary')[0]
            self.assertEqual(summary.getAttribute('type'), 'html')
    
    def test_custom_feed_generator(self):
        response = self.client.get('/syndication/feeds/custom/')
        doc = minidom.parseString(response.content)
        
        feed = doc.firstChild
        self.assertEqual(feed.nodeName, 'feed')
        self.assertEqual(feed.getAttribute('django'), 'rocks')
        self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry', 'spam'])        
        
        entries = feed.getElementsByTagName('entry')
        self.assertEqual(len(entries), Entry.objects.count())
        for entry in entries:
            self.assertEqual(entry.getAttribute('bacon'), 'yum')
            self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry'])
            summary = entry.getElementsByTagName('summary')[0]
            self.assertEqual(summary.getAttribute('type'), 'html')
        
    def test_complex_base_url(self):
        """
+2 −1
Original line number Diff line number Diff line
from feeds import TestRssFeed, TestAtomFeed, ComplexFeed
from feeds import TestRssFeed, TestAtomFeed, TestCustomFeed, ComplexFeed
from django.conf.urls.defaults import patterns

feed_dict = {
    'complex': ComplexFeed,
    'rss': TestRssFeed,
    'atom': TestAtomFeed,
    'custom': TestCustomFeed,
    
}
urlpatterns = patterns('',