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
This commit is contained in:
parent
899ca54fe2
commit
942e5246ac
|
@ -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
|
||||
|
|
|
@ -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`_.
|
||||
|
||||
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.
|
||||
|
||||
Required keyword arguments are:
|
||||
|
||||
* ``title``
|
||||
* ``link``
|
||||
* ``description``
|
||||
|
||||
Optional keyword arguments are:
|
||||
|
||||
* ``author_email``
|
||||
* ``author_name``
|
||||
* ``author_link``
|
||||
* ``pubdate``
|
||||
* ``comments``
|
||||
* ``unique_id``
|
||||
* ``enclosure``
|
||||
* ``categories``
|
||||
* ``item_copyright``
|
||||
* ``ttl``
|
||||
|
||||
Extra keyword arguments will be stored for `custom feed generators`_.
|
||||
|
||||
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.
|
||||
|
||||
``write(outfile, encoding)``
|
||||
``SyndicationFeed.write(outfile, encoding)``
|
||||
Outputs the feed in the given ``encoding`` to ``outfile``, which must be a
|
||||
file-like object.
|
||||
|
||||
Outputs the feed in the given encoding to outfile, which is a file-like object.
|
||||
``SyndicationFeed.writeString(encoding)``
|
||||
Returns the feed as a string in the given ``encoding``.
|
||||
|
||||
``writeString(encoding)``
|
||||
|
||||
Returns the feed as a string in the given encoding.
|
||||
|
||||
Example usage
|
||||
-------------
|
||||
|
||||
This example creates an Atom 1.0 feed and prints it to standard output::
|
||||
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"><p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p></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
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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('',
|
||||
|
|
Loading…
Reference in New Issue