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
This commit is contained in:
parent
e8ae356739
commit
944de9e9e6
|
@ -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'),
|
|
||||||
)
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
|
@ -19,21 +19,42 @@ http://diveintomark.org/archives/2004/02/04/incompatible-rss
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.utils.xmlutils import SimplerXMLGenerator
|
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:
|
class SyndicationFeed:
|
||||||
"Base class for all syndication feeds. Subclasses should provide write()"
|
"Base class for all syndication feeds. Subclasses should provide write()"
|
||||||
def __init__(self, title, link, description, language=None):
|
def __init__(self, title, link, description, language=None, author_email=None,
|
||||||
self.feed_info = {
|
author_name=None, author_link=None, subtitle=None, categories=None):
|
||||||
|
self.feed = {
|
||||||
'title': title,
|
'title': title,
|
||||||
'link': link,
|
'link': link,
|
||||||
'description': description,
|
'description': description,
|
||||||
'language': language,
|
'language': language,
|
||||||
|
'author_email': author_email,
|
||||||
|
'author_name': author_name,
|
||||||
|
'author_link': author_link,
|
||||||
|
'subtitle': subtitle,
|
||||||
|
'categories': categories or (),
|
||||||
}
|
}
|
||||||
self.items = []
|
self.items = []
|
||||||
|
|
||||||
def add_item(self, title, link, description, author_email=None,
|
def add_item(self, title, link, description, author_email=None,
|
||||||
author_name=None, pubdate=None, comments=None, unique_id=None,
|
author_name=None, pubdate=None, comments=None,
|
||||||
enclosure=None, categories=None):
|
unique_id=None, enclosure=None, categories=()):
|
||||||
"""
|
"""
|
||||||
Adds an item to the feed. All args are expected to be Python Unicode
|
Adds an item to the feed. All args are expected to be Python Unicode
|
||||||
objects except pubdate, which is a datetime.datetime object, and
|
objects except pubdate, which is a datetime.datetime object, and
|
||||||
|
@ -49,7 +70,7 @@ class SyndicationFeed:
|
||||||
'comments': comments,
|
'comments': comments,
|
||||||
'unique_id': unique_id,
|
'unique_id': unique_id,
|
||||||
'enclosure': enclosure,
|
'enclosure': enclosure,
|
||||||
'categories': categories or [],
|
'categories': categories or (),
|
||||||
})
|
})
|
||||||
|
|
||||||
def num_items(self):
|
def num_items(self):
|
||||||
|
@ -71,6 +92,18 @@ class SyndicationFeed:
|
||||||
self.write(s, encoding)
|
self.write(s, encoding)
|
||||||
return s.getvalue()
|
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:
|
class Enclosure:
|
||||||
"Represents an RSS enclosure"
|
"Represents an RSS enclosure"
|
||||||
def __init__(self, url, length, mime_type):
|
def __init__(self, url, length, mime_type):
|
||||||
|
@ -81,72 +114,136 @@ class RssFeed(SyndicationFeed):
|
||||||
def write(self, outfile, encoding):
|
def write(self, outfile, encoding):
|
||||||
handler = SimplerXMLGenerator(outfile, encoding)
|
handler = SimplerXMLGenerator(outfile, encoding)
|
||||||
handler.startDocument()
|
handler.startDocument()
|
||||||
self.writeRssElement(handler)
|
handler.startElement(u"rss", {u"version": self._version})
|
||||||
self.writeChannelElement(handler)
|
|
||||||
for item in self.items:
|
|
||||||
self.writeRssItem(handler, item)
|
|
||||||
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.startElement(u"channel", {})
|
||||||
handler.addQuickElement(u"title", self.feed_info['title'], {})
|
handler.addQuickElement(u"title", self.feed['title'])
|
||||||
handler.addQuickElement(u"link", self.feed_info['link'], {})
|
handler.addQuickElement(u"link", self.feed['link'])
|
||||||
handler.addQuickElement(u"description", self.feed_info['description'], {})
|
handler.addQuickElement(u"description", self.feed['description'])
|
||||||
if self.feed_info['language'] is not None:
|
if self.feed['language'] is not None:
|
||||||
handler.addQuickElement(u"language", self.feed_info['language'], {})
|
handler.addQuickElement(u"language", self.feed['language'])
|
||||||
|
self.write_items(handler)
|
||||||
|
self.endChannelElement(handler)
|
||||||
|
handler.endElement(u"rss")
|
||||||
|
|
||||||
def endChannelElement(self, handler):
|
def endChannelElement(self, handler):
|
||||||
handler.endElement(u"channel")
|
handler.endElement(u"channel")
|
||||||
|
|
||||||
class RssUserland091Feed(RssFeed):
|
class RssUserland091Feed(RssFeed):
|
||||||
def writeRssElement(self, handler):
|
_version = u"0.91"
|
||||||
handler.startElement(u"rss", {u"version": u"0.91"})
|
def write_items(self, handler):
|
||||||
|
for item in self.items:
|
||||||
def writeRssItem(self, handler, item):
|
handler.startElement(u"item", {})
|
||||||
handler.startElement(u"item", {})
|
handler.addQuickElement(u"title", item['title'])
|
||||||
handler.addQuickElement(u"title", item['title'], {})
|
handler.addQuickElement(u"link", item['link'])
|
||||||
handler.addQuickElement(u"link", item['link'], {})
|
if item['description'] is not None:
|
||||||
if item['description'] is not None:
|
handler.addQuickElement(u"description", item['description'])
|
||||||
handler.addQuickElement(u"description", item['description'], {})
|
handler.endElement(u"item")
|
||||||
handler.endElement(u"item")
|
|
||||||
|
|
||||||
class Rss201rev2Feed(RssFeed):
|
class Rss201rev2Feed(RssFeed):
|
||||||
# Spec: http://blogs.law.harvard.edu/tech/rss
|
# Spec: http://blogs.law.harvard.edu/tech/rss
|
||||||
def writeRssElement(self, handler):
|
_version = u"2.0"
|
||||||
handler.startElement(u"rss", {u"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):
|
# Author information.
|
||||||
handler.startElement(u"item", {})
|
if item['author_email'] is not None and item['author_name'] is not None:
|
||||||
handler.addQuickElement(u"title", item['title'], {})
|
handler.addQuickElement(u"author", u"%s (%s)" % \
|
||||||
handler.addQuickElement(u"link", item['link'], {})
|
(item['author_email'], item['author_name']))
|
||||||
if item['description'] is not None:
|
|
||||||
handler.addQuickElement(u"description", item['description'], {})
|
if item['pubdate'] is not None:
|
||||||
if item['author_email'] is not None and item['author_name'] is not None:
|
handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('ascii'))
|
||||||
handler.addQuickElement(u"author", u"%s (%s)" % \
|
if item['comments'] is not None:
|
||||||
(item['author_email'], item['author_name']), {})
|
handler.addQuickElement(u"comments", item['comments'])
|
||||||
if item['pubdate'] is not None:
|
if item['unique_id'] is not None:
|
||||||
handler.addQuickElement(u"pubDate", item['pubdate'].strftime('%a, %d %b %Y %H:%M:%S %Z'), {})
|
handler.addQuickElement(u"guid", item['unique_id'])
|
||||||
if item['comments'] is not None:
|
|
||||||
handler.addQuickElement(u"comments", item['comments'], {})
|
# Enclosure.
|
||||||
if item['unique_id'] is not None:
|
if item['enclosure'] is not None:
|
||||||
handler.addQuickElement(u"guid", item['unique_id'], {})
|
handler.addQuickElement(u"enclosure", '',
|
||||||
if item['enclosure'] is not None:
|
{u"url": item['enclosure'].url, u"length": item['enclosure'].length,
|
||||||
handler.addQuickElement(u"enclosure", '',
|
u"type": item['enclosure'].mime_type})
|
||||||
{u"url": item['enclosure'].url, u"length": item['enclosure'].length,
|
|
||||||
u"type": item['enclosure'].mime_type})
|
# Categories.
|
||||||
for cat in item['categories']:
|
for cat in item['categories']:
|
||||||
handler.addQuickElement(u"category", cat, {})
|
handler.addQuickElement(u"category", cat)
|
||||||
handler.endElement(u"item")
|
|
||||||
|
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
|
# This isolates the decision of what the system default is, so calling code can
|
||||||
# do "feedgenerator.DefaultRssFeed" instead of "feedgenerator.Rss201rev2Feed".
|
# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
|
||||||
DefaultRssFeed = Rss201rev2Feed
|
DefaultFeed = Rss201rev2Feed
|
||||||
|
|
|
@ -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
|
|
|
@ -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"><p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p></summary>
|
||||||
|
</entry></feed>
|
||||||
|
|
||||||
|
.. _django/utils/feedgenerator.py: http://code.djangoproject.com/browser/django/trunk/django/utils/feedgenerator.py
|
Loading…
Reference in New Issue