Changeset 8311
- Timestamp:
- 08/11/08 17:22:26 (11 months ago)
- Files:
-
- django/trunk/django/utils/feedgenerator.py (modified) (10 diffs)
- django/trunk/docs/syndication_feeds.txt (modified) (3 diffs)
- django/trunk/tests/regressiontests/syndication/feeds.py (modified) (1 diff)
- django/trunk/tests/regressiontests/syndication/tests.py (modified) (2 diffs)
- django/trunk/tests/regressiontests/syndication/urls.py (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
django/trunk/django/utils/feedgenerator.py
r8216 r8311 20 20 """ 21 21 22 import re 23 import datetime 22 24 from django.utils.xmlutils import SimplerXMLGenerator 23 25 from django.utils.encoding import force_unicode, iri_to_uri 24 import datetime, re, time25 26 26 27 def rfc2822_date(date): … … 57 58 def __init__(self, title, link, description, language=None, author_email=None, 58 59 author_name=None, author_link=None, subtitle=None, categories=None, 59 feed_url=None, feed_copyright=None, feed_guid=None, ttl=None ):60 feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs): 60 61 to_unicode = lambda s: force_unicode(s, strings_only=True) 61 62 if categories: … … 76 77 'ttl': ttl, 77 78 } 79 self.feed.update(kwargs) 78 80 self.items = [] 79 81 80 82 def add_item(self, title, link, description, author_email=None, 81 83 author_name=None, author_link=None, pubdate=None, comments=None, 82 unique_id=None, enclosure=None, categories=(), item_copyright=None, ttl=None): 84 unique_id=None, enclosure=None, categories=(), item_copyright=None, 85 ttl=None, **kwargs): 83 86 """ 84 87 Adds an item to the feed. All args are expected to be Python Unicode … … 89 92 if categories: 90 93 categories = [to_unicode(c) for c in categories] 91 self.items.append({94 item = { 92 95 'title': to_unicode(title), 93 96 'link': iri_to_uri(link), … … 103 106 'item_copyright': to_unicode(item_copyright), 104 107 'ttl': ttl, 105 }) 108 } 109 item.update(kwargs) 110 self.items.append(item) 106 111 107 112 def num_items(self): 108 113 return len(self.items) 109 114 115 def root_attributes(self): 116 """ 117 Return extra attributes to place on the root (i.e. feed/channel) element. 118 Called from write(). 119 """ 120 return {} 121 122 def add_root_elements(self, handler): 123 """ 124 Add elements in the the root (i.e. feed/channel) element. Called 125 from write(). 126 """ 127 pass 128 129 def item_attributes(self, item): 130 """ 131 Return extra attributes to place on each item (i.e. item/entry) element. 132 """ 133 return {} 134 135 def add_item_elements(self, handler, item): 136 """ 137 Add elements on each item (i.e. item/entry) element. 138 """ 139 pass 140 110 141 def write(self, outfile, encoding): 111 142 """ … … 149 180 handler.startDocument() 150 181 handler.startElement(u"rss", {u"version": self._version}) 151 handler.startElement(u"channel", {}) 182 handler.startElement(u"channel", self.root_attributes()) 183 self.add_root_elements(handler) 184 self.write_items(handler) 185 self.endChannelElement(handler) 186 handler.endElement(u"rss") 187 188 def write_items(self, handler): 189 for item in self.items: 190 handler.startElement(u'item', self.item_attributes(item)) 191 self.add_item_elements(handler, item) 192 handler.endElement(u"item") 193 194 def add_root_elements(self, handler): 152 195 handler.addQuickElement(u"title", self.feed['title']) 153 196 handler.addQuickElement(u"link", self.feed['link']) … … 162 205 if self.feed['ttl'] is not None: 163 206 handler.addQuickElement(u"ttl", self.feed['ttl']) 164 self.write_items(handler)165 self.endChannelElement(handler)166 handler.endElement(u"rss")167 207 168 208 def endChannelElement(self, handler): … … 171 211 class RssUserland091Feed(RssFeed): 172 212 _version = u"0.91" 173 def write_items(self, handler): 174 for item in self.items: 175 handler.startElement(u"item", {}) 176 handler.addQuickElement(u"title", item['title']) 177 handler.addQuickElement(u"link", item['link']) 178 if item['description'] is not None: 179 handler.addQuickElement(u"description", item['description']) 180 handler.endElement(u"item") 213 def add_item_elements(self, handler, item): 214 handler.addQuickElement(u"title", item['title']) 215 handler.addQuickElement(u"link", item['link']) 216 if item['description'] is not None: 217 handler.addQuickElement(u"description", item['description']) 181 218 182 219 class Rss201rev2Feed(RssFeed): 183 220 # Spec: http://blogs.law.harvard.edu/tech/rss 184 221 _version = u"2.0" 185 def write_items(self, handler): 186 for item in self.items: 187 handler.startElement(u"item", {}) 188 handler.addQuickElement(u"title", item['title']) 189 handler.addQuickElement(u"link", item['link']) 190 if item['description'] is not None: 191 handler.addQuickElement(u"description", item['description']) 192 193 # Author information. 194 if item["author_name"] and item["author_email"]: 195 handler.addQuickElement(u"author", "%s (%s)" % \ 196 (item['author_email'], item['author_name'])) 197 elif item["author_email"]: 198 handler.addQuickElement(u"author", item["author_email"]) 199 elif item["author_name"]: 200 handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"}) 201 202 if item['pubdate'] is not None: 203 handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('ascii')) 204 if item['comments'] is not None: 205 handler.addQuickElement(u"comments", item['comments']) 206 if item['unique_id'] is not None: 207 handler.addQuickElement(u"guid", item['unique_id']) 208 if item['ttl'] is not None: 209 handler.addQuickElement(u"ttl", item['ttl']) 210 211 # Enclosure. 212 if item['enclosure'] is not None: 213 handler.addQuickElement(u"enclosure", '', 214 {u"url": item['enclosure'].url, u"length": item['enclosure'].length, 215 u"type": item['enclosure'].mime_type}) 216 217 # Categories. 218 for cat in item['categories']: 219 handler.addQuickElement(u"category", cat) 220 221 handler.endElement(u"item") 222 def add_item_elements(self, handler, item): 223 handler.addQuickElement(u"title", item['title']) 224 handler.addQuickElement(u"link", item['link']) 225 if item['description'] is not None: 226 handler.addQuickElement(u"description", item['description']) 227 228 # Author information. 229 if item["author_name"] and item["author_email"]: 230 handler.addQuickElement(u"author", "%s (%s)" % \ 231 (item['author_email'], item['author_name'])) 232 elif item["author_email"]: 233 handler.addQuickElement(u"author", item["author_email"]) 234 elif item["author_name"]: 235 handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"}) 236 237 if item['pubdate'] is not None: 238 handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('ascii')) 239 if item['comments'] is not None: 240 handler.addQuickElement(u"comments", item['comments']) 241 if item['unique_id'] is not None: 242 handler.addQuickElement(u"guid", item['unique_id']) 243 if item['ttl'] is not None: 244 handler.addQuickElement(u"ttl", item['ttl']) 245 246 # Enclosure. 247 if item['enclosure'] is not None: 248 handler.addQuickElement(u"enclosure", '', 249 {u"url": item['enclosure'].url, u"length": item['enclosure'].length, 250 u"type": item['enclosure'].mime_type}) 251 252 # Categories. 253 for cat in item['categories']: 254 handler.addQuickElement(u"category", cat) 222 255 223 256 class Atom1Feed(SyndicationFeed): … … 225 258 mime_type = 'application/atom+xml' 226 259 ns = u"http://www.w3.org/2005/Atom" 260 227 261 def write(self, outfile, encoding): 228 262 handler = SimplerXMLGenerator(outfile, encoding) 229 263 handler.startDocument() 264 handler.startElement(u'feed', self.root_attributes()) 265 self.add_root_elements(handler) 266 self.write_items(handler) 267 handler.endElement(u"feed") 268 269 def root_element_attributes(self): 230 270 if self.feed['language'] is not None: 231 handler.startElement(u"feed", {u"xmlns": self.ns, u"xml:lang": self.feed['language']})271 return {u"xmlns": self.ns, u"xml:lang": self.feed['language']} 232 272 else: 233 handler.startElement(u"feed", {u"xmlns": self.ns}) 273 return {u"xmlns": self.ns} 274 275 def add_root_elements(self, handler): 234 276 handler.addQuickElement(u"title", self.feed['title']) 235 277 handler.addQuickElement(u"link", "", {u"rel": u"alternate", u"href": self.feed['link']}) … … 252 294 if self.feed['feed_copyright'] is not None: 253 295 handler.addQuickElement(u"rights", self.feed['feed_copyright']) 254 self.write_items(handler) 255 handler.endElement(u"feed") 256 296 257 297 def write_items(self, handler): 258 298 for item in self.items: 259 handler.startElement(u"entry", {}) 260 handler.addQuickElement(u"title", item['title']) 261 handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"alternate"}) 262 if item['pubdate'] is not None: 263 handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('ascii')) 264 265 # Author information. 266 if item['author_name'] is not None: 267 handler.startElement(u"author", {}) 268 handler.addQuickElement(u"name", item['author_name']) 269 if item['author_email'] is not None: 270 handler.addQuickElement(u"email", item['author_email']) 271 if item['author_link'] is not None: 272 handler.addQuickElement(u"uri", item['author_link']) 273 handler.endElement(u"author") 274 275 # Unique ID. 276 if item['unique_id'] is not None: 277 unique_id = item['unique_id'] 278 else: 279 unique_id = get_tag_uri(item['link'], item['pubdate']) 280 handler.addQuickElement(u"id", unique_id) 281 282 # Summary. 283 if item['description'] is not None: 284 handler.addQuickElement(u"summary", item['description'], {u"type": u"html"}) 285 286 # Enclosure. 287 if item['enclosure'] is not None: 288 handler.addQuickElement(u"link", '', 289 {u"rel": u"enclosure", 290 u"href": item['enclosure'].url, 291 u"length": item['enclosure'].length, 292 u"type": item['enclosure'].mime_type}) 293 294 # Categories. 295 for cat in item['categories']: 296 handler.addQuickElement(u"category", u"", {u"term": cat}) 297 298 # Rights. 299 if item['item_copyright'] is not None: 300 handler.addQuickElement(u"rights", item['item_copyright']) 301 299 handler.startElement(u"entry", self.item_attributes(item)) 300 self.add_item_elements(handler, item) 302 301 handler.endElement(u"entry") 302 303 def add_item_elements(self, handler, item): 304 handler.addQuickElement(u"title", item['title']) 305 handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"alternate"}) 306 if item['pubdate'] is not None: 307 handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('ascii')) 308 309 # Author information. 310 if item['author_name'] is not None: 311 handler.startElement(u"author", {}) 312 handler.addQuickElement(u"name", item['author_name']) 313 if item['author_email'] is not None: 314 handler.addQuickElement(u"email", item['author_email']) 315 if item['author_link'] is not None: 316 handler.addQuickElement(u"uri", item['author_link']) 317 handler.endElement(u"author") 318 319 # Unique ID. 320 if item['unique_id'] is not None: 321 unique_id = item['unique_id'] 322 else: 323 unique_id = get_tag_uri(item['link'], item['pubdate']) 324 handler.addQuickElement(u"id", unique_id) 325 326 # Summary. 327 if item['description'] is not None: 328 handler.addQuickElement(u"summary", item['description'], {u"type": u"html"}) 329 330 # Enclosure. 331 if item['enclosure'] is not None: 332 handler.addQuickElement(u"link", '', 333 {u"rel": u"enclosure", 334 u"href": item['enclosure'].url, 335 u"length": item['enclosure'].length, 336 u"type": item['enclosure'].mime_type}) 337 338 # Categories. 339 for cat in item['categories']: 340 handler.addQuickElement(u"category", u"", {u"term": cat}) 341 342 # Rights. 343 if item['item_copyright'] is not None: 344 handler.addQuickElement(u"rights", item['item_copyright']) 303 345 304 346 # This isolates the decision of what the system default is, so calling code can django/trunk/docs/syndication_feeds.txt
r7361 r8311 802 802 `django/utils/feedgenerator.py`_. 803 803 804 Feel free to use this framework on your own, for lower-level tasks. 804 You use this framework on your own, for lower-level feed generation. You can 805 also create custom feed generator subclasses for use with the ``feed_type`` 806 ``Feed`` option. 807 808 ``SyndicationFeed`` classes 809 --------------------------- 805 810 806 811 The ``feedgenerator`` module contains a base class ``SyndicationFeed`` and … … 814 819 They share this interface: 815 820 816 ``__init__(title, link, description, language=None, author_email=None,`` 817 ``author_name=None, author_link=None, subtitle=None, categories=None,`` 818 ``feed_url=None)`` 819 820 Initializes the feed with the given metadata, which applies to the entire feed 821 (i.e., not just to a specific item in the feed). 822 823 All parameters, if given, should be Unicode objects, except ``categories``, 824 which should be a sequence of Unicode objects. 825 826 ``add_item(title, link, description, author_email=None, author_name=None,`` 827 ``pubdate=None, comments=None, unique_id=None, enclosure=None, categories=())`` 828 829 Add an item to the feed with the given parameters. All parameters, if given, 830 should be Unicode objects, except: 831 832 * ``pubdate`` should be a `Python datetime object`_. 833 * ``enclosure`` should be an instance of ``feedgenerator.Enclosure``. 834 * ``categories`` should be a sequence of Unicode objects. 835 836 ``write(outfile, encoding)`` 837 838 Outputs the feed in the given encoding to outfile, which is a file-like object. 839 840 ``writeString(encoding)`` 841 842 Returns the feed as a string in the given encoding. 843 844 Example usage 845 ------------- 846 847 This example creates an Atom 1.0 feed and prints it to standard output:: 821 ``SyndicationFeed.__init__(**kwargs)`` 822 Initialize the feed with the given dictionary of metadata, which applies to 823 the entire feed. Required keyword arguments are: 824 825 * ``title`` 826 * ``link`` 827 * ``description`` 828 829 There's also a bunch of other optional keywords: 830 831 * ``language`` 832 * ``author_email`` 833 * ``author_name`` 834 * ``author_link`` 835 * ``subtitle`` 836 * ``categories`` 837 * ``feed_url`` 838 * ``feed_copyright`` 839 * ``feed_guid`` 840 * ``ttl`` 841 842 Any extra keyword arguments you pass to ``__init__`` will be stored in 843 ``self.feed`` for use with `custom feed generators`_. 844 845 All parameters should be Unicode objects, except ``categories``, which 846 should be a sequence of Unicode objects. 847 848 ``SyndicationFeed.add_item(**kwargs)`` 849 Add an item to the feed with the given parameters. 850 851 Required keyword arguments are: 852 853 * ``title`` 854 * ``link`` 855 * ``description`` 856 857 Optional keyword arguments are: 858 859 * ``author_email`` 860 * ``author_name`` 861 * ``author_link`` 862 * ``pubdate`` 863 * ``comments`` 864 * ``unique_id`` 865 * ``enclosure`` 866 * ``categories`` 867 * ``item_copyright`` 868 * ``ttl`` 869 870 Extra keyword arguments will be stored for `custom feed generators`_. 871 872 All parameters, if given, should be Unicode objects, except: 873 874 * ``pubdate`` should be a `Python datetime object`_. 875 * ``enclosure`` should be an instance of ``feedgenerator.Enclosure``. 876 * ``categories`` should be a sequence of Unicode objects. 877 878 ``SyndicationFeed.write(outfile, encoding)`` 879 Outputs the feed in the given ``encoding`` to ``outfile``, which must be a 880 file-like object. 881 882 ``SyndicationFeed.writeString(encoding)`` 883 Returns the feed as a string in the given ``encoding``. 884 885 For example, to create an Atom 1.0 feed and print it to standard output:: 848 886 849 887 >>> from django.utils import feedgenerator … … 858 896 >>> print f.writeString('utf8') 859 897 <?xml version="1.0" encoding="utf8"?> 860 <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><title>My Weblog</title> 861 <link href="http://www.example.com/"></link><id>http://www.example.com/</id> 862 <updated>Sat, 12 Nov 2005 00:28:43 -0000</updated><entry><title>Hot dog today</title> 863 <link>http://www.example.com/entries/1/</link><id>tag:www.example.com/entries/1/</id> 864 <summary type="html"><p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p></summary> 865 </entry></feed> 898 <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"> 899 ... 900 </feed> 866 901 867 902 .. _django/utils/feedgenerator.py: http://code.djangoproject.com/browser/django/trunk/django/utils/feedgenerator.py 868 903 .. _Python datetime object: http://www.python.org/doc/current/lib/module-datetime.html 904 905 Custom feed generators 906 ---------------------- 907 908 If you need to produce a custom feed format, you've got a couple of options. 909 910 If the feed format is totally custom, you'll want to subclass 911 ``SyndicationFeed`` and completely replace the ``write()`` and 912 ``writeString()`` methods. 913 914 However, if the feed format is a spin-off of RSS or Atom (i.e. GeoRSS_, Apple's 915 `iTunes podcast format`_, etc.), you've got a better choice. These types of 916 feeds typically add extra elements and/or attributes to the underlying format, 917 and there are a set of methods that ``SyndicationFeed`` calls to get these extra 918 attributes. Thus, you can subclass the appropriate feed generator class 919 (``Atom1Feed`` or ``Rss201rev2Feed``) and extend these callbacks. They are: 920 921 .. _georss: http://georss.org/ 922 .. _itunes podcast format: http://www.apple.com/itunes/store/podcaststechspecs.html 923 924 ``SyndicationFeed.root_attributes(self, )`` 925 Return a ``dict`` of attributes to add to the root feed element 926 (``feed``/``channel``). 927 928 ``SyndicationFeed.add_root_elements(self, handler)`` 929 Callback to add elements inside the root feed element 930 (``feed``/``channel``). ``handler`` is an `XMLGenerator`_ from Python's 931 built-in SAX library; you'll call methods on it to add to the XML 932 document in process. 933 934 ``SyndicationFeed.item_attributes(self, item)`` 935 Return a ``dict`` of attributes to add to each item (``item``/``entry``) 936 element. The argument, ``item``, is a dictionary of all the data passed to 937 ``SyndicationFeed.add_item()``. 938 939 ``SyndicationFeed.add_item_elements(self, handler, item)`` 940 Callback to add elements to each item (``item``/``entry``) element. 941 ``handler`` and ``item`` are as above. 942 943 .. warning:: 944 945 If you override any of these methods, be sure to call the superclass methods 946 since they add the required elements for each feed format. 947 948 For example, you might start implementing an iTunes RSS feed generator like so:: 949 950 class iTunesFeed(Rss201rev2Feed): 951 def root_attibutes(self): 952 attrs = super(iTunesFeed, self).root_attibutes() 953 attrs['xmlns:itunes'] = 'http://www.itunes.com/dtds/podcast-1.0.dtd 954 return attrs 955 956 def add_root_elements(self, handler): 957 super(iTunesFeed, self).add_root_elements(handler) 958 handler.addQuickElement('itunes:explicit', 'clean') 959 960 Obviously there's a lot more work to be done for a complete custom feed class, 961 but the above example should demonstrate the basic idea. 962 963 .. _XMLGenerator: http://docs.python.org/dev/library/xml.sax.utils.html#xml.sax.saxutils.XMLGenerator django/trunk/tests/regressiontests/syndication/feeds.py
r8310 r8311 22 22 class TestAtomFeed(TestRssFeed): 23 23 feed_type = Atom1Feed 24 25 class MyCustomAtom1Feed(Atom1Feed): 26 """ 27 Test of a custom feed generator class. 28 """ 29 def root_attributes(self): 30 attrs = super(MyCustomAtom1Feed, self).root_attributes() 31 attrs[u'django'] = u'rocks' 32 return attrs 33 34 def add_root_elements(self, handler): 35 super(MyCustomAtom1Feed, self).add_root_elements(handler) 36 handler.addQuickElement(u'spam', u'eggs') 37 38 def item_attributes(self, item): 39 attrs = super(MyCustomAtom1Feed, self).item_attributes(item) 40 attrs[u'bacon'] = u'yum' 41 return attrs 42 43 def add_item_elements(self, handler, item): 44 super(MyCustomAtom1Feed, self).add_item_elements(handler, item) 45 handler.addQuickElement(u'ministry', u'silly walks') 46 47 class TestCustomFeed(TestAtomFeed): 48 feed_type = MyCustomAtom1Feed django/trunk/tests/regressiontests/syndication/tests.py
r8310 r8311 5 5 from django.test.client import Client 6 6 from models import Entry 7 try: 8 set 9 except NameError: 10 from sets import Set as set 7 11 8 12 class SyndicationFeedTest(TestCase): 9 13 fixtures = ['feeddata.json'] 14 15 def assertChildNodes(self, elem, expected): 16 actual = set([n.nodeName for n in elem.childNodes]) 17 expected = set(expected) 18 self.assertEqual(actual, expected) 10 19 11 20 def test_rss_feed(self): … … 13 22 doc = minidom.parseString(response.content) 14 23 self.assertEqual(len(doc.getElementsByTagName('channel')), 1) 15 self.assertEqual(len(doc.getElementsByTagName('item')), Entry.objects.count()) 24 25 chan = doc.getElementsByTagName('channel')[0] 26 self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item']) 27 28 items = chan.getElementsByTagName('item') 29 self.assertEqual(len(items), Entry.objects.count()) 30 for item in items: 31 self.assertChildNodes(item, ['title', 'link', 'description', 'guid']) 16 32 17 33 def test_atom_feed(self): 18 34 response = self.client.get('/syndication/feeds/atom/') 19 35 doc = minidom.parseString(response.content) 20 self.assertEqual(len(doc.getElementsByTagName('feed')), 1) 21 self.assertEqual(len(doc.getElementsByTagName('entry')), Entry.objects.count()) 36 37 feed = doc.firstChild 38 self.assertEqual(feed.nodeName, 'feed') 39 self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry']) 40 41 entries = feed.getElementsByTagName('entry') 42 self.assertEqual(len(entries), Entry.objects.count()) 43 for entry in entries: 44 self.assertChildNodes(entry, ['title', 'link', 'id', 'summary']) 45 summary = entry.getElementsByTagName('summary')[0] 46 self.assertEqual(summary.getAttribute('type'), 'html') 22 47 48 def test_custom_feed_generator(self): 49 response = self.client.get('/syndication/feeds/custom/') 50 doc = minidom.parseString(response.content) 51 52 feed = doc.firstChild 53 self.assertEqual(feed.nodeName, 'feed') 54 self.assertEqual(feed.getAttribute('django'), 'rocks') 55 self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry', 'spam']) 56 57 entries = feed.getElementsByTagName('entry') 58 self.assertEqual(len(entries), Entry.objects.count()) 59 for entry in entries: 60 self.assertEqual(entry.getAttribute('bacon'), 'yum') 61 self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry']) 62 summary = entry.getElementsByTagName('summary')[0] 63 self.assertEqual(summary.getAttribute('type'), 'html') 64 23 65 def test_complex_base_url(self): 24 66 """ django/trunk/tests/regressiontests/syndication/urls.py
r8310 r8311 1 from feeds import TestRssFeed, TestAtomFeed, ComplexFeed1 from feeds import TestRssFeed, TestAtomFeed, TestCustomFeed, ComplexFeed 2 2 from django.conf.urls.defaults import patterns 3 3 … … 6 6 'rss': TestRssFeed, 7 7 'atom': TestAtomFeed, 8 'custom': TestCustomFeed, 8 9 9 10 }
