Kaydet (Commit) 942e5246 authored tarafından Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss

Added a number of callbacks to SyndicationFeed for adding custom attributes and…

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
üst 899ca54f
This diff is collapsed.
...@@ -801,7 +801,12 @@ Behind the scenes, the high-level RSS framework uses a lower-level framework ...@@ -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: for generating feeds' XML. This framework lives in a single module:
`django/utils/feedgenerator.py`_. `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 The ``feedgenerator`` module contains a base class ``SyndicationFeed`` and
several subclasses: several subclasses:
...@@ -813,38 +818,71 @@ several subclasses: ...@@ -813,38 +818,71 @@ several subclasses:
Each of these three classes knows how to render a certain type of feed as XML. Each of these three classes knows how to render a certain type of feed as XML.
They share this interface: They share this interface:
``__init__(title, link, description, language=None, author_email=None,`` ``SyndicationFeed.__init__(**kwargs)``
``author_name=None, author_link=None, subtitle=None, categories=None,`` Initialize the feed with the given dictionary of metadata, which applies to
``feed_url=None)`` the entire feed. Required keyword arguments are:
Initializes the feed with the given metadata, which applies to the entire feed * ``title``
(i.e., not just to a specific item in the feed). * ``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,`` * ``language``
``pubdate=None, comments=None, unique_id=None, enclosure=None, categories=())`` * ``author_email``
* ``author_name``
Add an item to the feed with the given parameters. All parameters, if given, * ``author_link``
should be Unicode objects, except: * ``subtitle``
* ``categories``
* ``pubdate`` should be a `Python datetime object`_. * ``feed_url``
* ``enclosure`` should be an instance of ``feedgenerator.Enclosure``. * ``feed_copyright``
* ``categories`` should be a sequence of Unicode objects. * ``feed_guid``
* ``ttl``
``write(outfile, encoding)``
Any extra keyword arguments you pass to ``__init__`` will be stored in
Outputs the feed in the given encoding to outfile, which is a file-like object. ``self.feed`` for use with `custom feed generators`_.
``writeString(encoding)`` All parameters should be Unicode objects, except ``categories``, which
should be a sequence of Unicode objects.
Returns the feed as a string in the given encoding.
``SyndicationFeed.add_item(**kwargs)``
Example usage Add an item to the feed with the given parameters.
-------------
Required keyword arguments are:
This example creates an Atom 1.0 feed and prints it to standard output::
* ``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.
``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 >>> from django.utils import feedgenerator
>>> f = feedgenerator.Atom1Feed( >>> f = feedgenerator.Atom1Feed(
...@@ -857,12 +895,69 @@ This example creates an Atom 1.0 feed and prints it to standard output:: ...@@ -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>") ... description=u"<p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p>")
>>> print f.writeString('utf8') >>> print f.writeString('utf8')
<?xml version="1.0" encoding="utf8"?> <?xml version="1.0" encoding="utf8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><title>My Weblog</title> <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
<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> </feed>
<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>
.. _django/utils/feedgenerator.py: http://code.djangoproject.com/browser/django/trunk/django/utils/feedgenerator.py .. _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 .. _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
...@@ -21,3 +21,28 @@ class TestRssFeed(feeds.Feed): ...@@ -21,3 +21,28 @@ class TestRssFeed(feeds.Feed):
class TestAtomFeed(TestRssFeed): class TestAtomFeed(TestRssFeed):
feed_type = Atom1Feed 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,22 +4,64 @@ from xml.dom import minidom ...@@ -4,22 +4,64 @@ from xml.dom import minidom
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from models import Entry from models import Entry
try:
set
except NameError:
from sets import Set as set
class SyndicationFeedTest(TestCase): class SyndicationFeedTest(TestCase):
fixtures = ['feeddata.json'] 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): def test_rss_feed(self):
response = self.client.get('/syndication/feeds/rss/') response = self.client.get('/syndication/feeds/rss/')
doc = minidom.parseString(response.content) doc = minidom.parseString(response.content)
self.assertEqual(len(doc.getElementsByTagName('channel')), 1) 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): def test_atom_feed(self):
response = self.client.get('/syndication/feeds/atom/') response = self.client.get('/syndication/feeds/atom/')
doc = minidom.parseString(response.content) 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): def test_complex_base_url(self):
""" """
Tests that that the base url for a complex feed doesn't raise a 500 Tests that that the base url for a complex feed doesn't raise a 500
......
from feeds import TestRssFeed, TestAtomFeed, ComplexFeed from feeds import TestRssFeed, TestAtomFeed, TestCustomFeed, ComplexFeed
from django.conf.urls.defaults import patterns from django.conf.urls.defaults import patterns
feed_dict = { feed_dict = {
'complex': ComplexFeed, 'complex': ComplexFeed,
'rss': TestRssFeed, 'rss': TestRssFeed,
'atom': TestAtomFeed, 'atom': TestAtomFeed,
'custom': TestCustomFeed,
} }
urlpatterns = patterns('', urlpatterns = patterns('',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment