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[^/]+)/$', 'rss.rss.feed'), - (r'^(?P[^/]+)/(?P.+)/$', 'rss.rss.feed'), -) diff --git a/django/contrib/syndication/__init__.py b/django/contrib/syndication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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) - self.endChannelElement(handler) - self.endRssElement(handler) - - def writeRssElement(self, handler): - "Adds the element to handler, taking care of versioning, etc." - raise NotImplementedError - - def endRssElement(self, handler): - "Ends the element." - handler.endElement(u"rss") - - def writeChannelElement(self, handler): + handler.startElement(u"rss", {u"version": self._version}) 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'], {}) + 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) + handler.endElement(u"rss") 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"}) + _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']) - 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") + # 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.*)/$', '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.*)/$', '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 ````, ``<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 + http://www.example.com/ + Sat, 12 Nov 2005 00:28:43 -0000Hot dog today + http://www.example.com/entries/1/tag:www.example.com/entries/1/ + <p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p> + + +.. _django/utils/feedgenerator.py: http://code.djangoproject.com/browser/django/trunk/django/utils/feedgenerator.py