Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Completely refactored legacy RSS framework to the new django.contrib.…

…syndication package. Also added Atom support, changed the way feeds are registered and added documentation for the whole lot. This is backwards-incompatible, but the RSS framework had not yet been documented, so this should only affect tinkerers and WorldOnline. Fixes #329, #498, #502 and #554. Thanks for various patches/ideas to alastair, ismael, hugo, eric moritz and garthk

git-svn-id: http://code.djangoproject.com/svn/django/trunk@1194 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 944de9e9e638bc239b03f67f459ad4abe4673e48 1 parent e8ae356
Adrian Holovaty authored
6  django/conf/urls/rss.py
... ...
@@ -1,6 +0,0 @@
1  
-from django.conf.urls.defaults import *
2  
-
3  
-urlpatterns = patterns('django.views',
4  
-    (r'^(?P<slug>[^/]+)/$', 'rss.rss.feed'),
5  
-    (r'^(?P<slug>[^/]+)/(?P<param>.+)/$', 'rss.rss.feed'),
6  
-)
89  django/contrib/syndication/feeds.py
... ...
@@ -0,0 +1,89 @@
  1
+from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
  2
+from django.core.template import Context, loader, Template, TemplateDoesNotExist
  3
+from django.models.core import sites
  4
+from django.utils import feedgenerator
  5
+from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE
  6
+
  7
+def add_domain(domain, url):
  8
+    if not url.startswith('http://'):
  9
+        url = u'http://%s%s' % (domain, url)
  10
+    return url
  11
+
  12
+class FeedDoesNotExist(ObjectDoesNotExist):
  13
+    pass
  14
+
  15
+class Feed:
  16
+    item_pubdate = None
  17
+    item_enclosure_url = None
  18
+    feed_type = feedgenerator.DefaultFeed
  19
+
  20
+    def __init__(self, slug):
  21
+        self.slug = slug
  22
+
  23
+    def item_link(self, item):
  24
+        try:
  25
+            return item.get_absolute_url()
  26
+        except AttributeError:
  27
+            raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__
  28
+
  29
+    def __get_dynamic_attr(self, attname, obj):
  30
+        attr = getattr(self, attname)
  31
+        if callable(attr):
  32
+            try:
  33
+                return attr(obj)
  34
+            except TypeError:
  35
+                return attr()
  36
+        return attr
  37
+
  38
+    def get_feed(self, url=None):
  39
+        """
  40
+        Returns a feedgenerator.DefaultFeed object, fully populated, for
  41
+        this feed. Raises FeedDoesNotExist for invalid parameters.
  42
+        """
  43
+        if url:
  44
+            try:
  45
+                obj = self.get_object(url.split('/'))
  46
+            except (AttributeError, ObjectDoesNotExist):
  47
+                raise FeedDoesNotExist
  48
+        else:
  49
+            obj = None
  50
+
  51
+        current_site = sites.get_current()
  52
+        link = self.__get_dynamic_attr('link', obj)
  53
+        link = add_domain(current_site.domain, link)
  54
+
  55
+        feed = self.feed_type(
  56
+            title = self.__get_dynamic_attr('title', obj),
  57
+            link = link,
  58
+            description = self.__get_dynamic_attr('description', obj),
  59
+            language = LANGUAGE_CODE.decode()
  60
+        )
  61
+
  62
+        try:
  63
+            title_template = loader.get_template('feeds/%s_title' % self.slug)
  64
+        except TemplateDoesNotExist:
  65
+            title_template = Template('{{ obj }}')
  66
+        try:
  67
+            description_template = loader.get_template('feeds/%s_description' % self.slug)
  68
+        except TemplateDoesNotExist:
  69
+            description_template = Template('{{ obj }}')
  70
+
  71
+        for item in self.__get_dynamic_attr('items', obj):
  72
+            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
  73
+            enc = None
  74
+            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
  75
+            if enc_url:
  76
+                enc = feedgenerator.Enclosure(
  77
+                    url = enc_url.decode('utf-8'),
  78
+                    length = str(self.__get_dynamic_attr('item_enclosure_length', item)).decode('utf-8'),
  79
+                    mime_type = self.__get_dynamic_attr('item_enclosure_mime_type', item).decode('utf-8'),
  80
+                )
  81
+            feed.add_item(
  82
+                title = title_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'),
  83
+                link = link,
  84
+                description = description_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'),
  85
+                unique_id = link,
  86
+                enclosure = enc,
  87
+                pubdate = self.__get_dynamic_attr('item_pubdate', item),
  88
+            )
  89
+        return feed
26  django/contrib/syndication/views.py
... ...
@@ -0,0 +1,26 @@
  1
+from django.contrib.syndication import feeds
  2
+from django.core.exceptions import Http404
  3
+from django.utils.httpwrappers import HttpResponse
  4
+
  5
+def feed(request, url, feed_dict=None):
  6
+    if not feed_dict:
  7
+        raise Http404, "No feeds are registered."
  8
+
  9
+    try:
  10
+        slug, param = url.split('/', 1)
  11
+    except ValueError:
  12
+        slug, param = url, ''
  13
+
  14
+    try:
  15
+        f = feed_dict[slug]
  16
+    except KeyError:
  17
+        raise Http404, "Slug %r isn't registered." % slug
  18
+
  19
+    try:
  20
+        feedgen = f(slug).get_feed(param)
  21
+    except feeds.FeedDoesNotExist:
  22
+        raise Http404, "Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug
  23
+
  24
+    response = HttpResponse(mimetype='application/xml')
  25
+    feedgen.write(response, 'utf-8')
  26
+    return response
223  django/core/rss.py
... ...
@@ -1,223 +0,0 @@
1  
-from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
2  
-from django.core.template import Context, loader, Template, TemplateDoesNotExist
3  
-from django.models.core import sites
4  
-from django.utils import feedgenerator
5  
-from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE
6  
-
7  
-def add_domain(domain, url):
8  
-    if not url.startswith('http://'):
9  
-        url = u'http://%s%s' % (domain, url)
10  
-    return url
11  
-
12  
-class FeedDoesNotExist(ObjectDoesNotExist):
13  
-    pass
14  
-
15  
-class Feed:
16  
-    item_pubdate = None
17  
-    item_enclosure_url = None
18  
-
19  
-    def item_link(self, item):
20  
-        try:
21  
-            return item.get_absolute_url()
22  
-        except AttributeError:
23  
-            raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your RSS class." % item.__class__.__name__
24  
-
25  
-    def __get_dynamic_attr(self, attname, obj):
26  
-        attr = getattr(self, attname)
27  
-        if callable(attr):
28  
-            try:
29  
-                return attr(obj)
30  
-            except TypeError:
31  
-                return attr()
32  
-        return attr
33  
-
34  
-    def get_feed(self, url=None):
35  
-        """
36  
-        Returns a feedgenerator.DefaultRssFeed object, fully populated, for
37  
-        this feed. Raises FeedDoesNotExist for invalid parameters.
38  
-        """
39  
-        if url:
40  
-            try:
41  
-                obj = self.get_object(url.split('/'))
42  
-            except (AttributeError, ObjectDoesNotExist):
43  
-                raise FeedDoesNotExist
44  
-        else:
45  
-            obj = None
46  
-
47  
-        current_site = sites.get_current()
48  
-        link = self.__get_dynamic_attr('link', obj)
49  
-        link = add_domain(current_site.domain, link)
50  
-
51  
-        feed = feedgenerator.DefaultRssFeed(
52  
-            title = self.__get_dynamic_attr('title', obj),
53  
-            link = link,
54  
-            description = self.__get_dynamic_attr('description', obj),
55  
-            language = LANGUAGE_CODE.decode()
56  
-        )
57  
-
58  
-        try:
59  
-            title_template = loader.get_template('rss/%s_title' % self.slug)
60  
-        except TemplateDoesNotExist:
61  
-            title_template = Template('{{ obj }}')
62  
-        try:
63  
-            description_template = loader.get_template('rss/%s_description' % self.slug)
64  
-        except TemplateDoesNotExist:
65  
-            description_template = Template('{{ obj }}')
66  
-
67  
-        for item in self.__get_dynamic_attr('items', obj):
68  
-            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
69  
-            enc = None
70  
-            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
71  
-            if enc_url:
72  
-                enc = feedgenerator.Enclosure(
73  
-                    url = enc_url.decode('utf-8'),
74  
-                    length = str(self.__get_dynamic_attr('item_enclosure_length', item)).decode('utf-8'),
75  
-                    mime_type = self.__get_dynamic_attr('item_enclosure_mime_type', item).decode('utf-8'),
76  
-                )
77  
-            feed.add_item(
78  
-                title = title_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'),
79  
-                link = link,
80  
-                description = description_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'),
81  
-                unique_id = link,
82  
-                enclosure = enc,
83  
-                pubdate = self.__get_dynamic_attr('item_pubdate', item),
84  
-            )
85  
-        return feed
86  
-
87  
-# DEPRECATED
88  
-class FeedConfiguration:
89  
-    def __init__(self, slug, title_cb, link_cb, description_cb, get_list_func_cb, get_list_kwargs,
90  
-        param_func=None, param_kwargs_cb=None, get_list_kwargs_cb=None, get_pubdate_cb=None,
91  
-        enc_url=None, enc_length=None, enc_mime_type=None):
92  
-        """
93  
-        slug -- Normal Python string. Used to register the feed.
94  
-
95  
-        title_cb, link_cb, description_cb -- Functions that take the param
96  
-        (if applicable) and return a normal Python string.
97  
-
98  
-        get_list_func_cb -- Function that takes the param and returns a
99  
-        function to use in retrieving items.
100  
-
101  
-        get_list_kwargs -- Dictionary of kwargs to pass to the function
102  
-        returned by get_list_func_cb.
103  
-
104  
-        param_func -- Function to use in retrieving the param (if applicable).
105  
-
106  
-        param_kwargs_cb -- Function that takes the slug and returns a
107  
-        dictionary of kwargs to use in param_func.
108  
-
109  
-        get_list_kwargs_cb -- Function that takes the param and returns a
110  
-        dictionary to use in addition to get_list_kwargs (if applicable).
111  
-
112  
-        get_pubdate_cb -- Function that takes the object and returns a datetime
113  
-        to use as the publication date in the feed.
114  
-
115  
-        The three enc_* parameters are strings representing methods or
116  
-        attributes to call on a particular item to get its enclosure
117  
-        information. Each of those methods/attributes should return a normal
118  
-        Python string.
119  
-        """
120  
-        self.slug = slug
121  
-        self.title_cb, self.link_cb = title_cb, link_cb
122  
-        self.description_cb = description_cb
123  
-        self.get_list_func_cb = get_list_func_cb
124  
-        self.get_list_kwargs = get_list_kwargs
125  
-        self.param_func, self.param_kwargs_cb = param_func, param_kwargs_cb
126  
-        self.get_list_kwargs_cb = get_list_kwargs_cb
127  
-        self.get_pubdate_cb = get_pubdate_cb
128  
-        assert (None == enc_url == enc_length == enc_mime_type) or (enc_url is not None and enc_length is not None and enc_mime_type is not None)
129  
-        self.enc_url = enc_url
130  
-        self.enc_length = enc_length
131  
-        self.enc_mime_type = enc_mime_type
132  
-
133  
-    def get_feed(self, param_slug=None):
134  
-        """
135  
-        Returns a utils.feedgenerator.DefaultRssFeed object, fully populated,
136  
-        representing this FeedConfiguration.
137  
-        """
138  
-        if param_slug:
139  
-            try:
140  
-                param = self.param_func(**self.param_kwargs_cb(param_slug))
141  
-            except ObjectDoesNotExist:
142  
-                raise FeedIsNotRegistered
143  
-        else:
144  
-            param = None
145  
-        current_site = sites.get_current()
146  
-        f = self._get_feed_generator_object(param)
147  
-        title_template = loader.get_template('rss/%s_title' % self.slug)
148  
-        description_template = loader.get_template('rss/%s_description' % self.slug)
149  
-        kwargs = self.get_list_kwargs.copy()
150  
-        if param and self.get_list_kwargs_cb:
151  
-            kwargs.update(self.get_list_kwargs_cb(param))
152  
-        get_list_func = self.get_list_func_cb(param)
153  
-        for obj in get_list_func(**kwargs):
154  
-            link = obj.get_absolute_url()
155  
-            if not link.startswith('http://'):
156  
-                link = u'http://%s%s' % (current_site.domain, link)
157  
-            enc = None
158  
-            if self.enc_url:
159  
-                enc_url = getattr(obj, self.enc_url)
160  
-                enc_length = getattr(obj, self.enc_length)
161  
-                enc_mime_type = getattr(obj, self.enc_mime_type)
162  
-                try:
163  
-                    enc_url = enc_url()
164  
-                except TypeError:
165  
-                    pass
166  
-                try:
167  
-                    enc_length = enc_length()
168  
-                except TypeError:
169  
-                    pass
170  
-                try:
171  
-                    enc_mime_type = enc_mime_type()
172  
-                except TypeError:
173  
-                    pass
174  
-                enc = feedgenerator.Enclosure(enc_url.decode('utf-8'),
175  
-                    (enc_length and str(enc_length).decode('utf-8') or ''), enc_mime_type.decode('utf-8'))
176  
-            f.add_item(
177  
-                title = title_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'),
178  
-                link = link,
179  
-                description = description_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'),
180  
-                unique_id=link,
181  
-                enclosure=enc,
182  
-                pubdate = self.get_pubdate_cb and self.get_pubdate_cb(obj) or None,
183  
-            )
184  
-        return f
185  
-
186  
-    def _get_feed_generator_object(self, param):
187  
-        current_site = sites.get_current()
188  
-        link = self.link_cb(param).decode()
189  
-        if not link.startswith('http://'):
190  
-            link = u'http://%s%s' % (current_site.domain, link)
191  
-        return feedgenerator.DefaultRssFeed(
192  
-            title = self.title_cb(param).decode(),
193  
-            link = link,
194  
-            description = self.description_cb(param).decode(),
195  
-            language = LANGUAGE_CODE.decode(),
196  
-        )
197  
-
198  
-
199  
-# global dict used by register_feed and get_registered_feed
200  
-_registered_feeds = {}
201  
-
202  
-# DEPRECATED
203  
-class FeedIsNotRegistered(Exception):
204  
-    pass
205  
-
206  
-# DEPRECATED
207  
-def register_feed(feed):
208  
-    _registered_feeds[feed.slug] = feed
209  
-
210  
-def register_feeds(*feeds):
211  
-    for f in feeds:
212  
-        _registered_feeds[f.slug] = f
213  
-
214  
-def get_registered_feed(slug):
215  
-    # try to load a RSS settings module so that feeds can be registered
216  
-    try:
217  
-        __import__(SETTINGS_MODULE + '_rss', '', '', [''])
218  
-    except (KeyError, ImportError, ValueError):
219  
-        pass
220  
-    try:
221  
-        return _registered_feeds[slug]
222  
-    except KeyError:
223  
-        raise FeedIsNotRegistered
221  django/utils/feedgenerator.py
@@ -19,21 +19,42 @@
19 19
 """
20 20
 
21 21
 from django.utils.xmlutils import SimplerXMLGenerator
  22
+import datetime, re, time
  23
+import email.Utils
  24
+from xml.dom import minidom
  25
+from xml.parsers.expat import ExpatError
  26
+
  27
+def rfc2822_date(date):
  28
+    return email.Utils.formatdate(time.mktime(date.timetuple()))
  29
+
  30
+def get_tag_uri(url, date):
  31
+    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
  32
+    tag = re.sub('^http://', '', url)
  33
+    if date is not None:
  34
+        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
  35
+    tag = re.sub('#', '/', tag)
  36
+    return 'tag:' + tag
22 37
 
23 38
 class SyndicationFeed:
24 39
     "Base class for all syndication feeds. Subclasses should provide write()"
25  
-    def __init__(self, title, link, description, language=None):
26  
-        self.feed_info = {
  40
+    def __init__(self, title, link, description, language=None, author_email=None,
  41
+            author_name=None, author_link=None, subtitle=None, categories=None):
  42
+        self.feed = {
27 43
             'title': title,
28 44
             'link': link,
29 45
             'description': description,
30 46
             'language': language,
  47
+            'author_email': author_email,
  48
+            'author_name': author_name,
  49
+            'author_link': author_link,
  50
+            'subtitle': subtitle,
  51
+            'categories': categories or (),
31 52
         }
32 53
         self.items = []
33 54
 
34 55
     def add_item(self, title, link, description, author_email=None,
35  
-        author_name=None, pubdate=None, comments=None, unique_id=None,
36  
-        enclosure=None, categories=None):
  56
+        author_name=None, pubdate=None, comments=None,
  57
+        unique_id=None, enclosure=None, categories=()):
37 58
         """
38 59
         Adds an item to the feed. All args are expected to be Python Unicode
39 60
         objects except pubdate, which is a datetime.datetime object, and
@@ -49,7 +70,7 @@ def add_item(self, title, link, description, author_email=None,
49 70
             'comments': comments,
50 71
             'unique_id': unique_id,
51 72
             'enclosure': enclosure,
52  
-            'categories': categories or [],
  73
+            'categories': categories or (),
53 74
         })
54 75
 
55 76
     def num_items(self):
@@ -71,6 +92,18 @@ def writeString(self, encoding):
71 92
         self.write(s, encoding)
72 93
         return s.getvalue()
73 94
 
  95
+    def latest_post_date(self):
  96
+        """
  97
+        Returns the latest item's pubdate. If none of them have a pubdate,
  98
+        this returns the current date/time.
  99
+        """
  100
+        updates = [i['pubdate'] for i in self.items if i['pubdate'] is not None]
  101
+        if len(updates) > 0:
  102
+            updates.sort()
  103
+            return updates[-1]
  104
+        else:
  105
+            return datetime.datetime.now()
  106
+
74 107
 class Enclosure:
75 108
     "Represents an RSS enclosure"
76 109
     def __init__(self, url, length, mime_type):
@@ -81,72 +114,136 @@ class RssFeed(SyndicationFeed):
81 114
     def write(self, outfile, encoding):
82 115
         handler = SimplerXMLGenerator(outfile, encoding)
83 116
         handler.startDocument()
84  
-        self.writeRssElement(handler)
85  
-        self.writeChannelElement(handler)
86  
-        for item in self.items:
87  
-            self.writeRssItem(handler, item)
  117
+        handler.startElement(u"rss", {u"version": self._version})
  118
+        handler.startElement(u"channel", {})
  119
+        handler.addQuickElement(u"title", self.feed['title'])
  120
+        handler.addQuickElement(u"link", self.feed['link'])
  121
+        handler.addQuickElement(u"description", self.feed['description'])
  122
+        if self.feed['language'] is not None:
  123
+            handler.addQuickElement(u"language", self.feed['language'])
  124
+        self.write_items(handler)
88 125
         self.endChannelElement(handler)
89  
-        self.endRssElement(handler)
90  
-
91  
-    def writeRssElement(self, handler):
92  
-        "Adds the <rss> element to handler, taking care of versioning, etc."
93  
-        raise NotImplementedError
94  
-
95  
-    def endRssElement(self, handler):
96  
-        "Ends the <rss> element."
97 126
         handler.endElement(u"rss")
98 127
 
99  
-    def writeChannelElement(self, handler):
100  
-        handler.startElement(u"channel", {})
101  
-        handler.addQuickElement(u"title", self.feed_info['title'], {})
102  
-        handler.addQuickElement(u"link", self.feed_info['link'], {})
103  
-        handler.addQuickElement(u"description", self.feed_info['description'], {})
104  
-        if self.feed_info['language'] is not None:
105  
-            handler.addQuickElement(u"language", self.feed_info['language'], {})
106  
-
107 128
     def endChannelElement(self, handler):
108 129
         handler.endElement(u"channel")
109 130
 
110 131
 class RssUserland091Feed(RssFeed):
111  
-    def writeRssElement(self, handler):
112  
-        handler.startElement(u"rss", {u"version": u"0.91"})
113  
-
114  
-    def writeRssItem(self, handler, item):
115  
-        handler.startElement(u"item", {})
116  
-        handler.addQuickElement(u"title", item['title'], {})
117  
-        handler.addQuickElement(u"link", item['link'], {})
118  
-        if item['description'] is not None:
119  
-            handler.addQuickElement(u"description", item['description'], {})
120  
-        handler.endElement(u"item")
  132
+    _version = u"0.91"
  133
+    def write_items(self, handler):
  134
+        for item in self.items:
  135
+            handler.startElement(u"item", {})
  136
+            handler.addQuickElement(u"title", item['title'])
  137
+            handler.addQuickElement(u"link", item['link'])
  138
+            if item['description'] is not None:
  139
+                handler.addQuickElement(u"description", item['description'])
  140
+            handler.endElement(u"item")
121 141
 
122 142
 class Rss201rev2Feed(RssFeed):
123 143
     # Spec: http://blogs.law.harvard.edu/tech/rss
124  
-    def writeRssElement(self, handler):
125  
-        handler.startElement(u"rss", {u"version": u"2.0"})
126  
-
127  
-    def writeRssItem(self, handler, item):
128  
-        handler.startElement(u"item", {})
129  
-        handler.addQuickElement(u"title", item['title'], {})
130  
-        handler.addQuickElement(u"link", item['link'], {})
131  
-        if item['description'] is not None:
132  
-            handler.addQuickElement(u"description", item['description'], {})
133  
-        if item['author_email'] is not None and item['author_name'] is not None:
134  
-            handler.addQuickElement(u"author", u"%s (%s)" % \
135  
-                (item['author_email'], item['author_name']), {})
136  
-        if item['pubdate'] is not None:
137  
-            handler.addQuickElement(u"pubDate", item['pubdate'].strftime('%a, %d %b %Y %H:%M:%S %Z'), {})
138  
-        if item['comments'] is not None:
139  
-            handler.addQuickElement(u"comments", item['comments'], {})
140  
-        if item['unique_id'] is not None:
141  
-            handler.addQuickElement(u"guid", item['unique_id'], {})
142  
-        if item['enclosure'] is not None:
143  
-            handler.addQuickElement(u"enclosure", '',
144  
-                {u"url": item['enclosure'].url, u"length": item['enclosure'].length,
145  
-                    u"type": item['enclosure'].mime_type})
146  
-        for cat in item['categories']:
147  
-            handler.addQuickElement(u"category", cat, {})
148  
-        handler.endElement(u"item")
  144
+    _version = u"2.0"
  145
+    def write_items(self, handler):
  146
+        for item in self.items:
  147
+            handler.startElement(u"item", {})
  148
+            handler.addQuickElement(u"title", item['title'])
  149
+            handler.addQuickElement(u"link", item['link'])
  150
+            if item['description'] is not None:
  151
+                handler.addQuickElement(u"description", item['description'])
  152
+
  153
+            # Author information.
  154
+            if item['author_email'] is not None and item['author_name'] is not None:
  155
+                handler.addQuickElement(u"author", u"%s (%s)" % \
  156
+                    (item['author_email'], item['author_name']))
  157
+
  158
+            if item['pubdate'] is not None:
  159
+                handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('ascii'))
  160
+            if item['comments'] is not None:
  161
+                handler.addQuickElement(u"comments", item['comments'])
  162
+            if item['unique_id'] is not None:
  163
+                handler.addQuickElement(u"guid", item['unique_id'])
  164
+
  165
+            # Enclosure.
  166
+            if item['enclosure'] is not None:
  167
+                handler.addQuickElement(u"enclosure", '',
  168
+                    {u"url": item['enclosure'].url, u"length": item['enclosure'].length,
  169
+                        u"type": item['enclosure'].mime_type})
  170
+
  171
+            # Categories.
  172
+            for cat in item['categories']:
  173
+                handler.addQuickElement(u"category", cat)
  174
+
  175
+            handler.endElement(u"item")
  176
+
  177
+class Atom1Feed(SyndicationFeed):
  178
+    # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html
  179
+    ns = u"http://www.w3.org/2005/Atom"
  180
+    def write(self, outfile, encoding):
  181
+        handler = SimplerXMLGenerator(outfile, encoding)
  182
+        handler.startDocument()
  183
+        if self.feed['language'] is not None:
  184
+            handler.startElement(u"feed", {u"xmlns": self.ns, u"xml:lang": self.feed['language']})
  185
+        else:
  186
+            handler.startElement(u"feed", {u"xmlns": self.ns})
  187
+        handler.addQuickElement(u"title", self.feed['title'])
  188
+        handler.addQuickElement(u"link", "", {u"href": self.feed['link']})
  189
+        handler.addQuickElement(u"id", self.feed['link'])
  190
+        handler.addQuickElement(u"updated", rfc2822_date(self.latest_post_date()).decode('ascii'))
  191
+        if self.feed['author_name'] is not None:
  192
+            handler.startElement(u"author", {})
  193
+            handler.addQuickElement(u"name", self.feed['author_name'])
  194
+            if self.feed['author_email'] is not None:
  195
+                handler.addQuickElement(u"email", self.feed['author_email'])
  196
+            if self.feed['author_link'] is not None:
  197
+                handler.addQuickElement(u"uri", self.feed['author_link'])
  198
+            handler.endElement(u"author")
  199
+        if self.feed['subtitle'] is not None:
  200
+            handler.addQuickElement(u"subtitle", self.feed['subtitle'])
  201
+        for cat in self.feed['categories']:
  202
+            handler.addQuickElement(u"category", "", {u"term": cat})
  203
+        self.write_items(handler)
  204
+        handler.endElement(u"feed")
  205
+
  206
+    def write_items(self, handler):
  207
+        for item in self.items:
  208
+            handler.startElement(u"entry", {})
  209
+            handler.addQuickElement(u"title", item['title'])
  210
+            handler.addQuickElement(u"link", item['link'])
  211
+            if item['pubdate'] is not None:
  212
+                handler.addQuickElement(u"updated", rfc2822_date(item['pubdate']).decode('ascii'))
  213
+
  214
+            # Author information.
  215
+            if item['author_name'] is not None:
  216
+                handler.startElement(u"author", {})
  217
+                handler.addQuickElement(u"name", item['author_name'])
  218
+                if item['author_email'] is not None:
  219
+                    handler.addQuickElement(u"email", item['author_email'])
  220
+                handler.endElement(u"author")
  221
+
  222
+            # Unique ID.
  223
+            if item['unique_id'] is not None:
  224
+                unique_id = item['unique_id']
  225
+            else:
  226
+                unique_id = get_tag_uri(item['link'], item['pubdate'])
  227
+            handler.addQuickElement(u"id", unique_id)
  228
+
  229
+            # Summary.
  230
+            if item['description'] is not None:
  231
+                handler.addQuickElement(u"summary", item['description'], {u"type": u"html"})
  232
+
  233
+            # Enclosure.
  234
+            if item['enclosure'] is not None:
  235
+                handler.addQuickElement(u"link", '',
  236
+                    {u"rel": u"enclosure",
  237
+                     u"href": item['enclosure'].url,
  238
+                     u"length": item['enclosure'].length,
  239
+                     u"type": item['enclosure'].mime_type})
  240
+
  241
+            # Categories:
  242
+            for cat in item['categories']:
  243
+                handler.addQuickElement(u"category", u"", {u"term": cat})
  244
+
  245
+            handler.endElement(u"entry")
149 246
 
150 247
 # This isolates the decision of what the system default is, so calling code can
151  
-# do "feedgenerator.DefaultRssFeed" instead of "feedgenerator.Rss201rev2Feed".
152  
-DefaultRssFeed = Rss201rev2Feed
  248
+# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
  249
+DefaultFeed = Rss201rev2Feed
12  django/views/rss/rss.py
... ...
@@ -1,12 +0,0 @@
1  
-from django.core import rss
2  
-from django.core.exceptions import Http404
3  
-from django.utils.httpwrappers import HttpResponse
4  
-
5  
-def feed(request, slug, param=None):
6  
-    try:
7  
-        f = rss.get_registered_feed(slug).get_feed(param)
8  
-    except (rss.FeedIsNotRegistered, rss.FeedDoesNotExist):
9  
-        raise Http404
10  
-    response = HttpResponse(mimetype='application/xml')
11  
-    f.write(response, 'utf-8')
12  
-    return response
548  docs/syndication_feeds.txt
... ...
@@ -0,0 +1,548 @@
  1
+==============================
  2
+The syndication feed framework
  3
+==============================
  4
+
  5
+Django comes with a high-level syndication-feed-generating framework that makes
  6
+creating RSS_ and Atom_ feeds easy.
  7
+
  8
+To create any syndication feed, all you have to do is write a short Python
  9
+class. You can create as many feeds as you want.
  10
+
  11
+Django also comes with a lower-level feed-generating API. Use this if you want
  12
+to generate feeds outside of a Web context, or in some other lower-level way.
  13
+
  14
+.. _RSS: http://www.whatisrss.com/
  15
+.. _Atom: http://www.atomenabled.org/
  16
+
  17
+The high-level framework
  18
+========================
  19
+
  20
+Overview
  21
+--------
  22
+
  23
+The high-level feed-generating framework is a view that's hooked to ``/feeds/``
  24
+by default. Django uses the remainder of the URL (everything after ``/feeds/``)
  25
+to determine which feed to output.
  26
+
  27
+To create a feed, just write a ``Feed`` class and point to it in your URLconf_.
  28
+
  29
+.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/
  30
+
  31
+Initialization
  32
+--------------
  33
+
  34
+To activate syndication feeds on your Django site, add this line to your
  35
+URLconf_::
  36
+
  37
+    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}),
  38
+
  39
+This tells Django to use the RSS framework to handle all URLs starting with
  40
+``"feeds/"``. (You can change that ``"feeds/"`` prefix to fit your own needs.)
  41
+
  42
+This URLconf line has an extra argument: ``{'feed_dict': feeds}``. Use this
  43
+extra argument to pass the syndication framework the feeds that should be
  44
+published under that URL.
  45
+
  46
+Specifically, ``feed_dict`` should be a dictionary that maps a feed's slug
  47
+(short URL label) to its ``Feed`` class.
  48
+
  49
+You can define the ``feed_dict`` in the URLconf itself. Here's a full example
  50
+URLconf::
  51
+
  52
+    from django.conf.urls.defaults import *
  53
+    from myproject.feeds import LatestEntries, LatestEntriesByCategory
  54
+
  55
+    feeds = {
  56
+        'latest': LatestEntries,
  57
+        'categories': LatestEntriesByCategory,
  58
+    }
  59
+
  60
+    urlpatterns = patterns('',
  61
+        # ...
  62
+        (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
  63
+            {'feed_dict': feeds}),
  64
+        # ...
  65
+    )
  66
+
  67
+The above example registers two feeds:
  68
+
  69
+    * The feed represented by ``LatestEntries`` will live at ``feeds/latest/``.
  70
+    * The feed represented by ``LatestEntriesByCategory`` will live at
  71
+      ``feeds/categories/``.
  72
+
  73
+Once that's set up, you just need to define the ``Feed`` classes themselves.
  74
+
  75
+.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/
  76
+.. _settings file: http://www.djangoproject.com/documentation/settings/
  77
+
  78
+Feed classes
  79
+------------
  80
+
  81
+A ``Feed`` class is a simple Python class that represents a syndication feed.
  82
+A feed can be simple (e.g., a "site news" feed, or a basic feed displaying
  83
+the latest entries of a blog) or more complex (e.g., a feed displaying all the
  84
+blog entries in a particular category, where the category is variable).
  85
+
  86
+``Feed`` classes must subclass ``django.contrib.syndication.feeds.Feed``. They
  87
+can live anywhere in your codebase.
  88
+
  89
+A simple example
  90
+----------------
  91
+
  92
+This simple example, taken from chicagocrime.org, describes a feed of the
  93
+latest five news items::
  94
+
  95
+    from django.contrib.syndication.feeds import Feed
  96
+    from django.models.chicagocrime import newsitems
  97
+
  98
+    class SiteNewsFeed(Feed):
  99
+        title = "Chicagocrime.org site news"
  100
+        link = "/sitenews/"
  101
+        description = "Updates on changes and additions to chicagocrime.org."
  102
+
  103
+        def items(self):
  104
+            return newsitems.get_list(order_by=('-pub_date',), limit=5)
  105
+
  106
+Note:
  107
+
  108
+    * The class subclasses ``django.contrib.syndication.feeds.Feed``.
  109
+    * ``title``, ``link`` and ``description`` correspond to the standard
  110
+      RSS ``<title>``, ``<link>`` and ``<description>`` elements, respectively.
  111
+    * ``items()`` is, simply, a method that returns a list of objects that
  112
+      should be included in the feed as ``<item>`` elements. Although this
  113
+      example returns ``NewsItem`` objects using Django's
  114
+      `object-relational mapper`_, ``items()`` doesn't have to return model
  115
+      instances. Although you get a few bits of functionality "for free" by
  116
+      using Django models, ``items()`` can return any type of object you want.
  117
+
  118
+One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``,
  119
+``<link>`` and ``<description>``. We need to tell the framework what data to
  120
+put into those elements.
  121
+
  122
+    * To specify the contents of ``<title>`` and ``<description>``, create
  123
+      `Django templates`_ called ``feeds/sitenews_title`` and
  124
+      ``feeds/sitenews_description``, where ``sitenews`` is the ``slug``
  125
+      specified in the URLconf for the given feed. The RSS system renders that
  126
+      template for each item, passing it two template context variables:
  127
+        * ``{{ obj }}`` -- The current object (one of whichever objects you
  128
+          returned in ``items()``).
  129
+        * ``{{ site }}`` -- A ``django.models.core.sites.Site`` object
  130
+          representing the current site. This is useful for
  131
+          ``{{ site.domain }}`` or ``{{ site.name }}``.
  132
+      If you don't create a template for either the title or description, the
  133
+      framework will use the template ``{{ obj }}`` by default -- that is, the
  134
+      normal string representation of the object.
  135
+    * To specify the contents of ``<link>``, you have two options. For each
  136
+      item in ``items()``, Django first tries executing a
  137
+      ``get_absolute_url()`` method on that object. If that method doesn't
  138
+      exist, it tries calling a method ``item_link()`` in the ``Feed`` class,
  139
+      passing it a single parameter, ``item``, which is the object itself.
  140
+      Both ``get_absolute_url()`` and ``item_link()`` should return the item's
  141
+      URL as a normal Python string.
  142
+
  143
+.. _object-relational mapper: http://www.djangoproject.com/documentation/db_api/
  144
+.. _Django templates: http://www.djangoproject.com/documentation/templates/
  145
+
  146
+A complex example
  147
+-----------------
  148
+
  149
+The framework also supports more complex feeds, via parameters.
  150
+
  151
+For example, chicagocrime.org offers an RSS feed of recent crimes for every
  152
+police beat in Chicago. It'd be silly to create a separate ``Feed`` class for
  153
+each police beat; that would violate the `DRY principle`_ and would couple data
  154
+to programming logic. Instead, the RSS framework lets you make generic feeds
  155
+that output items based on information in the feed's URL.
  156
+
  157
+On chicagocrime.org, the police-beat feeds are accessible via URLs like this:
  158
+
  159
+    * ``/rss/beats/0613/`` -- Returns recent crimes for beat 0613.
  160
+    * ``/rss/beats/1424/`` -- Returns recent crimes for beat 1424.
  161
+
  162
+The slug here is ``beats``. The syndication framework sees the extra URL bits
  163
+after the slug -- ``0613`` and ``1424`` -- and gives you a hook to tell it what
  164
+those URL bits mean, and how they should influence which items get published in
  165
+the feed.
  166
+
  167
+An example makes this clear. Here's the code for these beat-specific feeds::
  168
+
  169
+    class BeatFeed(Feed):
  170
+        def get_object(self, bits):
  171
+            # In case of "/rss/beats/0613/foo/bar/baz/", or other such clutter,
  172
+            # check that bits has only one member.
  173
+            if len(bits) != 1:
  174
+                raise ObjectDoesNotExist
  175
+            return beats.get_object(beat__exact=bits[0])
  176
+
  177
+        def title(self, obj):
  178
+            return "Chicagocrime.org: Crimes for beat %s" % obj.beat
  179
+
  180
+        def link(self, obj):
  181
+            return obj.get_absolute_url()
  182
+
  183
+        def description(self, obj):
  184
+            return "Crimes recently reported in police beat %s" % obj.beat
  185
+
  186
+        def items(self, obj):
  187
+            return crimes.get_list(beat__id__exact=obj.id, order_by=(('-crime_date'),), limit=30)
  188
+
  189
+Here's the basic algorithm the RSS framework follows, given this class and a
  190
+request to the URL ``/rss/beats/0613/``:
  191
+
  192
+    * The framework gets the URL ``/rss/beats/0613/`` and notices there's
  193
+      an extra bit of URL after the slug. It splits that remaining string by
  194
+      the slash character (``"/"``) and calls the ``Feed`` class'
  195
+      ``get_object()`` method, passing it the bits. In this case, bits is
  196
+      ``['0613']``. For a request to ``/rss/beats/0613/foo/bar/``, bits would
  197
+      be ``['0613', 'foo', 'bar']``.
  198
+    * ``get_object()`` is responsible for retrieving the given beat, from the
  199
+      given ``bits``. In this case, it uses the Django database API to retrieve
  200
+      the beat. Note that ``get_object()`` should raise
  201
+      ``django.core.exceptions.ObjectDoesNotExist`` if given invalid
  202
+      parameters. There's no ``try``/``except`` around the
  203
+      ``beats.get_object()`` call, because it's not necessary; that function
  204
+      raises ``BeatDoesNotExist`` on failure, and ``BeatDoesNotExist`` is a
  205
+      subclass of ``ObjectDoesNotExist``. Raising ``ObjectDoesNotExist`` in
  206
+      ``get_object()`` tells Django to produce a 404 error for that request.
  207
+    * To generate the feed's ``<title>``, ``<link>`` and ``<description>``,
  208
+      Django uses the ``title``, ``link`` and ``description`` methods. In the
  209
+      previous example, they were simple string class attributes, but this
  210
+      example illustrates that they can be either strings *or* methods. For
  211
+      each of ``title``, ``link`` and ``description``, Django follows this
  212
+      algorithm:
  213
+        * First, it tries to call a method, passing the ``obj`` argument, where
  214
+          ``obj`` is the object returned by ``get_object()``.
  215
+        * Failing that, it tries to call a method with no arguments.
  216
+        * Failing that, it uses the class attribute.
  217
+    * Finally, note that ``items()`` in this example also takes the ``obj``
  218
+      argument. The algorithm for ``items`` is the same as described in the
  219
+      previous step -- first, it tries ``items(obj)``, then ``items()``, then
  220
+      finally an ``items`` class attribute (which should be a list).
  221
+
  222
+The ``ExampleFeed`` class below gives full documentation on methods and
  223
+attributes of ``Feed`` classes.
  224
+
  225
+.. _DRY principle: http://c2.com/cgi/wiki?DontRepeatYourself
  226
+
  227
+Specifying the type of feed
  228
+---------------------------
  229
+
  230
+By default, feeds produced in this framework use RSS 2.0.
  231
+
  232
+To change that, add a ``feed_type`` attribute to your ``Feed`` class, like so::
  233
+
  234
+    from django.utils.feedgenerator import Atom1Feed
  235
+
  236
+    class MyFeed(Feed):
  237
+        feed_type = Atom1Feed
  238
+
  239
+Note that you set ``feed_type`` to a class object, not an instance.
  240
+
  241
+Currently available feed types are::
  242
+
  243
+    * ``django.utils.feedgenerator.Rss201rev2Feed`` (RSS 2.01. Default.)
  244
+    * ``django.utils.feedgenerator.RssUserland091Feed`` (RSS 0.91.)
  245
+    * ``django.utils.feedgenerator.Atom1Feed`` (Atom 1.0.)
  246
+
  247
+Enclosures
  248
+----------
  249
+
  250
+To specify enclosures, such as those used in creating podcast feeds, use the
  251
+``item_enclosure_url``, ``item_enclosure_length`` and
  252
+``item_enclosure_mime_type`` hooks. See the ``ExampleFeed`` class below for
  253
+usage examples.
  254
+
  255
+Language
  256
+--------
  257
+
  258
+Feeds created by the syndication framework automatically include the
  259
+appropriate ``<language>`` tag (RSS 2.0) or ``xml:lang`` attribute (Atom). This
  260
+comes directly from your `LANGUAGE_CODE setting`_.
  261
+
  262
+.. _LANGUAGE_CODE setting: http://www.djangoproject.com/documentation/settings/#language-code
  263
+
  264
+Publishing Atom and RSS feeds in tandem
  265
+---------------------------------------
  266
+
  267
+Some developers like to make available both Atom *and* RSS versions of their
  268
+feeds. That's easy to do with Django: Just create a subclass of your ``feed``
  269
+class and set the ``feed_type`` to something different. Then update your
  270
+URLconf to add the extra versions.
  271
+
  272
+Here's a full example::
  273
+
  274
+    from django.contrib.syndication.feeds import Feed
  275
+    from django.models.chicagocrime import newsitems
  276
+    from django.utils.feedgenerator import Atom1Feed
  277
+
  278
+    class RssSiteNewsFeed(Feed):
  279
+        title = "Chicagocrime.org site news"
  280
+        link = "/sitenews/"
  281
+        description = "Updates on changes and additions to chicagocrime.org."
  282
+
  283
+        def items(self):
  284
+            return newsitems.get_list(order_by=('-pub_date',), limit=5)
  285
+
  286
+    class AtomSiteNewsFeed(RssSiteNewsFeed):
  287
+        feed_type = Atom1Feed
  288
+
  289
+And the accompanying URLconf::
  290
+
  291
+    from django.conf.urls.defaults import *
  292
+    from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
  293
+
  294
+    feeds = {
  295
+        'rss': RssSiteNewsFeed,
  296
+        'atom': AtomSiteNewsFeed,
  297
+    }
  298
+
  299
+    urlpatterns = patterns('',
  300
+        # ...
  301
+        (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
  302
+            {'feed_dict': feeds}),
  303
+        # ...
  304
+    )
  305
+
  306
+Feed class reference
  307
+-------------------
  308
+
  309
+This example illustrates all possible attributes and methods for a ``Feed`` class::
  310
+
  311
+    class ExampleFeed(rss.Feed):
  312
+
  313
+        # FEED TYPE -- Optional. This should be a class that subclasses
  314
+        # django.utils.feedgenerator.SyndicationFeed. This designates which
  315
+        # type of feed this should be: RSS 2.0, Atom 1.0, etc.
  316
+        # If you don't specify feed_type, your feed will be RSS 2.0.
  317
+        # This should be a class, not an instance of the class.
  318
+
  319
+        feed_type = feedgenerator.Rss201rev2Feed
  320
+
  321
+        # TITLE -- One of the following three is required. The framework looks
  322
+        # for them in this order.
  323
+
  324
+        def title(self, obj):
  325
+            """
  326
+            Takes the object returned by get_object() and returns the feed's
  327
+            title as a normal Python string.
  328
+            """
  329
+
  330
+        def title(self):
  331
+            """
  332
+            Returns the feed's title as a normal Python string.
  333
+            """
  334
+
  335
+        title = 'foo' # Hard-coded title.
  336
+
  337
+        # LINK -- One of the following three is required. The framework looks
  338
+        # for them in this order.
  339
+
  340
+        def link(self, obj):
  341
+            """
  342
+            Takes the object returned by get_object() and returns the feed's
  343
+            link as a normal Python string.
  344
+            """
  345
+
  346
+        def link(self):
  347
+            """
  348
+            Returns the feed's link as a normal Python string.
  349
+            """
  350
+
  351
+        link = '/foo/bar/' # Hard-coded link.
  352
+
  353
+        # DESCRIPTION -- One of the following three is required. The framework
  354
+        # looks for them in this order.
  355
+
  356
+        def description(self, obj):
  357
+            """
  358
+            Takes the object returned by get_object() and returns the feed's
  359
+            description as a normal Python string.
  360
+            """
  361
+
  362
+        def description(self):
  363
+            """
  364
+            Returns the feed's description as a normal Python string.
  365
+            """
  366
+
  367
+        description = 'Foo bar baz.' # Hard-coded description.
  368
+
  369
+        # ITEMS -- One of the following three is required. The framework looks
  370
+        # for them in this order.
  371
+
  372
+        def items(self, obj):
  373
+            """
  374
+            Takes the object returned by get_object() and returns a list of
  375
+            items to publish in this feed.
  376
+            """
  377
+
  378
+        def items(self):
  379
+            """
  380
+            Returns a list of items to publish in this feed.
  381
+            """
  382
+
  383
+        items = ('Item 1', 'Item 2') # Hard-coded items.
  384
+
  385
+        # GET_OBJECT -- This is required for feeds that publish different data
  386
+        # for different URL parameters. (See "A complex example" above.)
  387
+
  388
+        def get_object(self, bits):
  389
+            """
  390
+            Takes a list of strings gleaned from the URL and returns an object
  391
+            represented by this feed. Raises
  392
+            django.core.exceptions.ObjectDoesNotExist on error.
  393
+            """
  394
+
  395
+        # ITEM LINK -- One of these three is required. The framework looks for
  396
+        # them in this order.
  397
+
  398
+        # First, the framework tries the get_absolute_url() method on each item
  399
+        # returned by items(). Failing that, it tries these two methods:
  400
+
  401
+        def item_link(self, item):
  402
+            """
  403
+            Takes an item, as returned by items(), and returns the item's URL.
  404
+            """
  405
+
  406
+        def item_link(self):
  407
+            """
  408
+            Returns the URL for every item in the feed.
  409
+            """
  410
+
  411
+        # ITEM ENCLOSURE URL -- One of these three is required if you're
  412
+        # publishing enclosures. The framework looks for them in this order.
  413
+
  414
+        def item_enclosure_url(self, item):
  415
+            """
  416
+            Takes an item, as returned by items(), and returns the item's
  417
+            enclosure URL.
  418
+            """
  419
+
  420
+        def item_enclosure_url(self):
  421
+            """
  422
+            Returns the enclosure URL for every item in the feed.
  423
+            """
  424
+
  425
+        item_enclosure_url = "/foo/bar.mp3" # Hard-coded enclosure link.
  426
+
  427
+        # ITEM ENCLOSURE LENGTH -- One of these three is required if you're
  428
+        # publishing enclosures. The framework looks for them in this order.
  429
+        # In each case, the returned value should be either an integer, or a
  430
+        # string representation of the integer, in bytes.
  431
+
  432
+        def item_enclosure_length(self, item):
  433
+            """
  434
+            Takes an item, as returned by items(), and returns the item's
  435
+            enclosure length.
  436
+            """
  437
+
  438
+        def item_enclosure_length(self):
  439
+            """
  440
+            Returns the enclosure length for every item in the feed.
  441
+            """
  442
+
  443
+        item_enclosure_length = 32000 # Hard-coded enclosure length.
  444
+
  445
+        # ITEM ENCLOSURE MIME TYPE -- One of these three is required if you're
  446
+        # publishing enclosures. The framework looks for them in this order.
  447
+
  448
+        def item_enclosure_mime_type(self, item):
  449
+            """
  450
+            Takes an item, as returned by items(), and returns the item's
  451
+            enclosure mime type.
  452
+            """
  453
+
  454
+        def item_enclosure_mime_type(self):
  455
+            """
  456
+            Returns the enclosure length, in bytes, for every item in the feed.
  457
+            """
  458
+
  459
+        item_enclosure_mime_type = "audio/mpeg" # Hard-coded enclosure mime-type.
  460
+
  461
+        # ITEM PUBDATE -- It's optional to use one of these three. This is a
  462
+        # hook that specifies how to get the pubdate for a given item.
  463
+        # In each case, the method/attribute should return a Python
  464
+        # datetime.datetime object.
  465
+
  466
+        def item_pubdate(self, item):
  467
+            """
  468
+            Takes an item, as returned by items(), and returns the item's
  469
+            pubdate.
  470
+            """
  471
+
  472
+        def item_pubdate(self):
  473
+            """
  474
+            Returns the pubdate for every item in the feed.
  475
+            """
  476
+
  477
+        item_pubdate = datetime.datetime(2005, 5, 3) # Hard-coded pubdate.
  478
+
  479
+The low-level framework
  480
+=======================
  481
+
  482
+Behind the scenes, the high-level RSS framework uses a lower-level framework
  483
+for generating feeds' XML. This framework lives in a single module:
  484
+`django/utils/feedgenerator.py`_.
  485
+
  486
+Feel free to use this framework on your own, for lower-level tasks.
  487
+
  488
+The ``feedgenerator`` module contains a base class ``SyndicationFeed`` and
  489
+several subclasses:
  490
+
  491
+    * ``RssUserland091Feed``
  492
+    * ``Rss201rev2Feed``
  493
+    * ``Atom1Feed``
  494
+
  495
+Each of these three classes knows how to render a certain type of feed as XML.
  496
+They share this interface::
  497
+
  498
+``__init__(title, link, description, language=None, author_email=None,
  499
+    author_name=None, author_link=None, subtitle=None, categories=None)``
  500
+
  501
+Initializes the feed with the given metadata, which applies to the entire feed
  502
+(i.e., not just to a specific item in the feed).
  503
+
  504
+All parameters, if given, should be Unicode objects, except ``categories``,
  505
+which should be a sequence of Unicode objects.
  506
+
  507
+``add_item(title, link, description, author_email=None, author_name=None,
  508
+    pubdate=None, comments=None, unique_id=None, enclosure=None, categories=())``
  509
+
  510
+Add an item to the feed with the given parameters. All parameters, if given,
  511
+should be Unicode objects, except:
  512
+
  513
+    * ``pubdate`` should be a Python datetime object.
  514
+    * ``enclosure`` should be an instance of ``feedgenerator.Enclosure``.
  515
+    * ``categories`` should be a sequence of Unicode objects.
  516
+
  517
+``write(outfile, encoding)``
  518
+
  519
+Outputs the feed in the given encoding to outfile, which is a file-like object.
  520
+
  521
+``writeString(encoding)``
  522
+
  523
+Returns the feed as a string in the given encoding.