summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrian Holovaty <adrian@holovaty.com>2005-11-12 03:44:53 +0000
committerAdrian Holovaty <adrian@holovaty.com>2005-11-12 03:44:53 +0000
commit944de9e9e638bc239b03f67f459ad4abe4673e48 (patch)
treed144b7c68fc2f0a7169aa07d0a91daa246b94473
parente8ae3567393443934d896579dd6d1dfc92bddb95 (diff)
downloaddjango-944de9e9e638bc239b03f67f459ad4abe4673e48.tar.gz
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
-rw-r--r--django/conf/urls/rss.py6
-rw-r--r--django/contrib/syndication/__init__.py0
-rw-r--r--django/contrib/syndication/feeds.py89
-rw-r--r--django/contrib/syndication/views.py26
-rw-r--r--django/core/rss.py223
-rw-r--r--django/utils/feedgenerator.py221
-rw-r--r--django/views/rss/rss.py12
-rw-r--r--docs/syndication_feeds.txt548
8 files changed, 822 insertions, 303 deletions
diff --git a/django/conf/urls/rss.py b/django/conf/urls/rss.py
deleted file mode 100644
index bb6f84e07d..0000000000
--- a/django/conf/urls/rss.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.conf.urls.defaults import *
-
-urlpatterns = patterns('django.views',
- (r'^(?P<slug>[^/]+)/$', 'rss.rss.feed'),
- (r'^(?P<slug>[^/]+)/(?P<param>.+)/$', 'rss.rss.feed'),
-)
diff --git a/django/contrib/syndication/__init__.py b/django/contrib/syndication/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/syndication/__init__.py
diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py
new file mode 100644
index 0000000000..c495990c51
--- /dev/null
+++ b/django/contrib/syndication/feeds.py
@@ -0,0 +1,89 @@
+from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
+from django.core.template import Context, loader, Template, TemplateDoesNotExist
+from django.models.core import sites
+from django.utils import feedgenerator
+from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE
+
+def add_domain(domain, url):
+ if not url.startswith('http://'):
+ url = u'http://%s%s' % (domain, url)
+ return url
+
+class FeedDoesNotExist(ObjectDoesNotExist):
+ pass
+
+class Feed:
+ item_pubdate = None
+ item_enclosure_url = None
+ feed_type = feedgenerator.DefaultFeed
+
+ def __init__(self, slug):
+ self.slug = slug
+
+ def item_link(self, item):
+ try:
+ return item.get_absolute_url()
+ except AttributeError:
+ raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__
+
+ def __get_dynamic_attr(self, attname, obj):
+ attr = getattr(self, attname)
+ if callable(attr):
+ try:
+ return attr(obj)
+ except TypeError:
+ return attr()
+ return attr
+
+ def get_feed(self, url=None):
+ """
+ Returns a feedgenerator.DefaultFeed object, fully populated, for
+ this feed. Raises FeedDoesNotExist for invalid parameters.
+ """
+ if url:
+ try:
+ obj = self.get_object(url.split('/'))
+ except (AttributeError, ObjectDoesNotExist):
+ raise FeedDoesNotExist
+ else:
+ obj = None
+
+ current_site = sites.get_current()
+ link = self.__get_dynamic_attr('link', obj)
+ link = add_domain(current_site.domain, link)
+
+ feed = self.feed_type(
+ title = self.__get_dynamic_attr('title', obj),
+ link = link,
+ description = self.__get_dynamic_attr('description', obj),
+ language = LANGUAGE_CODE.decode()
+ )
+
+ try:
+ title_template = loader.get_template('feeds/%s_title' % self.slug)
+ except TemplateDoesNotExist:
+ title_template = Template('{{ obj }}')
+ try:
+ description_template = loader.get_template('feeds/%s_description' % self.slug)
+ except TemplateDoesNotExist:
+ description_template = Template('{{ obj }}')
+
+ for item in self.__get_dynamic_attr('items', obj):
+ link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
+ enc = None
+ enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
+ if enc_url:
+ enc = feedgenerator.Enclosure(
+ url = enc_url.decode('utf-8'),
+ length = str(self.__get_dynamic_attr('item_enclosure_length', item)).decode('utf-8'),
+ mime_type = self.__get_dynamic_attr('item_enclosure_mime_type', item).decode('utf-8'),
+ )
+ feed.add_item(
+ title = title_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'),
+ link = link,
+ description = description_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'),
+ unique_id = link,
+ enclosure = enc,
+ pubdate = self.__get_dynamic_attr('item_pubdate', item),
+ )
+ return feed
diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py
new file mode 100644
index 0000000000..3236f9dfab
--- /dev/null
+++ b/django/contrib/syndication/views.py
@@ -0,0 +1,26 @@
+from django.contrib.syndication import feeds
+from django.core.exceptions import Http404
+from django.utils.httpwrappers import HttpResponse
+
+def feed(request, url, feed_dict=None):
+ if not feed_dict:
+ raise Http404, "No feeds are registered."
+
+ try:
+ slug, param = url.split('/', 1)
+ except ValueError:
+ slug, param = url, ''
+
+ try:
+ f = feed_dict[slug]
+ except KeyError:
+ raise Http404, "Slug %r isn't registered." % slug
+
+ try:
+ feedgen = f(slug).get_feed(param)
+ except feeds.FeedDoesNotExist:
+ raise Http404, "Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug
+
+ response = HttpResponse(mimetype='application/xml')
+ feedgen.write(response, 'utf-8')
+ return response
diff --git a/django/core/rss.py b/django/core/rss.py
deleted file mode 100644
index 7ba0c1e30c..0000000000
--- a/django/core/rss.py
+++ /dev/null
@@ -1,223 +0,0 @@
-from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
-from django.core.template import Context, loader, Template, TemplateDoesNotExist
-from django.models.core import sites
-from django.utils import feedgenerator
-from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE
-
-def add_domain(domain, url):
- if not url.startswith('http://'):
- url = u'http://%s%s' % (domain, url)
- return url
-
-class FeedDoesNotExist(ObjectDoesNotExist):
- pass
-
-class Feed:
- item_pubdate = None
- item_enclosure_url = None
-
- def item_link(self, item):
- try:
- return item.get_absolute_url()
- except AttributeError:
- raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your RSS class." % item.__class__.__name__
-
- def __get_dynamic_attr(self, attname, obj):
- attr = getattr(self, attname)
- if callable(attr):
- try:
- return attr(obj)
- except TypeError:
- return attr()
- return attr
-
- def get_feed(self, url=None):
- """
- Returns a feedgenerator.DefaultRssFeed object, fully populated, for
- this feed. Raises FeedDoesNotExist for invalid parameters.
- """
- if url:
- try:
- obj = self.get_object(url.split('/'))
- except (AttributeError, ObjectDoesNotExist):
- raise FeedDoesNotExist
- else:
- obj = None
-
- current_site = sites.get_current()
- link = self.__get_dynamic_attr('link', obj)
- link = add_domain(current_site.domain, link)
-
- feed = feedgenerator.DefaultRssFeed(
- title = self.__get_dynamic_attr('title', obj),
- link = link,
- description = self.__get_dynamic_attr('description', obj),
- language = LANGUAGE_CODE.decode()
- )
-
- try:
- title_template = loader.get_template('rss/%s_title' % self.slug)
- except TemplateDoesNotExist:
- title_template = Template('{{ obj }}')
- try:
- description_template = loader.get_template('rss/%s_description' % self.slug)
- except TemplateDoesNotExist:
- description_template = Template('{{ obj }}')
-
- for item in self.__get_dynamic_attr('items', obj):
- link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
- enc = None
- enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
- if enc_url:
- enc = feedgenerator.Enclosure(
- url = enc_url.decode('utf-8'),
- length = str(self.__get_dynamic_attr('item_enclosure_length', item)).decode('utf-8'),
- mime_type = self.__get_dynamic_attr('item_enclosure_mime_type', item).decode('utf-8'),
- )
- feed.add_item(
- title = title_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'),
- link = link,
- description = description_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'),
- unique_id = link,
- enclosure = enc,
- pubdate = self.__get_dynamic_attr('item_pubdate', item),
- )
- return feed
-
-# DEPRECATED
-class FeedConfiguration:
- def __init__(self, slug, title_cb, link_cb, description_cb, get_list_func_cb, get_list_kwargs,
- param_func=None, param_kwargs_cb=None, get_list_kwargs_cb=None, get_pubdate_cb=None,
- enc_url=None, enc_length=None, enc_mime_type=None):
- """
- slug -- Normal Python string. Used to register the feed.
-
- title_cb, link_cb, description_cb -- Functions that take the param
- (if applicable) and return a normal Python string.
-
- get_list_func_cb -- Function that takes the param and returns a
- function to use in retrieving items.
-
- get_list_kwargs -- Dictionary of kwargs to pass to the function
- returned by get_list_func_cb.
-
- param_func -- Function to use in retrieving the param (if applicable).
-
- param_kwargs_cb -- Function that takes the slug and returns a
- dictionary of kwargs to use in param_func.
-
- get_list_kwargs_cb -- Function that takes the param and returns a
- dictionary to use in addition to get_list_kwargs (if applicable).
-
- get_pubdate_cb -- Function that takes the object and returns a datetime
- to use as the publication date in the feed.
-
- The three enc_* parameters are strings representing methods or
- attributes to call on a particular item to get its enclosure
- information. Each of those methods/attributes should return a normal
- Python string.
- """
- self.slug = slug
- self.title_cb, self.link_cb = title_cb, link_cb
- self.description_cb = description_cb
- self.get_list_func_cb = get_list_func_cb
- self.get_list_kwargs = get_list_kwargs
- self.param_func, self.param_kwargs_cb = param_func, param_kwargs_cb
- self.get_list_kwargs_cb = get_list_kwargs_cb
- self.get_pubdate_cb = get_pubdate_cb
- 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)
- self.enc_url = enc_url
- self.enc_length = enc_length
- self.enc_mime_type = enc_mime_type
-
- def get_feed(self, param_slug=None):
- """
- Returns a utils.feedgenerator.DefaultRssFeed object, fully populated,
- representing this FeedConfiguration.
- """
- if param_slug:
- try:
- param = self.param_func(**self.param_kwargs_cb(param_slug))
- except ObjectDoesNotExist:
- raise FeedIsNotRegistered
- else:
- param = None
- current_site = sites.get_current()
- f = self._get_feed_generator_object(param)
- title_template = loader.get_template('rss/%s_title' % self.slug)
- description_template = loader.get_template('rss/%s_description' % self.slug)
- kwargs = self.get_list_kwargs.copy()
- if param and self.get_list_kwargs_cb:
- kwargs.update(self.get_list_kwargs_cb(param))
- get_list_func = self.get_list_func_cb(param)
- for obj in get_list_func(**kwargs):
- link = obj.get_absolute_url()
- if not link.startswith('http://'):
- link = u'http://%s%s' % (current_site.domain, link)
- enc = None
- if self.enc_url:
- enc_url = getattr(obj, self.enc_url)
- enc_length = getattr(obj, self.enc_length)
- enc_mime_type = getattr(obj, self.enc_mime_type)
- try:
- enc_url = enc_url()
- except TypeError:
- pass
- try:
- enc_length = enc_length()
- except TypeError:
- pass
- try:
- enc_mime_type = enc_mime_type()
- except TypeError:
- pass
- enc = feedgenerator.Enclosure(enc_url.decode('utf-8'),
- (enc_length and str(enc_length).decode('utf-8') or ''), enc_mime_type.decode('utf-8'))
- f.add_item(
- title = title_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'),
- link = link,
- description = description_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'),
- unique_id=link,
- enclosure=enc,
- pubdate = self.get_pubdate_cb and self.get_pubdate_cb(obj) or None,
- )
- return f
-
- def _get_feed_generator_object(self, param):
- current_site = sites.get_current()
- link = self.link_cb(param).decode()
- if not link.startswith('http://'):
- link = u'http://%s%s' % (current_site.domain, link)
- return feedgenerator.DefaultRssFeed(
- title = self.title_cb(param).decode(),
- link = link,
- description = self.description_cb(param).decode(),
- language = LANGUAGE_CODE.decode(),
- )
-
-
-# global dict used by register_feed and get_registered_feed
-_registered_feeds = {}
-
-# DEPRECATED
-class FeedIsNotRegistered(Exception):
- pass
-
-# DEPRECATED
-def register_feed(feed):
- _registered_feeds[feed.slug] = feed
-
-def register_feeds(*feeds):
- for f in feeds:
- _registered_feeds[f.slug] = f
-
-def get_registered_feed(slug):
- # try to load a RSS settings module so that feeds can be registered
- try:
- __import__(SETTINGS_MODULE + '_rss', '', '', [''])
- except (KeyError, ImportError, ValueError):
- pass
- try:
- return _registered_feeds[slug]
- except KeyError:
- raise FeedIsNotRegistered
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
index e8e58718ff..d482685383 100644
--- a/django/utils/feedgenerator.py
+++ b/django/utils/feedgenerator.py
@@ -19,21 +19,42 @@ http://diveintomark.org/archives/2004/02/04/incompatible-rss
"""
from django.utils.xmlutils import SimplerXMLGenerator
+import datetime, re, time
+import email.Utils
+from xml.dom import minidom
+from xml.parsers.expat import ExpatError
+
+def rfc2822_date(date):
+ return email.Utils.formatdate(time.mktime(date.timetuple()))
+
+def get_tag_uri(url, date):
+ "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
+ tag = re.sub('^http://', '', url)
+ if date is not None:
+ tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
+ tag = re.sub('#', '/', tag)
+ return 'tag:' + tag
class SyndicationFeed:
"Base class for all syndication feeds. Subclasses should provide write()"
- def __init__(self, title, link, description, language=None):
- self.feed_info = {
+ def __init__(self, title, link, description, language=None, author_email=None,
+ author_name=None, author_link=None, subtitle=None, categories=None):
+ self.feed = {
'title': title,
'link': link,
'description': description,
'language': language,
+ 'author_email': author_email,
+ 'author_name': author_name,
+ 'author_link': author_link,
+ 'subtitle': subtitle,
+ 'categories': categories or (),
}
self.items = []
def add_item(self, title, link, description, author_email=None,
- author_name=None, pubdate=None, comments=None, unique_id=None,
- enclosure=None, categories=None):
+ author_name=None, pubdate=None, comments=None,
+ unique_id=None, enclosure=None, categories=()):
"""
Adds an item to the feed. All args are expected to be Python Unicode
objects except pubdate, which is a datetime.datetime object, and
@@ -49,7 +70,7 @@ class SyndicationFeed:
'comments': comments,
'unique_id': unique_id,
'enclosure': enclosure,
- 'categories': categories or [],
+ 'categories': categories or (),
})
def num_items(self):
@@ -71,6 +92,18 @@ class SyndicationFeed:
self.write(s, encoding)
return s.getvalue()
+ def latest_post_date(self):
+ """
+ Returns the latest item's pubdate. If none of them have a pubdate,
+ this returns the current date/time.
+ """
+ updates = [i['pubdate'] for i in self.items if i['pubdate'] is not None]
+ if len(updates) > 0:
+ updates.sort()
+ return updates[-1]
+ else:
+ return datetime.datetime.now()
+
class Enclosure:
"Represents an RSS enclosure"
def __init__(self, url, length, mime_type):
@@ -81,72 +114,136 @@ class RssFeed(SyndicationFeed):
def write(self, outfile, encoding):
handler = SimplerXMLGenerator(outfile, encoding)
handler.startDocument()
- self.writeRssElement(handler)
- self.writeChannelElement(handler)
- for item in self.items:
- self.writeRssItem(handler, item)
+ handler.startElement(u"rss", {u"version": self._version})
+ handler.startElement(u"channel", {})
+ handler.addQuickElement(u"title", self.feed['title'])
+ handler.addQuickElement(u"link", self.feed['link'])
+ handler.addQuickElement(u"description", self.feed['description'])
+ if self.feed['language'] is not None:
+ handler.addQuickElement(u"language", self.feed['language'])
+ self.write_items(handler)
self.endChannelElement(handler)
- self.endRssElement(handler)
-
- def writeRssElement(self, handler):
- "Adds the <rss> element to handler, taking care of versioning, etc."
- raise NotImplementedError
-
- def endRssElement(self, handler):
- "Ends the <rss> element."
handler.endElement(u"rss")
- def writeChannelElement(self, handler):
- handler.startElement(u"channel", {})
- handler.addQuickElement(u"title", self.feed_info['title'], {})
- handler.addQuickElement(u"link", self.feed_info['link'], {})
- handler.addQuickElement(u"description", self.feed_info['description'], {})
- if self.feed_info['language'] is not None:
- handler.addQuickElement(u"language", self.feed_info['language'], {})
-
def endChannelElement(self, handler):
handler.endElement(u"channel")
class RssUserland091Feed(RssFeed):
- def writeRssElement(self, handler):
- handler.startElement(u"rss", {u"version": u"0.91"})
-
- def writeRssItem(self, handler, item):
- handler.startElement(u"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")
+ _version = u"0.91"
+ def write_items(self, handler):
+ for item in self.items:
+ handler.startElement(u"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
- def writeRssElement(self, handler):
- handler.startElement(u"rss", {u"version": u"2.0"})
-
- def writeRssItem(self, handler, item):
- handler.startElement(u"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'], {})
- if item['author_email'] is not None and item['author_name'] is not None:
- handler.addQuickElement(u"author", u"%s (%s)" % \
- (item['author_email'], item['author_name']), {})
- if item['pubdate'] is not None:
- handler.addQuickElement(u"pubDate", item['pubdate'].strftime('%a, %d %b %Y %H:%M:%S %Z'), {})
- if item['comments'] is not None:
- handler.addQuickElement(u"comments", item['comments'], {})
- if item['unique_id'] is not None:
- handler.addQuickElement(u"guid", item['unique_id'], {})
- if item['enclosure'] is not None:
- handler.addQuickElement(u"enclosure", '',
- {u"url": item['enclosure'].url, u"length": item['enclosure'].length,
- u"type": item['enclosure'].mime_type})
- for cat in item['categories']:
- handler.addQuickElement(u"category", cat, {})
- handler.endElement(u"item")
+ _version = u"2.0"
+ def write_items(self, handler):
+ for item in self.items:
+ handler.startElement(u"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'])
+
+ # Author information.
+ if item['author_email'] is not None and item['author_name'] is not None:
+ handler.addQuickElement(u"author", u"%s (%s)" % \
+ (item['author_email'], item['author_name']))
+
+ if item['pubdate'] is not None:
+ handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('ascii'))
+ if item['comments'] is not None:
+ handler.addQuickElement(u"comments", item['comments'])
+ if item['unique_id'] is not None:
+ handler.addQuickElement(u"guid", item['unique_id'])
+
+ # Enclosure.
+ if item['enclosure'] is not None:
+ handler.addQuickElement(u"enclosure", '',
+ {u"url": item['enclosure'].url, u"length": item['enclosure'].length,
+ u"type": item['enclosure'].mime_type})
+
+ # Categories.
+ 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
+ ns = u"http://www.w3.org/2005/Atom"
+ def write(self, outfile, encoding):
+ handler = SimplerXMLGenerator(outfile, encoding)
+ handler.startDocument()
+ if self.feed['language'] is not None:
+ handler.startElement(u"feed", {u"xmlns": self.ns, u"xml:lang": self.feed['language']})
+ else:
+ handler.startElement(u"feed", {u"xmlns": self.ns})
+ handler.addQuickElement(u"title", self.feed['title'])
+ handler.addQuickElement(u"link", "", {u"href": self.feed['link']})
+ handler.addQuickElement(u"id", self.feed['link'])
+ handler.addQuickElement(u"updated", rfc2822_date(self.latest_post_date()).decode('ascii'))
+ if self.feed['author_name'] is not None:
+ handler.startElement(u"author", {})
+ handler.addQuickElement(u"name", self.feed['author_name'])
+ if self.feed['author_email'] is not None:
+ handler.addQuickElement(u"email", self.feed['author_email'])
+ if self.feed['author_link'] is not None:
+ handler.addQuickElement(u"uri", self.feed['author_link'])
+ handler.endElement(u"author")
+ if self.feed['subtitle'] is not None:
+ handler.addQuickElement(u"subtitle", self.feed['subtitle'])
+ for cat in self.feed['categories']:
+ handler.addQuickElement(u"category", "", {u"term": cat})
+ self.write_items(handler)
+ handler.endElement(u"feed")
+
+ def write_items(self, handler):
+ for item in self.items:
+ handler.startElement(u"entry", {})
+ handler.addQuickElement(u"title", item['title'])
+ handler.addQuickElement(u"link", item['link'])
+ if item['pubdate'] is not None:
+ handler.addQuickElement(u"updated", rfc2822_date(item['pubdate']).decode('ascii'))
+
+ # Author information.
+ if item['author_name'] is not None:
+ handler.startElement(u"author", {})
+ handler.addQuickElement(u"name", item['author_name'])
+ if item['author_email'] is not None:
+ handler.addQuickElement(u"email", item['author_email'])
+ handler.endElement(u"author")
+
+ # Unique ID.
+ if item['unique_id'] is not None:
+ unique_id = item['unique_id']
+ else:
+ unique_id = get_tag_uri(item['link'], item['pubdate'])
+ handler.addQuickElement(u"id", unique_id)
+
+ # Summary.
+ if item['description'] is not None:
+ handler.addQuickElement(u"summary", item['description'], {u"type": u"html"})
+
+ # Enclosure.
+ if item['enclosure'] is not None:
+ handler.addQuickElement(u"link", '',
+ {u"rel": u"enclosure",
+ u"href": item['enclosure'].url,
+ u"length": item['enclosure'].length,
+ u"type": item['enclosure'].mime_type})
+
+ # Categories:
+ for cat in item['categories']:
+ handler.addQuickElement(u"category", u"", {u"term": cat})
+
+ handler.endElement(u"entry")
# This isolates the decision of what the system default is, so calling code can
-# do "feedgenerator.DefaultRssFeed" instead of "feedgenerator.Rss201rev2Feed".
-DefaultRssFeed = Rss201rev2Feed
+# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
+DefaultFeed = Rss201rev2Feed
diff --git a/django/views/rss/rss.py b/django/views/rss/rss.py
deleted file mode 100644
index 97c2f22dc4..0000000000
--- a/django/views/rss/rss.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from django.core import rss
-from django.core.exceptions import Http404
-from django.utils.httpwrappers import HttpResponse
-
-def feed(request, slug, param=None):
- try:
- f = rss.get_registered_feed(slug).get_feed(param)
- except (rss.FeedIsNotRegistered, rss.FeedDoesNotExist):
- raise Http404
- response = HttpResponse(mimetype='application/xml')
- f.write(response, 'utf-8')
- return response
diff --git a/docs/syndication_feeds.txt b/docs/syndication_feeds.txt
new file mode 100644
index 0000000000..6368817d19
--- /dev/null
+++ b/docs/syndication_feeds.txt
@@ -0,0 +1,548 @@
+==============================
+The syndication feed framework
+==============================
+
+Django comes with a high-level syndication-feed-generating framework that makes
+creating RSS_ and Atom_ feeds easy.
+
+To create any syndication feed, all you have to do is write a short Python
+class. You can create as many feeds as you want.
+
+Django also comes with a lower-level feed-generating API. Use this if you want
+to generate feeds outside of a Web context, or in some other lower-level way.
+
+.. _RSS: http://www.whatisrss.com/
+.. _Atom: http://www.atomenabled.org/
+
+The high-level framework
+========================
+
+Overview
+--------
+
+The high-level feed-generating framework is a view that's hooked to ``/feeds/``
+by default. Django uses the remainder of the URL (everything after ``/feeds/``)
+to determine which feed to output.
+
+To create a feed, just write a ``Feed`` class and point to it in your URLconf_.
+
+.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/
+
+Initialization
+--------------
+
+To activate syndication feeds on your Django site, add this line to your
+URLconf_::
+
+ (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}),
+
+This tells Django to use the RSS framework to handle all URLs starting with
+``"feeds/"``. (You can change that ``"feeds/"`` prefix to fit your own needs.)
+
+This URLconf line has an extra argument: ``{'feed_dict': feeds}``. Use this
+extra argument to pass the syndication framework the feeds that should be
+published under that URL.
+
+Specifically, ``feed_dict`` should be a dictionary that maps a feed's slug
+(short URL label) to its ``Feed`` class.
+
+You can define the ``feed_dict`` in the URLconf itself. Here's a full example
+URLconf::
+
+ from django.conf.urls.defaults import *
+ from myproject.feeds import LatestEntries, LatestEntriesByCategory
+
+ feeds = {
+ 'latest': LatestEntries,
+ 'categories': LatestEntriesByCategory,
+ }
+
+ urlpatterns = patterns('',
+ # ...
+ (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
+ {'feed_dict': feeds}),
+ # ...
+ )
+
+The above example registers two feeds:
+
+ * The feed represented by ``LatestEntries`` will live at ``feeds/latest/``.
+ * The feed represented by ``LatestEntriesByCategory`` will live at
+ ``feeds/categories/``.
+
+Once that's set up, you just need to define the ``Feed`` classes themselves.
+
+.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/
+.. _settings file: http://www.djangoproject.com/documentation/settings/
+
+Feed classes
+------------
+
+A ``Feed`` class is a simple Python class that represents a syndication feed.
+A feed can be simple (e.g., a "site news" feed, or a basic feed displaying
+the latest entries of a blog) or more complex (e.g., a feed displaying all the
+blog entries in a particular category, where the category is variable).
+
+``Feed`` classes must subclass ``django.contrib.syndication.feeds.Feed``. They
+can live anywhere in your codebase.
+
+A simple example
+----------------
+
+This simple example, taken from chicagocrime.org, describes a feed of the
+latest five news items::
+
+ from django.contrib.syndication.feeds import Feed
+ from django.models.chicagocrime import newsitems
+
+ class SiteNewsFeed(Feed):
+ title = "Chicagocrime.org site news"
+ link = "/sitenews/"
+ description = "Updates on changes and additions to chicagocrime.org."
+
+ def items(self):
+ return newsitems.get_list(order_by=('-pub_date',), limit=5)
+
+Note:
+
+ * The class subclasses ``django.contrib.syndication.feeds.Feed``.
+ * ``title``, ``link`` and ``description`` correspond to the standard
+ RSS ``<title>``, ``<link>`` and ``<description>`` elements, respectively.
+ * ``items()`` is, simply, a method that returns a list of objects that
+ should be included in the feed as ``<item>`` elements. Although this
+ example returns ``NewsItem`` objects using Django's
+ `object-relational mapper`_, ``items()`` doesn't have to return model
+ instances. Although you get a few bits of functionality "for free" by
+ using Django models, ``items()`` can return any type of object you want.
+
+One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``,
+``<link>`` and ``<description>``. We need to tell the framework what data to
+put into those elements.
+
+ * To specify the contents of ``<title>`` and ``<description>``, create
+ `Django templates`_ called ``feeds/sitenews_title`` and
+ ``feeds/sitenews_description``, where ``sitenews`` is the ``slug``
+ specified in the URLconf for the given feed. The RSS system renders that
+ template for each item, passing it two template context variables:
+ * ``{{ obj }}`` -- The current object (one of whichever objects you
+ returned in ``items()``).
+ * ``{{ site }}`` -- A ``django.models.core.sites.Site`` object
+ representing the current site. This is useful for
+ ``{{ site.domain }}`` or ``{{ site.name }}``.
+ If you don't create a template for either the title or description, the
+ framework will use the template ``{{ obj }}`` by default -- that is, the
+ normal string representation of the object.
+ * To specify the contents of ``<link>``, you have two options. For each
+ item in ``items()``, Django first tries executing a
+ ``get_absolute_url()`` method on that object. If that method doesn't
+ exist, it tries calling a method ``item_link()`` in the ``Feed`` class,
+ passing it a single parameter, ``item``, which is the object itself.
+ Both ``get_absolute_url()`` and ``item_link()`` should return the item's
+ URL as a normal Python string.
+
+.. _object-relational mapper: http://www.djangoproject.com/documentation/db_api/
+.. _Django templates: http://www.djangoproject.com/documentation/templates/
+
+A complex example
+-----------------
+
+The framework also supports more complex feeds, via parameters.
+
+For example, chicagocrime.org offers an RSS feed of recent crimes for every
+police beat in Chicago. It'd be silly to create a separate ``Feed`` class for
+each police beat; that would violate the `DRY principle`_ and would couple data
+to programming logic. Instead, the RSS framework lets you make generic feeds
+that output items based on information in the feed's URL.
+
+On chicagocrime.org, the police-beat feeds are accessible via URLs like this:
+
+ * ``/rss/beats/0613/`` -- Returns recent crimes for beat 0613.
+ * ``/rss/beats/1424/`` -- Returns recent crimes for beat 1424.
+
+The slug here is ``beats``. The syndication framework sees the extra URL bits
+after the slug -- ``0613`` and ``1424`` -- and gives you a hook to tell it what
+those URL bits mean, and how they should influence which items get published in
+the feed.
+
+An example makes this clear. Here's the code for these beat-specific feeds::
+
+ class BeatFeed(Feed):
+ def get_object(self, bits):
+ # In case of "/rss/beats/0613/foo/bar/baz/", or other such clutter,
+ # check that bits has only one member.
+ if len(bits) != 1:
+ raise ObjectDoesNotExist
+ return beats.get_object(beat__exact=bits[0])
+
+ def title(self, obj):
+ return "Chicagocrime.org: Crimes for beat %s" % obj.beat
+
+ def link(self, obj):
+ return obj.get_absolute_url()
+
+ def description(self, obj):
+ return "Crimes recently reported in police beat %s" % obj.beat
+
+ def items(self, obj):
+ return crimes.get_list(beat__id__exact=obj.id, order_by=(('-crime_date'),), limit=30)
+
+Here's the basic algorithm the RSS framework follows, given this class and a
+request to the URL ``/rss/beats/0613/``:
+
+ * The framework gets the URL ``/rss/beats/0613/`` and notices there's
+ an extra bit of URL after the slug. It splits that remaining string by
+ the slash character (``"/"``) and calls the ``Feed`` class'
+ ``get_object()`` method, passing it the bits. In this case, bits is
+ ``['0613']``. For a request to ``/rss/beats/0613/foo/bar/``, bits would
+ be ``['0613', 'foo', 'bar']``.
+ * ``get_object()`` is responsible for retrieving the given beat, from the
+ given ``bits``. In this case, it uses the Django database API to retrieve
+ the beat. Note that ``get_object()`` should raise
+ ``django.core.exceptions.ObjectDoesNotExist`` if given invalid
+ parameters. There's no ``try``/``except`` around the
+ ``beats.get_object()`` call, because it's not necessary; that function
+ raises ``BeatDoesNotExist`` on failure, and ``BeatDoesNotExist`` is a
+ subclass of ``ObjectDoesNotExist``. Raising ``ObjectDoesNotExist`` in
+ ``get_object()`` tells Django to produce a 404 error for that request.
+ * To generate the feed's ``<title>``, ``<link>`` and ``<description>``,
+ Django uses the ``title``, ``link`` and ``description`` methods. In the
+ previous example, they were simple string class attributes, but this
+ example illustrates that they can be either strings *or* methods. For
+ each of ``title``, ``link`` and ``description``, Django follows this
+ algorithm:
+ * First, it tries to call a method, passing the ``obj`` argument, where
+ ``obj`` is the object returned by ``get_object()``.
+ * Failing that, it tries to call a method with no arguments.
+ * Failing that, it uses the class attribute.
+ * Finally, note that ``items()`` in this example also takes the ``obj``
+ argument. The algorithm for ``items`` is the same as described in the
+ previous step -- first, it tries ``items(obj)``, then ``items()``, then
+ finally an ``items`` class attribute (which should be a list).
+
+The ``ExampleFeed`` class below gives full documentation on methods and
+attributes of ``Feed`` classes.
+
+.. _DRY principle: http://c2.com/cgi/wiki?DontRepeatYourself
+
+Specifying the type of feed
+---------------------------
+
+By default, feeds produced in this framework use RSS 2.0.
+
+To change that, add a ``feed_type`` attribute to your ``Feed`` class, like so::
+
+ from django.utils.feedgenerator import Atom1Feed
+
+ class MyFeed(Feed):
+ feed_type = Atom1Feed
+
+Note that you set ``feed_type`` to a class object, not an instance.
+
+Currently available feed types are::
+
+ * ``django.utils.feedgenerator.Rss201rev2Feed`` (RSS 2.01. Default.)
+ * ``django.utils.feedgenerator.RssUserland091Feed`` (RSS 0.91.)
+ * ``django.utils.feedgenerator.Atom1Feed`` (Atom 1.0.)
+
+Enclosures
+----------
+
+To specify enclosures, such as those used in creating podcast feeds, use the
+``item_enclosure_url``, ``item_enclosure_length`` and
+``item_enclosure_mime_type`` hooks. See the ``ExampleFeed`` class below for
+usage examples.
+
+Language
+--------
+
+Feeds created by the syndication framework automatically include the
+appropriate ``<language>`` tag (RSS 2.0) or ``xml:lang`` attribute (Atom). This
+comes directly from your `LANGUAGE_CODE setting`_.
+
+.. _LANGUAGE_CODE setting: http://www.djangoproject.com/documentation/settings/#language-code
+
+Publishing Atom and RSS feeds in tandem
+---------------------------------------
+
+Some developers like to make available both Atom *and* RSS versions of their
+feeds. That's easy to do with Django: Just create a subclass of your ``feed``
+class and set the ``feed_type`` to something different. Then update your
+URLconf to add the extra versions.
+
+Here's a full example::
+
+ from django.contrib.syndication.feeds import Feed
+ from django.models.chicagocrime import newsitems
+ from django.utils.feedgenerator import Atom1Feed
+
+ class RssSiteNewsFeed(Feed):
+ title = "Chicagocrime.org site news"
+ link = "/sitenews/"
+ description = "Updates on changes and additions to chicagocrime.org."
+
+ def items(self):
+ return newsitems.get_list(order_by=('-pub_date',), limit=5)
+
+ class AtomSiteNewsFeed(RssSiteNewsFeed):
+ feed_type = Atom1Feed
+
+And the accompanying URLconf::
+
+ from django.conf.urls.defaults import *
+ from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
+
+ feeds = {
+ 'rss': RssSiteNewsFeed,
+ 'atom': AtomSiteNewsFeed,
+ }
+
+ urlpatterns = patterns('',
+ # ...
+ (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
+ {'feed_dict': feeds}),
+ # ...
+ )
+
+Feed class reference
+-------------------
+
+This example illustrates all possible attributes and methods for a ``Feed`` class::
+
+ class ExampleFeed(rss.Feed):
+
+ # FEED TYPE -- Optional. This should be a class that subclasses
+ # django.utils.feedgenerator.SyndicationFeed. This designates which
+ # type of feed this should be: RSS 2.0, Atom 1.0, etc.
+ # If you don't specify feed_type, your feed will be RSS 2.0.
+ # This should be a class, not an instance of the class.
+
+ feed_type = feedgenerator.Rss201rev2Feed
+
+ # TITLE -- One of the following three is required. The framework looks
+ # for them in this order.
+
+ def title(self, obj):
+ """
+ Takes the object returned by get_object() and returns the feed's
+ title as a normal Python string.
+ """
+
+ def title(self):
+ """
+ Returns the feed's title as a normal Python string.
+ """
+
+ title = 'foo' # Hard-coded title.
+
+ # LINK -- One of the following three is required. The framework looks
+ # for them in this order.
+
+ def link(self, obj):
+ """
+ Takes the object returned by get_object() and returns the feed's
+ link as a normal Python string.
+ """
+
+ def link(self):
+ """
+ Returns the feed's link as a normal Python string.
+ """
+
+ link = '/foo/bar/' # Hard-coded link.
+
+ # DESCRIPTION -- One of the following three is required. The framework
+ # looks for them in this order.
+
+ def description(self, obj):
+ """
+ Takes the object returned by get_object() and returns the feed's
+ description as a normal Python string.
+ """
+
+ def description(self):
+ """
+ Returns the feed's description as a normal Python string.
+ """
+
+ description = 'Foo bar baz.' # Hard-coded description.
+
+ # ITEMS -- One of the following three is required. The framework looks
+ # for them in this order.
+
+ def items(self, obj):
+ """
+ Takes the object returned by get_object() and returns a list of
+ items to publish in this feed.
+ """
+
+ def items(self):
+ """
+ Returns a list of items to publish in this feed.
+ """
+
+ items = ('Item 1', 'Item 2') # Hard-coded items.
+
+ # GET_OBJECT -- This is required for feeds that publish different data
+ # for different URL parameters. (See "A complex example" above.)
+
+ def get_object(self, bits):
+ """
+ Takes a list of strings gleaned from the URL and returns an object
+ represented by this feed. Raises
+ django.core.exceptions.ObjectDoesNotExist on error.
+ """
+
+ # ITEM LINK -- One of these three is required. The framework looks for
+ # them in this order.
+
+ # First, the framework tries the get_absolute_url() method on each item
+ # returned by items(). Failing that, it tries these two methods:
+
+ def item_link(self, item):
+ """
+ Takes an item, as returned by items(), and returns the item's URL.
+ """
+
+ def item_link(self):
+ """
+ Returns the URL for every item in the feed.
+ """
+
+ # ITEM ENCLOSURE URL -- One of these three is required if you're
+ # publishing enclosures. The framework looks for them in this order.
+
+ def item_enclosure_url(self, item):
+ """
+ Takes an item, as returned by items(), and returns the item's
+ enclosure URL.
+ """
+
+ def item_enclosure_url(self):
+ """
+ Returns the enclosure URL for every item in the feed.
+ """
+
+ item_enclosure_url = "/foo/bar.mp3" # Hard-coded enclosure link.
+
+ # ITEM ENCLOSURE LENGTH -- One of these three is required if you're
+ # publishing enclosures. The framework looks for them in this order.
+ # In each case, the returned value should be either an integer, or a
+ # string representation of the integer, in bytes.
+
+ def item_enclosure_length(self, item):
+ """
+ Takes an item, as returned by items(), and returns the item's
+ enclosure length.
+ """
+
+ def item_enclosure_length(self):
+ """
+ Returns the enclosure length for every item in the feed.
+ """
+
+ item_enclosure_length = 32000 # Hard-coded enclosure length.
+
+ # ITEM ENCLOSURE MIME TYPE -- One of these three is required if you're
+ # publishing enclosures. The framework looks for them in this order.
+
+ def item_enclosure_mime_type(self, item):
+ """
+ Takes an item, as returned by items(), and returns the item's
+ enclosure mime type.
+ """
+
+ def item_enclosure_mime_type(self):
+ """
+ Returns the enclosure length, in bytes, for every item in the feed.
+ """
+
+ item_enclosure_mime_type = "audio/mpeg" # Hard-coded enclosure mime-type.
+
+ # ITEM PUBDATE -- It's optional to use one of these three. This is a
+ # hook that specifies how to get the pubdate for a given item.
+ # In each case, the method/attribute should return a Python
+ # datetime.datetime object.
+
+ def item_pubdate(self, item):
+ """
+ Takes an item, as returned by items(), and returns the item's
+ pubdate.
+ """
+
+ def item_pubdate(self):
+ """
+ Returns the pubdate for every item in the feed.
+ """
+
+ item_pubdate = datetime.datetime(2005, 5, 3) # Hard-coded pubdate.
+
+The low-level framework
+=======================
+
+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.
+
+The ``feedgenerator`` module contains a base class ``SyndicationFeed`` and
+several subclasses:
+
+ * ``RssUserland091Feed``
+ * ``Rss201rev2Feed``
+ * ``Atom1Feed``
+
+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)``
+
+Initializes the feed with the given metadata, which applies to the entire feed
+(i.e., not just to a specific item in the feed).
+
+All parameters, if given, should be Unicode objects, except ``categories``,
+which should be a sequence of Unicode objects.
+
+``add_item(title, link, description, author_email=None, author_name=None,
+ pubdate=None, comments=None, unique_id=None, enclosure=None, categories=())``
+
+Add an item to the feed with the given parameters. 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)``
+
+Outputs the feed in the given encoding to outfile, which is a file-like object.
+
+``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::
+
+ >>> from django.utils import feedgenerator
+ >>> f = feedgenerator.Atom1Feed(
+ ... title=u"My Weblog",
+ ... link=u"http://www.example.com/",
+ ... description=u"In which I write about what I ate today.",
+ ... language=u"en"),
+ >>> f.add_item(title=u"Hot dog today",
+ ... link=u"http://www.example.com/entries/1/",
+ ... description=u"<p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p>")
+ >>> print f.writeString('utf8')
+ <?xml version="1.0" encoding="utf8"?>
+ <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><title>My Weblog</title>
+ <link href="http://www.example.com/"></link><id>http://www.example.com/</id>
+ <updated>Sat, 12 Nov 2005 00:28:43 -0000</updated><entry><title>Hot dog today</title>
+ <link>http://www.example.com/entries/1/</link><id>tag:www.example.com/entries/1/</id>
+ <summary type="html">&lt;p&gt;Today I had a Vienna Beef hot dog. It was pink, plump and perfect.&lt;/p&gt;</summary>
+ </entry></feed>
+
+.. _django/utils/feedgenerator.py: http://code.djangoproject.com/browser/django/trunk/django/utils/feedgenerator.py