Fixed #6188, #6304, #6618, #6969, #8758, #8989, #10334, #11069, #11973 and #12403 -- Modified the syndication framework to use class-based views. Thanks to Ben Firshman for his work on this patch.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@12338 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
3f68d255e2
commit
c4c27d8a04
1
AUTHORS
1
AUTHORS
|
@ -166,6 +166,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Afonso Fernández Nogueira <fonzzo.django@gmail.com>
|
||||
J. Pablo Fernandez <pupeno@pupeno.com>
|
||||
Maciej Fijalkowski
|
||||
Ben Firshman <ben@firshman.co.uk>
|
||||
Matthew Flanagan <http://wadofstuff.blogspot.com>
|
||||
Eric Floehr <eric@intellovations.com>
|
||||
Eric Florenzano <floguy@gmail.com>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.syndication.feeds import Feed
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.contrib.sites.models import Site
|
||||
from django.contrib import comments
|
||||
from django.utils.translation import ugettext as _
|
||||
|
@ -33,6 +33,6 @@ class LatestCommentFeed(Feed):
|
|||
params = [settings.COMMENTS_BANNED_USERS_GROUP]
|
||||
qs = qs.extra(where=where, params=params)
|
||||
return qs.order_by('-submit_date')[:40]
|
||||
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.submit_date
|
||||
|
|
|
@ -1,78 +1,22 @@
|
|||
from datetime import datetime, timedelta
|
||||
from django.contrib.syndication import views
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
import warnings
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django.template import loader, Template, TemplateDoesNotExist
|
||||
from django.contrib.sites.models import Site, RequestSite
|
||||
from django.utils import feedgenerator
|
||||
from django.utils.tzinfo import FixedOffset
|
||||
from django.utils.encoding import smart_unicode, iri_to_uri
|
||||
from django.conf import settings
|
||||
from django.template import RequestContext
|
||||
|
||||
def add_domain(domain, url):
|
||||
if not (url.startswith('http://') or url.startswith('https://')):
|
||||
# 'url' must already be ASCII and URL-quoted, so no need for encoding
|
||||
# conversions here.
|
||||
url = iri_to_uri(u'http://%s%s' % (domain, url))
|
||||
return url
|
||||
|
||||
class FeedDoesNotExist(ObjectDoesNotExist):
|
||||
pass
|
||||
|
||||
class Feed(object):
|
||||
item_pubdate = None
|
||||
item_enclosure_url = None
|
||||
feed_type = feedgenerator.DefaultFeed
|
||||
feed_url = None
|
||||
title_template = None
|
||||
description_template = None
|
||||
# This is part of the deprecated API
|
||||
from django.contrib.syndication.views import FeedDoesNotExist, add_domain
|
||||
|
||||
class Feed(views.Feed):
|
||||
"""Provided for backwards compatibility."""
|
||||
def __init__(self, slug, request):
|
||||
warnings.warn('The syndication feeds.Feed class is deprecated. Please '
|
||||
'use the new class based view API.',
|
||||
category=PendingDeprecationWarning)
|
||||
|
||||
self.slug = slug
|
||||
self.request = request
|
||||
self.feed_url = self.feed_url or request.path
|
||||
self.title_template_name = self.title_template or ('feeds/%s_title.html' % slug)
|
||||
self.description_template_name = self.description_template or ('feeds/%s_description.html' % 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, default=None):
|
||||
try:
|
||||
attr = getattr(self, attname)
|
||||
except AttributeError:
|
||||
return default
|
||||
if callable(attr):
|
||||
# Check func_code.co_argcount rather than try/excepting the
|
||||
# function and catching the TypeError, because something inside
|
||||
# the function may raise the TypeError. This technique is more
|
||||
# accurate.
|
||||
if hasattr(attr, 'func_code'):
|
||||
argcount = attr.func_code.co_argcount
|
||||
else:
|
||||
argcount = attr.__call__.func_code.co_argcount
|
||||
if argcount == 2: # one argument is 'self'
|
||||
return attr(obj)
|
||||
else:
|
||||
return attr()
|
||||
return attr
|
||||
|
||||
def feed_extra_kwargs(self, obj):
|
||||
"""
|
||||
Returns an extra keyword arguments dictionary that is used when
|
||||
initializing the feed generator.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def item_extra_kwargs(self, item):
|
||||
"""
|
||||
Returns an extra keyword arguments dictionary that is used with
|
||||
the `add_item` call of the feed generator.
|
||||
"""
|
||||
return {}
|
||||
self.feed_url = getattr(self, 'feed_url', None) or request.path
|
||||
self.title_template = self.title_template or ('feeds/%s_title.html' % slug)
|
||||
self.description_template = self.description_template or ('feeds/%s_description.html' % slug)
|
||||
|
||||
def get_object(self, bits):
|
||||
return None
|
||||
|
@ -86,94 +30,9 @@ class Feed(object):
|
|||
bits = url.split('/')
|
||||
else:
|
||||
bits = []
|
||||
|
||||
try:
|
||||
obj = self.get_object(bits)
|
||||
except ObjectDoesNotExist:
|
||||
raise FeedDoesNotExist
|
||||
return super(Feed, self).get_feed(obj, self.request)
|
||||
|
||||
if Site._meta.installed:
|
||||
current_site = Site.objects.get_current()
|
||||
else:
|
||||
current_site = RequestSite(self.request)
|
||||
|
||||
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),
|
||||
subtitle = self.__get_dynamic_attr('subtitle', obj),
|
||||
link = link,
|
||||
description = self.__get_dynamic_attr('description', obj),
|
||||
language = settings.LANGUAGE_CODE.decode(),
|
||||
feed_url = add_domain(current_site.domain,
|
||||
self.__get_dynamic_attr('feed_url', obj)),
|
||||
author_name = self.__get_dynamic_attr('author_name', obj),
|
||||
author_link = self.__get_dynamic_attr('author_link', obj),
|
||||
author_email = self.__get_dynamic_attr('author_email', obj),
|
||||
categories = self.__get_dynamic_attr('categories', obj),
|
||||
feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
|
||||
feed_guid = self.__get_dynamic_attr('feed_guid', obj),
|
||||
ttl = self.__get_dynamic_attr('ttl', obj),
|
||||
**self.feed_extra_kwargs(obj)
|
||||
)
|
||||
|
||||
try:
|
||||
title_tmp = loader.get_template(self.title_template_name)
|
||||
except TemplateDoesNotExist:
|
||||
title_tmp = Template('{{ obj }}')
|
||||
try:
|
||||
description_tmp = loader.get_template(self.description_template_name)
|
||||
except TemplateDoesNotExist:
|
||||
description_tmp = 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 = smart_unicode(enc_url),
|
||||
length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
|
||||
mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
|
||||
)
|
||||
author_name = self.__get_dynamic_attr('item_author_name', item)
|
||||
if author_name is not None:
|
||||
author_email = self.__get_dynamic_attr('item_author_email', item)
|
||||
author_link = self.__get_dynamic_attr('item_author_link', item)
|
||||
else:
|
||||
author_email = author_link = None
|
||||
|
||||
pubdate = self.__get_dynamic_attr('item_pubdate', item)
|
||||
if pubdate and not pubdate.tzinfo:
|
||||
now = datetime.now()
|
||||
utcnow = datetime.utcnow()
|
||||
|
||||
# Must always subtract smaller time from larger time here.
|
||||
if utcnow > now:
|
||||
sign = -1
|
||||
tzDifference = (utcnow - now)
|
||||
else:
|
||||
sign = 1
|
||||
tzDifference = (now - utcnow)
|
||||
|
||||
# Round the timezone offset to the nearest half hour.
|
||||
tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
|
||||
tzOffset = timedelta(minutes=tzOffsetMinutes)
|
||||
pubdate = pubdate.replace(tzinfo=FixedOffset(tzOffset))
|
||||
|
||||
feed.add_item(
|
||||
title = title_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
|
||||
link = link,
|
||||
description = description_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
|
||||
unique_id = self.__get_dynamic_attr('item_guid', item, link),
|
||||
enclosure = enc,
|
||||
pubdate = pubdate,
|
||||
author_name = author_name,
|
||||
author_email = author_email,
|
||||
author_link = author_link,
|
||||
categories = self.__get_dynamic_attr('item_categories', item),
|
||||
item_copyright = self.__get_dynamic_attr('item_copyright', item),
|
||||
**self.item_extra_kwargs(item)
|
||||
)
|
||||
return feed
|
||||
|
|
|
@ -1,7 +1,203 @@
|
|||
from django.contrib.syndication import feeds
|
||||
import datetime
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site, RequestSite
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.template import loader, Template, TemplateDoesNotExist, RequestContext
|
||||
from django.utils import feedgenerator, tzinfo
|
||||
from django.utils.encoding import force_unicode, iri_to_uri, smart_unicode
|
||||
from django.utils.html import escape
|
||||
|
||||
def add_domain(domain, url):
|
||||
if not (url.startswith('http://')
|
||||
or url.startswith('https://')
|
||||
or url.startswith('mailto:')):
|
||||
# 'url' must already be ASCII and URL-quoted, so no need for encoding
|
||||
# conversions here.
|
||||
url = iri_to_uri(u'http://%s%s' % (domain, url))
|
||||
return url
|
||||
|
||||
class FeedDoesNotExist(ObjectDoesNotExist):
|
||||
pass
|
||||
|
||||
|
||||
class Feed(object):
|
||||
feed_type = feedgenerator.DefaultFeed
|
||||
title_template = None
|
||||
description_template = None
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
try:
|
||||
obj = self.get_object(request, *args, **kwargs)
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404('Feed object does not exist.')
|
||||
feedgen = self.get_feed(obj, request)
|
||||
response = HttpResponse(mimetype=feedgen.mime_type)
|
||||
feedgen.write(response, 'utf-8')
|
||||
return response
|
||||
|
||||
def item_title(self, item):
|
||||
# Titles should be double escaped by default (see #6533)
|
||||
return escape(force_unicode(item))
|
||||
|
||||
def item_description(self, item):
|
||||
return force_unicode(item)
|
||||
|
||||
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, default=None):
|
||||
try:
|
||||
attr = getattr(self, attname)
|
||||
except AttributeError:
|
||||
return default
|
||||
if callable(attr):
|
||||
# Check func_code.co_argcount rather than try/excepting the
|
||||
# function and catching the TypeError, because something inside
|
||||
# the function may raise the TypeError. This technique is more
|
||||
# accurate.
|
||||
if hasattr(attr, 'func_code'):
|
||||
argcount = attr.func_code.co_argcount
|
||||
else:
|
||||
argcount = attr.__call__.func_code.co_argcount
|
||||
if argcount == 2: # one argument is 'self'
|
||||
return attr(obj)
|
||||
else:
|
||||
return attr()
|
||||
return attr
|
||||
|
||||
def feed_extra_kwargs(self, obj):
|
||||
"""
|
||||
Returns an extra keyword arguments dictionary that is used when
|
||||
initializing the feed generator.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def item_extra_kwargs(self, item):
|
||||
"""
|
||||
Returns an extra keyword arguments dictionary that is used with
|
||||
the `add_item` call of the feed generator.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_object(self, request, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def get_feed(self, obj, request):
|
||||
"""
|
||||
Returns a feedgenerator.DefaultFeed object, fully populated, for
|
||||
this feed. Raises FeedDoesNotExist for invalid parameters.
|
||||
"""
|
||||
if Site._meta.installed:
|
||||
current_site = Site.objects.get_current()
|
||||
else:
|
||||
current_site = RequestSite(request)
|
||||
|
||||
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),
|
||||
subtitle = self.__get_dynamic_attr('subtitle', obj),
|
||||
link = link,
|
||||
description = self.__get_dynamic_attr('description', obj),
|
||||
language = settings.LANGUAGE_CODE.decode(),
|
||||
feed_url = add_domain(current_site.domain,
|
||||
self.__get_dynamic_attr('feed_url', obj) or request.path),
|
||||
author_name = self.__get_dynamic_attr('author_name', obj),
|
||||
author_link = self.__get_dynamic_attr('author_link', obj),
|
||||
author_email = self.__get_dynamic_attr('author_email', obj),
|
||||
categories = self.__get_dynamic_attr('categories', obj),
|
||||
feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
|
||||
feed_guid = self.__get_dynamic_attr('feed_guid', obj),
|
||||
ttl = self.__get_dynamic_attr('ttl', obj),
|
||||
**self.feed_extra_kwargs(obj)
|
||||
)
|
||||
|
||||
title_tmp = None
|
||||
if self.title_template is not None:
|
||||
try:
|
||||
title_tmp = loader.get_template(self.title_template)
|
||||
except TemplateDoesNotExist:
|
||||
pass
|
||||
|
||||
description_tmp = None
|
||||
if self.description_template is not None:
|
||||
try:
|
||||
description_tmp = loader.get_template(self.description_template)
|
||||
except TemplateDoesNotExist:
|
||||
pass
|
||||
|
||||
for item in self.__get_dynamic_attr('items', obj):
|
||||
if title_tmp is not None:
|
||||
title = title_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
|
||||
else:
|
||||
title = self.__get_dynamic_attr('item_title', item)
|
||||
if description_tmp is not None:
|
||||
description = description_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
|
||||
else:
|
||||
description = self.__get_dynamic_attr('item_description', item)
|
||||
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 = smart_unicode(enc_url),
|
||||
length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
|
||||
mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
|
||||
)
|
||||
author_name = self.__get_dynamic_attr('item_author_name', item)
|
||||
if author_name is not None:
|
||||
author_email = self.__get_dynamic_attr('item_author_email', item)
|
||||
author_link = self.__get_dynamic_attr('item_author_link', item)
|
||||
else:
|
||||
author_email = author_link = None
|
||||
|
||||
pubdate = self.__get_dynamic_attr('item_pubdate', item)
|
||||
if pubdate and not pubdate.tzinfo:
|
||||
now = datetime.datetime.now()
|
||||
utcnow = datetime.datetime.utcnow()
|
||||
|
||||
# Must always subtract smaller time from larger time here.
|
||||
if utcnow > now:
|
||||
sign = -1
|
||||
tzDifference = (utcnow - now)
|
||||
else:
|
||||
sign = 1
|
||||
tzDifference = (now - utcnow)
|
||||
|
||||
# Round the timezone offset to the nearest half hour.
|
||||
tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
|
||||
tzOffset = datetime.timedelta(minutes=tzOffsetMinutes)
|
||||
pubdate = pubdate.replace(tzinfo=tzinfo.FixedOffset(tzOffset))
|
||||
|
||||
feed.add_item(
|
||||
title = title,
|
||||
link = link,
|
||||
description = description,
|
||||
unique_id = self.__get_dynamic_attr('item_guid', item, link),
|
||||
enclosure = enc,
|
||||
pubdate = pubdate,
|
||||
author_name = author_name,
|
||||
author_email = author_email,
|
||||
author_link = author_link,
|
||||
categories = self.__get_dynamic_attr('item_categories', item),
|
||||
item_copyright = self.__get_dynamic_attr('item_copyright', item),
|
||||
**self.item_extra_kwargs(item)
|
||||
)
|
||||
return feed
|
||||
|
||||
|
||||
def feed(request, url, feed_dict=None):
|
||||
"""Provided for backwards compatibility."""
|
||||
import warnings
|
||||
warnings.warn('The syndication feed() view is deprecated. Please use the '
|
||||
'new class based view API.',
|
||||
category=PendingDeprecationWarning)
|
||||
|
||||
if not feed_dict:
|
||||
raise Http404("No feeds are registered.")
|
||||
|
||||
|
@ -17,9 +213,10 @@ def feed(request, url, feed_dict=None):
|
|||
|
||||
try:
|
||||
feedgen = f(slug, request).get_feed(param)
|
||||
except feeds.FeedDoesNotExist:
|
||||
except FeedDoesNotExist:
|
||||
raise Http404("Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug)
|
||||
|
||||
response = HttpResponse(mimetype=feedgen.mime_type)
|
||||
feedgen.write(response, 'utf-8')
|
||||
return response
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ For definitions of the different versions of RSS, see:
|
|||
http://diveintomark.org/archives/2004/02/04/incompatible-rss
|
||||
"""
|
||||
|
||||
import re
|
||||
import datetime
|
||||
import urlparse
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
from django.utils.encoding import force_unicode, iri_to_uri
|
||||
|
||||
|
@ -46,12 +46,16 @@ def rfc3339_date(date):
|
|||
return date.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
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)
|
||||
"""
|
||||
Creates a TagURI.
|
||||
|
||||
See http://diveintomark.org/archives/2004/05/28/howto-atom-id
|
||||
"""
|
||||
url_split = urlparse.urlparse(url)
|
||||
d = ''
|
||||
if date is not None:
|
||||
tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
|
||||
tag = re.sub('#', '/', tag)
|
||||
return u'tag:' + tag
|
||||
d = ',%s' % date.strftime('%Y-%m-%d')
|
||||
return u'tag:%s%s:%s/%s' % (url_split.hostname, d, url_split.path, url_split.fragment)
|
||||
|
||||
class SyndicationFeed(object):
|
||||
"Base class for all syndication feeds. Subclasses should provide write()"
|
||||
|
@ -61,6 +65,9 @@ class SyndicationFeed(object):
|
|||
to_unicode = lambda s: force_unicode(s, strings_only=True)
|
||||
if categories:
|
||||
categories = [force_unicode(c) for c in categories]
|
||||
if ttl is not None:
|
||||
# Force ints to unicode
|
||||
ttl = force_unicode(ttl)
|
||||
self.feed = {
|
||||
'title': to_unicode(title),
|
||||
'link': iri_to_uri(link),
|
||||
|
@ -91,6 +98,9 @@ class SyndicationFeed(object):
|
|||
to_unicode = lambda s: force_unicode(s, strings_only=True)
|
||||
if categories:
|
||||
categories = [to_unicode(c) for c in categories]
|
||||
if ttl is not None:
|
||||
# Force ints to unicode
|
||||
ttl = force_unicode(ttl)
|
||||
item = {
|
||||
'title': to_unicode(title),
|
||||
'link': iri_to_uri(link),
|
||||
|
@ -186,7 +196,8 @@ class RssFeed(SyndicationFeed):
|
|||
handler.endElement(u"rss")
|
||||
|
||||
def rss_attributes(self):
|
||||
return {u"version": self._version}
|
||||
return {u"version": self._version,
|
||||
u"xmlns:atom": u"http://www.w3.org/2005/Atom"}
|
||||
|
||||
def write_items(self, handler):
|
||||
for item in self.items:
|
||||
|
@ -198,6 +209,7 @@ class RssFeed(SyndicationFeed):
|
|||
handler.addQuickElement(u"title", self.feed['title'])
|
||||
handler.addQuickElement(u"link", self.feed['link'])
|
||||
handler.addQuickElement(u"description", self.feed['description'])
|
||||
handler.addQuickElement(u"atom:link", None, {u"rel": u"self", u"href": self.feed['feed_url']})
|
||||
if self.feed['language'] is not None:
|
||||
handler.addQuickElement(u"language", self.feed['language'])
|
||||
for cat in self.feed['categories']:
|
||||
|
@ -235,7 +247,7 @@ class Rss201rev2Feed(RssFeed):
|
|||
elif item["author_email"]:
|
||||
handler.addQuickElement(u"author", item["author_email"])
|
||||
elif item["author_name"]:
|
||||
handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
|
||||
handler.addQuickElement(u"dc:creator", item["author_name"], {u"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
|
||||
|
||||
if item['pubdate'] is not None:
|
||||
handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
|
||||
|
|
|
@ -82,6 +82,10 @@ their deprecation, as per the :ref:`Django deprecation policy
|
|||
* The ability to use a function-based test runners will be removed,
|
||||
along with the ``django.test.simple.run_tests()`` test runner.
|
||||
|
||||
* The ``views.feed()`` view and ``feeds.Feed`` class in
|
||||
``django.contrib.syndication`` have been deprecated since the 1.2
|
||||
release. The class-based view ``views.Feed`` should be used instead.
|
||||
|
||||
* 2.0
|
||||
* ``django.views.defaults.shortcut()``. This function has been moved
|
||||
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
|
||||
|
|
|
@ -8,14 +8,15 @@ The syndication feed framework
|
|||
:synopsis: A framework for generating syndication feeds, in RSS and Atom,
|
||||
quite easily.
|
||||
|
||||
Django comes with a high-level syndication-feed-generating framework that makes
|
||||
creating RSS_ and Atom_ feeds easy.
|
||||
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.
|
||||
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.
|
||||
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/
|
||||
|
@ -23,74 +24,37 @@ to generate feeds outside of a Web context, or in some other lower-level way.
|
|||
The high-level framework
|
||||
========================
|
||||
|
||||
.. versionchanged:: 1.2
|
||||
The high-level feeds framework was refactored in Django 1.2. The
|
||||
pre-1.2 interface still exists, but it has been deprecated, and
|
||||
will be removed in Django 1.4. If you need to maintain an old-style
|
||||
Django feed, please consult the Django 1.1 documentation. For
|
||||
details on updating to use the new high-level feed framework, see
|
||||
the :ref:`Django 1.2 release notes <1.2-updating-feeds>`.
|
||||
|
||||
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 :class:`~django.contrib.syndication.feeds.Feed`
|
||||
class and point to it in your :ref:`URLconf <topics-http-urls>`.
|
||||
|
||||
Initialization
|
||||
--------------
|
||||
|
||||
To activate syndication feeds on your Django site, add this line to your
|
||||
:ref:`URLconf <topics-http-urls>`::
|
||||
|
||||
(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
|
||||
:file:`"feeds/"`. (You can change that :file:`"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, :data:`feed_dict` should be a dictionary that maps a feed's slug
|
||||
(short URL label) to its :class:`~django.contrib.syndication.feeds.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
|
||||
:class:`~django.contrib.syndication.feeds.Feed` classes themselves.
|
||||
The high-level feed-generating framework is supplied by the
|
||||
:class:`~django.contrib.syndication.views.Feed` class. To create a
|
||||
feed, write a :class:`~django.contrib.syndication.views.Feed` class
|
||||
and point to an instance of it in your :ref:`URLconf
|
||||
<topics-http-urls>`.
|
||||
|
||||
Feed classes
|
||||
------------
|
||||
|
||||
A :class:`~django.contrib.syndication.feeds.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).
|
||||
A :class:`~django.contrib.syndication.views.Feed` class is a 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).
|
||||
|
||||
:class:`~django.contrib.syndication.feeds.Feed` classes must subclass
|
||||
``django.contrib.syndication.feeds.Feed``. They can live anywhere in your
|
||||
codebase.
|
||||
Feed classes subclass :class:`django.contrib.syndication.views.Feed`.
|
||||
They can live anywhere in your codebase.
|
||||
|
||||
Instances of :class:`~django.contrib.syndication.views.Feed` classes
|
||||
are views which can be used in your :ref:`URLconf <topics-http-urls>`.
|
||||
|
||||
A simple example
|
||||
----------------
|
||||
|
@ -98,10 +62,10 @@ 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.contrib.syndication.views import Feed
|
||||
from chicagocrime.models import NewsItem
|
||||
|
||||
class LatestEntries(Feed):
|
||||
class LatestEntriesFeed(Feed):
|
||||
title = "Chicagocrime.org site news"
|
||||
link = "/sitenews/"
|
||||
description = "Updates on changes and additions to chicagocrime.org."
|
||||
|
@ -109,9 +73,27 @@ latest five news items::
|
|||
def items(self):
|
||||
return NewsItem.objects.order_by('-pub_date')[:5]
|
||||
|
||||
def item_title(self, item):
|
||||
return item.title
|
||||
|
||||
def item_description(self, item):
|
||||
return item.description
|
||||
|
||||
To connect a URL to this feed, put an instance of the Feed object in
|
||||
your :ref:`URLconf <topics-http-urls>`. For example::
|
||||
|
||||
from django.conf.urls.defaults import *
|
||||
from myproject.feeds import LatestEntriesFeed
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# ...
|
||||
(r'^latest/feed/$', LatestEntriesFeed()),
|
||||
# ...
|
||||
)
|
||||
|
||||
Note:
|
||||
|
||||
* The class subclasses ``django.contrib.syndication.feeds.Feed``.
|
||||
* The Feed class subclasses :class:`django.contrib.syndication.views.Feed`.
|
||||
|
||||
* :attr:`title`, :attr:`link` and :attr:`description` correspond to the
|
||||
standard RSS ``<title>``, ``<link>`` and ``<description>`` elements,
|
||||
|
@ -129,17 +111,23 @@ Note:
|
|||
:attr:`subtitle` attribute instead of the :attr:`description` attribute.
|
||||
See `Publishing Atom and RSS feeds in tandem`_, later, for an example.
|
||||
|
||||
One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``,
|
||||
One thing is 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
|
||||
:ref:`Django templates <topics-templates>` called
|
||||
:file:`feeds/latest_title.html` and
|
||||
:file:`feeds/latest_description.html`, where :attr:`latest` is the
|
||||
:attr:`slug` specified in the URLconf for the given feed. Note the
|
||||
``.html`` extension is required. The RSS system renders that template for
|
||||
each item, passing it two template context variables:
|
||||
* For the contents of ``<title>`` and ``<description>``, Django tries
|
||||
calling the methods :meth:`item_title()` and :meth:`item_description()` on
|
||||
the :class:`~django.contrib.syndication.views.Feed` class. They are passed
|
||||
a single parameter, :attr:`item`, which is the object itself. These are
|
||||
optional; by default, the unicode representation of the object is used for
|
||||
both.
|
||||
|
||||
If you want to do any special formatting for either the title or
|
||||
description, :ref:`Django templates <topics-templates>` can be used
|
||||
instead. Their paths can be specified with the ``title_template`` and
|
||||
``description_template`` attributes on the
|
||||
:class:`~django.contrib.syndication.views.Feed` class. The templates are
|
||||
rendered for each item and are passed two template context variables:
|
||||
|
||||
* ``{{ obj }}`` -- The current object (one of whichever objects you
|
||||
returned in :meth:`items()`).
|
||||
|
@ -152,152 +140,102 @@ into those elements.
|
|||
:ref:`RequestSite section of the sites framework documentation
|
||||
<requestsite-objects>` for more.
|
||||
|
||||
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. You can also change the names
|
||||
of these two templates by specifying ``title_template`` and
|
||||
``description_template`` as attributes of your
|
||||
:class:`~django.contrib.syndication.feeds.Feed` class.
|
||||
See `a complex example`_ below that uses a description template.
|
||||
|
||||
* To specify the contents of ``<link>``, you have two options. For each item
|
||||
in :meth:`items()`, Django first tries calling a method
|
||||
:meth:`item_link()` in the :class:`~django.contrib.syndication.feeds.Feed`
|
||||
class, passing it a single parameter, :attr:`item`, which is the object
|
||||
itself. If that method doesn't exist, Django tries executing a
|
||||
``get_absolute_url()`` method on that object. . Both
|
||||
``get_absolute_url()`` and :meth:`item_link()` should return the item's
|
||||
URL as a normal Python string. As with ``get_absolute_url()``, the result
|
||||
of :meth:`item_link()` will be included directly in the URL, so you are
|
||||
responsible for doing all necessary URL quoting and conversion to ASCII
|
||||
inside the method itself.
|
||||
|
||||
* For the LatestEntries example above, we could have very simple feed
|
||||
templates:
|
||||
|
||||
* latest_title.html:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ obj.title }}
|
||||
|
||||
* latest_description.html:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ obj.description }}
|
||||
in :meth:`items()`, Django first tries calling the
|
||||
:meth:`item_link()` method on the
|
||||
:class:`~django.contrib.syndication.views.Feed` class. In a similar way to
|
||||
the title and description, it is passed it a single parameter,
|
||||
:attr:`item`. If that method doesn't exist, Django tries executing a
|
||||
``get_absolute_url()`` method on that object. Both
|
||||
:meth:`get_absolute_url()` and :meth:`item_link()` should return the
|
||||
item's URL as a normal Python string. As with ``get_absolute_url()``, the
|
||||
result of :meth:`item_link()` will be included directly in the URL, so you
|
||||
are responsible for doing all necessary URL quoting and conversion to
|
||||
ASCII inside the method itself.
|
||||
|
||||
.. _chicagocrime.org: http://www.chicagocrime.org/
|
||||
|
||||
A complex example
|
||||
-----------------
|
||||
|
||||
The framework also supports more complex feeds, via parameters.
|
||||
The framework also supports more complex feeds, via arguments.
|
||||
|
||||
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
|
||||
:class:`~django.contrib.syndication.feeds.Feed` class for each police beat; that
|
||||
:class:`~django.contrib.syndication.views.Feed` class for each police beat; that
|
||||
would violate the :ref:`DRY principle <dry>` and would couple data to
|
||||
programming logic. Instead, the syndication framework lets you make generic
|
||||
feeds that output items based on information in the feed's URL.
|
||||
programming logic. Instead, the syndication framework lets you access the
|
||||
arguments passed from your :ref:`URLconf <topics-http-urls>` so feeds can output
|
||||
items based on information in the feed's URL.
|
||||
|
||||
On chicagocrime.org, the police-beat feeds are accessible via URLs like this:
|
||||
|
||||
* :file:`/rss/beats/0613/` -- Returns recent crimes for beat 0613.
|
||||
* :file:`/rss/beats/1424/` -- Returns recent crimes for beat 1424.
|
||||
* :file:`/beats/613/rss/` -- Returns recent crimes for beat 613.
|
||||
* :file:`/beats/1424/rss/` -- 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.
|
||||
These can be matched with a :ref:`URLconf <topics-http-urls>` line such as::
|
||||
|
||||
An example makes this clear. Here's the code for these beat-specific feeds::
|
||||
(r'^beats/(?P<beat_id>\d+)/rss/$', BeatFeed()),
|
||||
|
||||
from django.contrib.syndication.feeds import FeedDoesNotExist
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
Like a view, the arguments in the URL are passed to the :meth:`get_object()`
|
||||
method along with the request object.
|
||||
|
||||
.. versionchanged:: 1.2
|
||||
Prior to version 1.2, ``get_object()`` only accepted a ``bits`` argument.
|
||||
|
||||
Here's the code for these beat-specific feeds::
|
||||
|
||||
from django.contrib.syndication.views import FeedDoesNotExist
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
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 Beat.objects.get(beat__exact=bits[0])
|
||||
description_template = 'feeds/beat_description.html'
|
||||
|
||||
def get_object(self, request, beat_id):
|
||||
return get_object_or_404(Beat, pk=beat_id)
|
||||
|
||||
def title(self, obj):
|
||||
return "Chicagocrime.org: Crimes for beat %s" % obj.beat
|
||||
|
||||
def link(self, obj):
|
||||
if not obj:
|
||||
raise FeedDoesNotExist
|
||||
return obj.get_absolute_url()
|
||||
|
||||
def description(self, obj):
|
||||
return "Crimes recently reported in police beat %s" % obj.beat
|
||||
|
||||
def items(self, obj):
|
||||
return Crime.objects.filter(beat__id__exact=obj.id).order_by('-crime_date')[:30]
|
||||
return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30]
|
||||
|
||||
Here's the basic algorithm the RSS framework follows, given this class and a
|
||||
request to the URL :file:`/rss/beats/0613/`:
|
||||
To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django
|
||||
uses the :meth:`title()`, :meth:`link()` and :meth:`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
|
||||
:attr:`title`, :attr:`link` and :attr:`description`, Django follows this
|
||||
algorithm:
|
||||
|
||||
* The framework gets the URL :file:`/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
|
||||
:class:`~django.contrib.syndication.feeds.Feed` class'
|
||||
:meth:`get_object()` method, passing it the bits. In this case, bits is
|
||||
``['0613']``. For a request to :file:`/rss/beats/0613/foo/bar/`, bits
|
||||
would be ``['0613', 'foo', 'bar']``.
|
||||
* First, it tries to call a method, passing the ``obj`` argument, where
|
||||
``obj`` is the object returned by :meth:`get_object()`.
|
||||
|
||||
* :meth:`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 :meth:`get_object()` should raise
|
||||
:exc:`django.core.exceptions.ObjectDoesNotExist` if given invalid
|
||||
parameters. There's no ``try``/``except`` around the
|
||||
``Beat.objects.get()`` call, because it's not necessary; that function
|
||||
raises :exc:`Beat.DoesNotExist` on failure, and :exc:`Beat.DoesNotExist`
|
||||
is a subclass of :exc:`ObjectDoesNotExist`. Raising
|
||||
:exc:`ObjectDoesNotExist` in :meth:`get_object()` tells Django to produce
|
||||
a 404 error for that request.
|
||||
* Failing that, it tries to call a method with no arguments.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
:meth:`get_object()` can handle the :file:`/rss/beats/` url.
|
||||
* Failing that, it uses the class attribute.
|
||||
|
||||
The :meth:`get_object()` method also has a chance to handle the
|
||||
:file:`/rss/beats/` url. In this case, :data:`bits` will be an
|
||||
empty list. In our example, ``len(bits) != 1`` and an
|
||||
:exc:`ObjectDoesNotExist` exception will be raised, so
|
||||
:file:`/rss/beats/` will generate a 404 page. But you can handle this case
|
||||
however you like. For example, you could generate a combined feed for all
|
||||
beats.
|
||||
Also note that :meth:`items()` also follows the same algorithm -- first, it
|
||||
tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items`
|
||||
class attribute (which should be a list).
|
||||
|
||||
* To generate the feed's ``<title>``, ``<link>`` and ``<description>``,
|
||||
Django uses the :meth:`title()`, :meth:`link()` and :meth:`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 :attr:`title`, :attr:`link` and
|
||||
:attr:`description`, Django follows this algorithm:
|
||||
We are using a template for the item descriptions. It can be very simple:
|
||||
|
||||
* First, it tries to call a method, passing the ``obj`` argument, where
|
||||
``obj`` is the object returned by :meth:`get_object()`.
|
||||
.. code-block:: html+django
|
||||
|
||||
* Failing that, it tries to call a method with no arguments.
|
||||
{{ obj.description }}
|
||||
|
||||
* Failing that, it uses the class attribute.
|
||||
|
||||
Inside the :meth:`link()` method, we handle the possibility that ``obj``
|
||||
might be ``None``, which can occur when the URL isn't fully specified. In
|
||||
some cases, you might want to do something else in this case, which would
|
||||
mean you'd need to check for ``obj`` existing in other methods as well.
|
||||
(The :meth:`link()` method is called very early in the feed generation
|
||||
process, so it's a good place to bail out early.)
|
||||
|
||||
* Finally, note that :meth:`items()` in this example also takes the ``obj``
|
||||
argument. The algorithm for :attr:`items` is the same as described in the
|
||||
previous step -- first, it tries :meth:`items(obj)`, then :meth:`items()`,
|
||||
then finally an :attr:`items` class attribute (which should be a list).
|
||||
However, you are free to add formatting as desired.
|
||||
|
||||
The ``ExampleFeed`` class below gives full documentation on methods and
|
||||
attributes of :class:`~django.contrib.syndication.feeds.Feed` classes.
|
||||
attributes of :class:`~django.contrib.syndication.views.Feed` classes.
|
||||
|
||||
Specifying the type of feed
|
||||
---------------------------
|
||||
|
@ -305,7 +243,7 @@ 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
|
||||
:class:`~django.contrib.syndication.feeds.Feed` class, like so::
|
||||
:class:`~django.contrib.syndication.views.Feed` class, like so::
|
||||
|
||||
from django.utils.feedgenerator import Atom1Feed
|
||||
|
||||
|
@ -353,13 +291,13 @@ 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
|
||||
:class:`~django.contrib.syndication.feeds.Feed`
|
||||
:class:`~django.contrib.syndication.views.Feed`
|
||||
class and set the :attr:`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.contrib.syndication.views import Feed
|
||||
from chicagocrime.models import NewsItem
|
||||
from django.utils.feedgenerator import Atom1Feed
|
||||
|
||||
|
@ -381,7 +319,7 @@ Here's a full example::
|
|||
a feed-level "description," but they *do* provide for a "subtitle."
|
||||
|
||||
If you provide a :attr:`description` in your
|
||||
:class:`~django.contrib.syndication.feeds.Feed` class, Django will *not*
|
||||
:class:`~django.contrib.syndication.views.Feed` class, Django will *not*
|
||||
automatically put that into the :attr:`subtitle` element, because a
|
||||
subtitle and description are not necessarily the same thing. Instead, you
|
||||
should define a :attr:`subtitle` attribute.
|
||||
|
@ -394,56 +332,50 @@ 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}),
|
||||
(r'^sitenews/rss/$', RssSiteNewsFeed()),
|
||||
(r'^sitenews/atom/$', AtomSiteNewsFeed()),
|
||||
# ...
|
||||
)
|
||||
|
||||
Feed class reference
|
||||
--------------------
|
||||
|
||||
.. class:: django.contrib.syndication.feeds.Feed
|
||||
.. class:: django.contrib.syndication.views.Feed
|
||||
|
||||
This example illustrates all possible attributes and methods for a
|
||||
:class:`~django.contrib.syndication.feeds.Feed` class::
|
||||
:class:`~django.contrib.syndication.views.Feed` class::
|
||||
|
||||
from django.contrib.syndication.feeds import Feed
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.utils import feedgenerator
|
||||
|
||||
class ExampleFeed(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.
|
||||
# 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
|
||||
|
||||
# TEMPLATE NAMES -- Optional. These should be strings representing
|
||||
# names of Django templates that the system should use in rendering the
|
||||
# title and description of your feed items. Both are optional.
|
||||
# If you don't specify one, or either, Django will use the template
|
||||
# 'feeds/SLUG_title.html' and 'feeds/SLUG_description.html', where SLUG
|
||||
# is the slug you specify in the URL.
|
||||
# TEMPLATE NAMES -- Optional. These should be strings
|
||||
# representing names of Django templates that the system should
|
||||
# use in rendering the title and description of your feed items.
|
||||
# Both are optional. If a template is not specified, the
|
||||
# item_title() or item_description() methods are used instead.
|
||||
|
||||
title_template = None
|
||||
description_template = None
|
||||
|
||||
# TITLE -- One of the following three is required. The framework looks
|
||||
# for them in this order.
|
||||
# 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.
|
||||
Takes the object returned by get_object() and returns the
|
||||
feed's title as a normal Python string.
|
||||
"""
|
||||
|
||||
def title(self):
|
||||
|
@ -453,13 +385,13 @@ This example illustrates all possible attributes and methods for a
|
|||
|
||||
title = 'foo' # Hard-coded title.
|
||||
|
||||
# LINK -- One of the following three is required. The framework looks
|
||||
# for them in this order.
|
||||
# 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.
|
||||
# Takes the object returned by get_object() and returns the feed's
|
||||
# link as a normal Python string.
|
||||
"""
|
||||
|
||||
def link(self):
|
||||
|
@ -572,18 +504,18 @@ This example illustrates all possible attributes and methods for a
|
|||
# COPYRIGHT NOTICE -- One of the following three is optional. The
|
||||
# framework looks for them in this order.
|
||||
|
||||
def copyright(self, obj):
|
||||
def feed_copyright(self, obj):
|
||||
"""
|
||||
Takes the object returned by get_object() and returns the feed's
|
||||
copyright notice as a normal Python string.
|
||||
"""
|
||||
|
||||
def copyright(self):
|
||||
def feed_copyright(self):
|
||||
"""
|
||||
Returns the feed's copyright notice as a normal Python string.
|
||||
"""
|
||||
|
||||
copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
|
||||
feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
|
||||
|
||||
# TTL -- One of the following three is optional. The framework looks
|
||||
# for them in this order. Ignored for Atom feeds.
|
||||
|
@ -620,13 +552,44 @@ This example illustrates all possible attributes and methods for a
|
|||
# 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):
|
||||
def get_object(self, request, *args, **kwargs):
|
||||
"""
|
||||
Takes a list of strings gleaned from the URL and returns an object
|
||||
represented by this feed. Raises
|
||||
Takes the current request and the arguments from the URL, and
|
||||
returns an object represented by this feed. Raises
|
||||
django.core.exceptions.ObjectDoesNotExist on error.
|
||||
"""
|
||||
|
||||
# ITEM TITLE AND DESCRIPTION -- If title_template or
|
||||
# description_template are not defined, these are used instead. Both are
|
||||
# optional, by default they will use the unicode representation of the
|
||||
# item.
|
||||
|
||||
def item_title(self, item):
|
||||
"""
|
||||
Takes an item, as returned by items(), and returns the item's
|
||||
title as a normal Python string.
|
||||
"""
|
||||
|
||||
def item_title(self):
|
||||
"""
|
||||
Returns the title for every item in the feed.
|
||||
"""
|
||||
|
||||
item_title = 'Breaking News: Nothing Happening' # Hard-coded title.
|
||||
|
||||
def item_description(self, item):
|
||||
"""
|
||||
Takes an item, as returned by items(), and returns the item's
|
||||
description as a normal Python string.
|
||||
"""
|
||||
|
||||
def item_description(self):
|
||||
"""
|
||||
Returns the description for every item in the feed.
|
||||
"""
|
||||
|
||||
item_description = 'A description of the item.' # Hard-coded description.
|
||||
|
||||
# ITEM LINK -- One of these three is required. The framework looks for
|
||||
# them in this order.
|
||||
|
||||
|
@ -686,7 +649,7 @@ This example illustrates all possible attributes and methods for a
|
|||
|
||||
item_author_email = 'test@example.com' # Hard-coded author e-mail.
|
||||
|
||||
# ITEM AUTHOR LINK --One of the following three is optional. The
|
||||
# ITEM AUTHOR LINK -- One of the following three is optional. The
|
||||
# framework looks for them in this order. In each case, the URL should
|
||||
# include the "http://" and domain name.
|
||||
#
|
||||
|
|
|
@ -386,6 +386,87 @@ approach. Old style function-based test runners will still work, but
|
|||
should be updated to use the new :ref:`class-based runners
|
||||
<topics-testing-test_runner>`.
|
||||
|
||||
.. _1.2-updating-feeds:
|
||||
|
||||
``Feed`` in ``django.contrib.syndication.feeds``
|
||||
------------------------------------------------
|
||||
|
||||
The :class:`django.contrib.syndication.feeds.Feed` class has been
|
||||
replaced by the :class:`django.contrib.syndication.views.Feed` class.
|
||||
The old ``feeds.Feed`` class is deprecated, and will be removed in
|
||||
Django 1.4.
|
||||
|
||||
The new class has an almost identical API, but allows instances to be
|
||||
used as views. For example, consider the use of the old framework in
|
||||
the following :ref:`URLconf <topics-http-urls>`::
|
||||
|
||||
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}),
|
||||
# ...
|
||||
)
|
||||
|
||||
Using the new Feed class, these feeds can be deployed directly as views::
|
||||
|
||||
from django.conf.urls.defaults import *
|
||||
from myproject.feeds import LatestEntries, LatestEntriesByCategory
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# ...
|
||||
(r'^feeds/latest/$', LatestEntries()),
|
||||
(r'^feeds/categories/(?P<category_id>\d+)/$', LatestEntriesByCategory()),
|
||||
# ...
|
||||
)
|
||||
|
||||
If you currently use the ``feed()`` view, the ``LatestEntries`` class
|
||||
would not need to be modified apart from subclassing the new
|
||||
:class:`~django.contrib.syndication.views.Feed` class.
|
||||
|
||||
However, ``LatestEntriesByCategory`` uses the ``get_object()`` method
|
||||
with the ``bits`` argument to specify a specific category to show. In
|
||||
the new :class:`~django.contrib.syndication.views.Feed` class,
|
||||
``get_object()`` method takes a ``request`` and arguments from the
|
||||
URL, so it would look like this::
|
||||
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.shortcuts import get_object_or_404
|
||||
from myproject.models import Category
|
||||
|
||||
class LatestEntriesByCategory(Feed):
|
||||
def get_object(self, request, category_id):
|
||||
return get_object_or_404(Category, id=category_id)
|
||||
|
||||
# ...
|
||||
|
||||
Additionally, the ``get_feed()`` method on ``Feed`` classes now take
|
||||
different arguments, which may impact you if you use the ``Feed``
|
||||
classes directly. Instead of just taking an optional ``url`` argument,
|
||||
it now takes two arguments: the object returned by its own
|
||||
``get_object()`` method, and the current ``request`` object.
|
||||
|
||||
To take into account ``Feed`` classes not being initialized for each
|
||||
request, the ``__init__()`` method now takes no arguments by default.
|
||||
Previously it would have taken the ``slug`` from the URL and the
|
||||
``request`` object.
|
||||
|
||||
In accordance with `RSS best practices`_, RSS feeds will now include
|
||||
an ``atom:link`` element. You may need to update your tests to take
|
||||
this into account.
|
||||
|
||||
For more information, see the full :ref:`syndication framework
|
||||
documentation <ref-contrib-syndication>`.
|
||||
|
||||
.. _RSS best practices: http://www.rssboard.org/rss-profile
|
||||
|
||||
What's new in Django 1.2
|
||||
========================
|
||||
|
||||
|
@ -556,7 +637,7 @@ Object-level permissions
|
|||
A foundation for specifying permissions at the per-object level has been added.
|
||||
Although there is no implementation of this in core, a custom authentication
|
||||
backend can provide this implementation and it will be used by
|
||||
:class:`django.contrib.auth.models.User`. See the :ref:`authentication docs
|
||||
:class:`django.contrib.auth.models.User`. See the :ref:`authentication docs
|
||||
<topics-auth>` for more information.
|
||||
|
||||
Permissions for anonymous users
|
||||
|
@ -568,3 +649,12 @@ User already did. This is useful for centralizing permission handling - apps
|
|||
can always delegate the question of whether something is allowed or not to
|
||||
the authorization/authentication backend. See the :ref:`authentication
|
||||
docs <topics-auth>` for more details.
|
||||
|
||||
Syndication feeds as views
|
||||
--------------------------
|
||||
|
||||
:ref:`Syndication feeds <ref-contrib-syndication>` can now be used directly as
|
||||
views in your :ref:`URLconf <topics-http-urls>`. This means that you can
|
||||
maintain complete control over the URL structure of your feeds. Like any other view, feeds views are passed a ``request`` object, so you can
|
||||
do anything you would normally do with a view, like user based access control,
|
||||
or making a feed a named URL.
|
||||
|
|
|
@ -1,66 +1,142 @@
|
|||
from django.contrib.syndication import feeds, views
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.syndication import feeds
|
||||
from django.utils.feedgenerator import Atom1Feed
|
||||
from django.utils import tzinfo
|
||||
from django.utils import feedgenerator, tzinfo
|
||||
from models import Article, Entry
|
||||
|
||||
class ComplexFeed(feeds.Feed):
|
||||
def get_object(self, bits):
|
||||
if len(bits) != 1:
|
||||
|
||||
class ComplexFeed(views.Feed):
|
||||
def get_object(self, request, foo=None):
|
||||
if foo is not None:
|
||||
raise ObjectDoesNotExist
|
||||
return None
|
||||
|
||||
class TestRssFeed(feeds.Feed):
|
||||
link = "/blog/"
|
||||
|
||||
class TestRss2Feed(views.Feed):
|
||||
title = 'My blog'
|
||||
|
||||
description = 'A more thorough description of my blog.'
|
||||
link = '/blog/'
|
||||
feed_guid = '/foo/bar/1234'
|
||||
author_name = 'Sally Smith'
|
||||
author_email = 'test@example.com'
|
||||
author_link = 'http://www.example.com/'
|
||||
categories = ('python', 'django')
|
||||
feed_copyright = 'Copyright (c) 2007, Sally Smith'
|
||||
ttl = 600
|
||||
|
||||
def items(self):
|
||||
from models import Entry
|
||||
return Entry.objects.all()
|
||||
|
||||
def item_link(self, item):
|
||||
return "/blog/%s/" % item.pk
|
||||
|
||||
class TestAtomFeed(TestRssFeed):
|
||||
feed_type = Atom1Feed
|
||||
def item_description(self, item):
|
||||
return "Overridden description: %s" % item
|
||||
|
||||
class MyCustomAtom1Feed(Atom1Feed):
|
||||
def item_pubdate(self, item):
|
||||
return item.date
|
||||
|
||||
item_author_name = 'Sally Smith'
|
||||
item_author_email = 'test@example.com'
|
||||
item_author_link = 'http://www.example.com/'
|
||||
item_categories = ('python', 'testing')
|
||||
item_copyright = 'Copyright (c) 2007, Sally Smith'
|
||||
|
||||
|
||||
class TestRss091Feed(TestRss2Feed):
|
||||
feed_type = feedgenerator.RssUserland091Feed
|
||||
|
||||
|
||||
class TestAtomFeed(TestRss2Feed):
|
||||
feed_type = feedgenerator.Atom1Feed
|
||||
subtitle = TestRss2Feed.description
|
||||
|
||||
|
||||
class ArticlesFeed(TestRss2Feed):
|
||||
"""
|
||||
Test of a custom feed generator class.
|
||||
"""
|
||||
def root_attributes(self):
|
||||
attrs = super(MyCustomAtom1Feed, self).root_attributes()
|
||||
attrs[u'django'] = u'rocks'
|
||||
return attrs
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
super(MyCustomAtom1Feed, self).add_root_elements(handler)
|
||||
handler.addQuickElement(u'spam', u'eggs')
|
||||
|
||||
def item_attributes(self, item):
|
||||
attrs = super(MyCustomAtom1Feed, self).item_attributes(item)
|
||||
attrs[u'bacon'] = u'yum'
|
||||
return attrs
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
super(MyCustomAtom1Feed, self).add_item_elements(handler, item)
|
||||
handler.addQuickElement(u'ministry', u'silly walks')
|
||||
|
||||
class TestCustomFeed(TestAtomFeed):
|
||||
feed_type = MyCustomAtom1Feed
|
||||
|
||||
A feed to test no link being defined. Articles have no get_absolute_url()
|
||||
method, and item_link() is not defined.
|
||||
"""
|
||||
def items(self):
|
||||
return Article.objects.all()
|
||||
|
||||
|
||||
class TestEnclosureFeed(TestRss2Feed):
|
||||
pass
|
||||
|
||||
|
||||
class TemplateFeed(TestRss2Feed):
|
||||
"""
|
||||
A feed to test defining item titles and descriptions with templates.
|
||||
"""
|
||||
title_template = 'syndication/title.html'
|
||||
description_template = 'syndication/description.html'
|
||||
|
||||
# Defining a template overrides any item_title definition
|
||||
def item_title(self):
|
||||
return "Not in a template"
|
||||
|
||||
|
||||
class NaiveDatesFeed(TestAtomFeed):
|
||||
"""
|
||||
A feed with naive (non-timezone-aware) dates.
|
||||
"""
|
||||
def item_pubdate(self, item):
|
||||
return item.date
|
||||
|
||||
|
||||
|
||||
class TZAwareDatesFeed(TestAtomFeed):
|
||||
"""
|
||||
A feed with timezone-aware dates.
|
||||
"""
|
||||
def item_pubdate(self, item):
|
||||
# Provide a weird offset so that the test can know it's getting this
|
||||
# specific offset and not accidentally getting on from
|
||||
# specific offset and not accidentally getting on from
|
||||
# settings.TIME_ZONE.
|
||||
return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
|
||||
return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
|
||||
|
||||
|
||||
class TestFeedUrlFeed(TestAtomFeed):
|
||||
feed_url = 'http://example.com/customfeedurl/'
|
||||
|
||||
|
||||
class MyCustomAtom1Feed(feedgenerator.Atom1Feed):
|
||||
"""
|
||||
Test of a custom feed generator class.
|
||||
"""
|
||||
def root_attributes(self):
|
||||
attrs = super(MyCustomAtom1Feed, self).root_attributes()
|
||||
attrs[u'django'] = u'rocks'
|
||||
return attrs
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
super(MyCustomAtom1Feed, self).add_root_elements(handler)
|
||||
handler.addQuickElement(u'spam', u'eggs')
|
||||
|
||||
def item_attributes(self, item):
|
||||
attrs = super(MyCustomAtom1Feed, self).item_attributes(item)
|
||||
attrs[u'bacon'] = u'yum'
|
||||
return attrs
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
super(MyCustomAtom1Feed, self).add_item_elements(handler, item)
|
||||
handler.addQuickElement(u'ministry', u'silly walks')
|
||||
|
||||
|
||||
class TestCustomFeed(TestAtomFeed):
|
||||
feed_type = MyCustomAtom1Feed
|
||||
|
||||
|
||||
class DeprecatedComplexFeed(feeds.Feed):
|
||||
def get_object(self, bits):
|
||||
if len(bits) != 1:
|
||||
raise ObjectDoesNotExist
|
||||
return None
|
||||
|
||||
|
||||
class DeprecatedRssFeed(feeds.Feed):
|
||||
link = "/blog/"
|
||||
title = 'My blog'
|
||||
|
||||
def items(self):
|
||||
return Entry.objects.all()
|
||||
|
||||
def item_link(self, item):
|
||||
return "/blog/%s/" % item.pk
|
||||
|
||||
|
|
|
@ -30,5 +30,13 @@
|
|||
"title": "A & B < C > D",
|
||||
"date": "2008-01-03 13:30:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "syndication.article",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"title": "My first article",
|
||||
"entry": "1"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -3,6 +3,21 @@ from django.db import models
|
|||
class Entry(models.Model):
|
||||
title = models.CharField(max_length=200)
|
||||
date = models.DateTimeField()
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ('date',)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "/blog/%s/" % self.pk
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
title = models.CharField(max_length=200)
|
||||
entry = models.ForeignKey(Entry)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Description in your templates: {{ obj }}
|
|
@ -0,0 +1 @@
|
|||
Title in your templates: {{ obj }}
|
|
@ -1,17 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from xml.dom import minidom
|
||||
from django.contrib.syndication import feeds, views
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.utils import tzinfo
|
||||
from models import Entry
|
||||
from xml.dom import minidom
|
||||
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
|
||||
class SyndicationFeedTest(TestCase):
|
||||
class FeedTestCase(TestCase):
|
||||
fixtures = ['feeddata.json']
|
||||
|
||||
def assertChildNodes(self, elem, expected):
|
||||
|
@ -19,101 +19,300 @@ class SyndicationFeedTest(TestCase):
|
|||
expected = set(expected)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_rss_feed(self):
|
||||
response = self.client.get('/syndication/feeds/rss/')
|
||||
def assertChildNodeContent(self, elem, expected):
|
||||
for k, v in expected.items():
|
||||
self.assertEqual(
|
||||
elem.getElementsByTagName(k)[0].firstChild.wholeText, v)
|
||||
|
||||
def assertCategories(self, elem, expected):
|
||||
self.assertEqual(set(i.firstChild.wholeText for i in elem.childNodes if i.nodeName == 'category'), set(expected));
|
||||
|
||||
######################################
|
||||
# Feed view
|
||||
######################################
|
||||
|
||||
class SyndicationFeedTest(FeedTestCase):
|
||||
"""
|
||||
Tests for the high-level syndication feed framework.
|
||||
"""
|
||||
|
||||
def test_rss2_feed(self):
|
||||
"""
|
||||
Test the structure and content of feeds generated by Rss201rev2Feed.
|
||||
"""
|
||||
response = self.client.get('/syndication/rss2/')
|
||||
doc = minidom.parseString(response.content)
|
||||
|
||||
|
||||
# Making sure there's only 1 `rss` element and that the correct
|
||||
# RSS version was specified.
|
||||
feed_elem = doc.getElementsByTagName('rss')
|
||||
self.assertEqual(len(feed_elem), 1)
|
||||
feed = feed_elem[0]
|
||||
self.assertEqual(feed.getAttribute('version'), '2.0')
|
||||
|
||||
|
||||
# Making sure there's only one `channel` element w/in the
|
||||
# `rss` element.
|
||||
chan_elem = feed.getElementsByTagName('channel')
|
||||
self.assertEqual(len(chan_elem), 1)
|
||||
chan = chan_elem[0]
|
||||
self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item'])
|
||||
|
||||
self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category'])
|
||||
self.assertChildNodeContent(chan, {
|
||||
'title': 'My blog',
|
||||
'description': 'A more thorough description of my blog.',
|
||||
'link': 'http://example.com/blog/',
|
||||
'language': 'en',
|
||||
'lastBuildDate': 'Thu, 03 Jan 2008 13:30:00 -0600',
|
||||
#'atom:link': '',
|
||||
'ttl': '600',
|
||||
'copyright': 'Copyright (c) 2007, Sally Smith',
|
||||
})
|
||||
self.assertCategories(chan, ['python', 'django']);
|
||||
|
||||
# Ensure the content of the channel is correct
|
||||
self.assertChildNodeContent(chan, {
|
||||
'title': 'My blog',
|
||||
'link': 'http://example.com/blog/',
|
||||
})
|
||||
|
||||
# Check feed_url is passed
|
||||
self.assertEqual(
|
||||
chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
|
||||
'http://example.com/syndication/rss2/'
|
||||
)
|
||||
|
||||
items = chan.getElementsByTagName('item')
|
||||
self.assertEqual(len(items), Entry.objects.count())
|
||||
self.assertChildNodeContent(items[0], {
|
||||
'title': 'My first entry',
|
||||
'description': 'Overridden description: My first entry',
|
||||
'link': 'http://example.com/blog/1/',
|
||||
'guid': 'http://example.com/blog/1/',
|
||||
'pubDate': 'Tue, 01 Jan 2008 12:30:00 -0600',
|
||||
'author': 'test@example.com (Sally Smith)',
|
||||
})
|
||||
self.assertCategories(items[0], ['python', 'testing']);
|
||||
|
||||
for item in items:
|
||||
self.assertChildNodes(item, ['title', 'link', 'description', 'guid'])
|
||||
|
||||
def test_atom_feed(self):
|
||||
response = self.client.get('/syndication/feeds/atom/')
|
||||
self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'category', 'pubDate', 'author'])
|
||||
|
||||
def test_rss091_feed(self):
|
||||
"""
|
||||
Test the structure and content of feeds generated by RssUserland091Feed.
|
||||
"""
|
||||
response = self.client.get('/syndication/rss091/')
|
||||
doc = minidom.parseString(response.content)
|
||||
|
||||
feed = doc.firstChild
|
||||
|
||||
# Making sure there's only 1 `rss` element and that the correct
|
||||
# RSS version was specified.
|
||||
feed_elem = doc.getElementsByTagName('rss')
|
||||
self.assertEqual(len(feed_elem), 1)
|
||||
feed = feed_elem[0]
|
||||
self.assertEqual(feed.getAttribute('version'), '0.91')
|
||||
|
||||
# Making sure there's only one `channel` element w/in the
|
||||
# `rss` element.
|
||||
chan_elem = feed.getElementsByTagName('channel')
|
||||
self.assertEqual(len(chan_elem), 1)
|
||||
chan = chan_elem[0]
|
||||
self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category'])
|
||||
|
||||
# Ensure the content of the channel is correct
|
||||
self.assertChildNodeContent(chan, {
|
||||
'title': 'My blog',
|
||||
'link': 'http://example.com/blog/',
|
||||
})
|
||||
self.assertCategories(chan, ['python', 'django'])
|
||||
|
||||
# Check feed_url is passed
|
||||
self.assertEqual(
|
||||
chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
|
||||
'http://example.com/syndication/rss091/'
|
||||
)
|
||||
|
||||
items = chan.getElementsByTagName('item')
|
||||
self.assertEqual(len(items), Entry.objects.count())
|
||||
self.assertChildNodeContent(items[0], {
|
||||
'title': 'My first entry',
|
||||
'description': 'Overridden description: My first entry',
|
||||
'link': 'http://example.com/blog/1/',
|
||||
})
|
||||
for item in items:
|
||||
self.assertChildNodes(item, ['title', 'link', 'description'])
|
||||
self.assertCategories(item, [])
|
||||
|
||||
def test_atom_feed(self):
|
||||
"""
|
||||
Test the structure and content of feeds generated by Atom1Feed.
|
||||
"""
|
||||
response = self.client.get('/syndication/atom/')
|
||||
feed = minidom.parseString(response.content).firstChild
|
||||
|
||||
self.assertEqual(feed.nodeName, 'feed')
|
||||
self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom')
|
||||
self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry'])
|
||||
|
||||
self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom')
|
||||
self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'rights', 'category', 'author'])
|
||||
for link in feed.getElementsByTagName('link'):
|
||||
if link.getAttribute('rel') == 'self':
|
||||
self.assertEqual(link.getAttribute('href'), 'http://example.com/syndication/atom/')
|
||||
|
||||
entries = feed.getElementsByTagName('entry')
|
||||
self.assertEqual(len(entries), Entry.objects.count())
|
||||
for entry in entries:
|
||||
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary'])
|
||||
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'category', 'updated', 'rights', 'author'])
|
||||
summary = entry.getElementsByTagName('summary')[0]
|
||||
self.assertEqual(summary.getAttribute('type'), 'html')
|
||||
|
||||
|
||||
def test_custom_feed_generator(self):
|
||||
response = self.client.get('/syndication/feeds/custom/')
|
||||
doc = minidom.parseString(response.content)
|
||||
|
||||
feed = doc.firstChild
|
||||
response = self.client.get('/syndication/custom/')
|
||||
feed = minidom.parseString(response.content).firstChild
|
||||
|
||||
self.assertEqual(feed.nodeName, 'feed')
|
||||
self.assertEqual(feed.getAttribute('django'), 'rocks')
|
||||
self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry', 'spam'])
|
||||
|
||||
self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'spam', 'rights', 'category', 'author'])
|
||||
|
||||
entries = feed.getElementsByTagName('entry')
|
||||
self.assertEqual(len(entries), Entry.objects.count())
|
||||
for entry in entries:
|
||||
self.assertEqual(entry.getAttribute('bacon'), 'yum')
|
||||
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry'])
|
||||
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry', 'rights', 'author', 'updated', 'category'])
|
||||
summary = entry.getElementsByTagName('summary')[0]
|
||||
self.assertEqual(summary.getAttribute('type'), 'html')
|
||||
|
||||
def test_complex_base_url(self):
|
||||
"""
|
||||
Tests that that the base url for a complex feed doesn't raise a 500
|
||||
exception.
|
||||
"""
|
||||
response = self.client.get('/syndication/feeds/complex/')
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_title_escaping(self):
|
||||
"""
|
||||
Tests that titles are escaped correctly in RSS feeds.
|
||||
"""
|
||||
response = self.client.get('/syndication/feeds/rss/')
|
||||
response = self.client.get('/syndication/rss2/')
|
||||
doc = minidom.parseString(response.content)
|
||||
for item in doc.getElementsByTagName('item'):
|
||||
link = item.getElementsByTagName('link')[0]
|
||||
if link.firstChild.wholeText == 'http://example.com/blog/4/':
|
||||
title = item.getElementsByTagName('title')[0]
|
||||
self.assertEquals(title.firstChild.wholeText, u'A & B < C > D')
|
||||
|
||||
|
||||
def test_naive_datetime_conversion(self):
|
||||
"""
|
||||
Test that datetimes are correctly converted to the local time zone.
|
||||
"""
|
||||
# Naive date times passed in get converted to the local time zone, so
|
||||
# check the recived zone offset against the local offset.
|
||||
response = self.client.get('/syndication/feeds/naive-dates/')
|
||||
response = self.client.get('/syndication/naive-dates/')
|
||||
doc = minidom.parseString(response.content)
|
||||
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
|
||||
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
|
||||
tz = tzinfo.LocalTimezone(datetime.datetime.now())
|
||||
now = datetime.datetime.now(tz)
|
||||
self.assertEqual(updated[-6:], str(now)[-6:])
|
||||
|
||||
|
||||
def test_aware_datetime_conversion(self):
|
||||
"""
|
||||
Test that datetimes with timezones don't get trodden on.
|
||||
"""
|
||||
response = self.client.get('/syndication/feeds/aware-dates/')
|
||||
response = self.client.get('/syndication/aware-dates/')
|
||||
doc = minidom.parseString(response.content)
|
||||
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
|
||||
self.assertEqual(updated[-6:], '+00:42')
|
||||
|
||||
|
||||
def test_feed_url(self):
|
||||
"""
|
||||
Test that the feed_url can be overridden.
|
||||
"""
|
||||
response = self.client.get('/syndication/feedurl/')
|
||||
doc = minidom.parseString(response.content)
|
||||
for link in doc.getElementsByTagName('link'):
|
||||
if link.getAttribute('rel') == 'self':
|
||||
self.assertEqual(link.getAttribute('href'), 'http://example.com/customfeedurl/')
|
||||
|
||||
def test_item_link_error(self):
|
||||
"""
|
||||
Test that a ImproperlyConfigured is raised if no link could be found
|
||||
for the item(s).
|
||||
"""
|
||||
self.assertRaises(ImproperlyConfigured,
|
||||
self.client.get,
|
||||
'/syndication/articles/')
|
||||
|
||||
def test_template_feed(self):
|
||||
"""
|
||||
Test that the item title and description can be overridden with
|
||||
templates.
|
||||
"""
|
||||
response = self.client.get('/syndication/template/')
|
||||
doc = minidom.parseString(response.content)
|
||||
feed = doc.getElementsByTagName('rss')[0]
|
||||
chan = feed.getElementsByTagName('channel')[0]
|
||||
items = chan.getElementsByTagName('item')
|
||||
|
||||
self.assertChildNodeContent(items[0], {
|
||||
'title': 'Title in your templates: My first entry',
|
||||
'description': 'Description in your templates: My first entry',
|
||||
'link': 'http://example.com/blog/1/',
|
||||
})
|
||||
|
||||
def test_add_domain(self):
|
||||
"""
|
||||
Test add_domain() prefixes domains onto the correct URLs.
|
||||
"""
|
||||
self.assertEqual(
|
||||
views.add_domain('example.com', '/foo/?arg=value'),
|
||||
'http://example.com/foo/?arg=value'
|
||||
)
|
||||
self.assertEqual(
|
||||
views.add_domain('example.com', 'http://djangoproject.com/doc/'),
|
||||
'http://djangoproject.com/doc/'
|
||||
)
|
||||
self.assertEqual(
|
||||
views.add_domain('example.com', 'https://djangoproject.com/doc/'),
|
||||
'https://djangoproject.com/doc/'
|
||||
)
|
||||
self.assertEqual(
|
||||
views.add_domain('example.com', 'mailto:uhoh@djangoproject.com'),
|
||||
'mailto:uhoh@djangoproject.com'
|
||||
)
|
||||
|
||||
|
||||
######################################
|
||||
# Deprecated feeds
|
||||
######################################
|
||||
|
||||
class DeprecatedSyndicationFeedTest(FeedTestCase):
|
||||
"""
|
||||
Tests for the deprecated API (feed() view and the feed_dict etc).
|
||||
"""
|
||||
|
||||
def test_empty_feed_dict(self):
|
||||
"""
|
||||
Test that an empty feed_dict raises a 404.
|
||||
"""
|
||||
response = self.client.get('/syndication/depr-feeds-empty/aware-dates/')
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_nonexistent_slug(self):
|
||||
"""
|
||||
Test that a non-existent slug raises a 404.
|
||||
"""
|
||||
response = self.client.get('/syndication/depr-feeds/foobar/')
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_rss_feed(self):
|
||||
"""
|
||||
A simple test for Rss201rev2Feed feeds generated by the deprecated
|
||||
system.
|
||||
"""
|
||||
response = self.client.get('/syndication/depr-feeds/rss/')
|
||||
doc = minidom.parseString(response.content)
|
||||
feed = doc.getElementsByTagName('rss')[0]
|
||||
self.assertEqual(feed.getAttribute('version'), '2.0')
|
||||
|
||||
chan = feed.getElementsByTagName('channel')[0]
|
||||
self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link'])
|
||||
|
||||
items = chan.getElementsByTagName('item')
|
||||
self.assertEqual(len(items), Entry.objects.count())
|
||||
|
||||
def test_complex_base_url(self):
|
||||
"""
|
||||
Tests that the base url for a complex feed doesn't raise a 500
|
||||
exception.
|
||||
"""
|
||||
response = self.client.get('/syndication/depr-feeds/complex/')
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
from django.conf.urls.defaults import *
|
||||
|
||||
import feeds
|
||||
from django.conf.urls.defaults import patterns
|
||||
|
||||
feed_dict = {
|
||||
'complex': feeds.ComplexFeed,
|
||||
'rss': feeds.TestRssFeed,
|
||||
'atom': feeds.TestAtomFeed,
|
||||
'custom': feeds.TestCustomFeed,
|
||||
'naive-dates': feeds.NaiveDatesFeed,
|
||||
'aware-dates': feeds.TZAwareDatesFeed,
|
||||
'complex': feeds.DeprecatedComplexFeed,
|
||||
'rss': feeds.DeprecatedRssFeed,
|
||||
}
|
||||
urlpatterns = patterns('',
|
||||
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feed_dict})
|
||||
|
||||
urlpatterns = patterns('django.contrib.syndication.views',
|
||||
(r'^complex/(?P<foo>.*)/$', feeds.ComplexFeed()),
|
||||
(r'^rss2/$', feeds.TestRss2Feed()),
|
||||
(r'^rss091/$', feeds.TestRss091Feed()),
|
||||
(r'^atom/$', feeds.TestAtomFeed()),
|
||||
(r'^custom/$', feeds.TestCustomFeed()),
|
||||
(r'^naive-dates/$', feeds.NaiveDatesFeed()),
|
||||
(r'^aware-dates/$', feeds.TZAwareDatesFeed()),
|
||||
(r'^feedurl/$', feeds.TestFeedUrlFeed()),
|
||||
(r'^articles/$', feeds.ArticlesFeed()),
|
||||
(r'^template/$', feeds.TemplateFeed()),
|
||||
|
||||
(r'^depr-feeds/(?P<url>.*)/$', 'feed', {'feed_dict': feed_dict}),
|
||||
(r'^depr-feeds-empty/(?P<url>.*)/$', 'feed', {'feed_dict': None}),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import datetime
|
||||
from unittest import TestCase
|
||||
|
||||
from django.utils import feedgenerator, tzinfo
|
||||
|
||||
class FeedgeneratorTest(TestCase):
|
||||
"""
|
||||
Tests for the low-level syndication feed framework.
|
||||
"""
|
||||
|
||||
def test_get_tag_uri(self):
|
||||
"""
|
||||
Test get_tag_uri() correctly generates TagURIs.
|
||||
"""
|
||||
self.assertEqual(
|
||||
feedgenerator.get_tag_uri('http://example.org/foo/bar#headline', datetime.date(2004, 10, 25)),
|
||||
u'tag:example.org,2004-10-25:/foo/bar/headline')
|
||||
|
||||
def test_get_tag_uri_with_port(self):
|
||||
"""
|
||||
Test that get_tag_uri() correctly generates TagURIs from URLs with port
|
||||
numbers.
|
||||
"""
|
||||
self.assertEqual(
|
||||
feedgenerator.get_tag_uri('http://www.example.org:8000/2008/11/14/django#headline', datetime.datetime(2008, 11, 14, 13, 37, 0)),
|
||||
u'tag:www.example.org,2008-11-14:/2008/11/14/django/headline')
|
||||
|
||||
def test_rfc2822_date(self):
|
||||
"""
|
||||
Test rfc2822_date() correctly formats datetime objects.
|
||||
"""
|
||||
self.assertEqual(
|
||||
feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0)),
|
||||
"Fri, 14 Nov 2008 13:37:00 -0000"
|
||||
)
|
||||
|
||||
def test_rfc2822_date_with_timezone(self):
|
||||
"""
|
||||
Test rfc2822_date() correctly formats datetime objects with tzinfo.
|
||||
"""
|
||||
self.assertEqual(
|
||||
feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=60)))),
|
||||
"Fri, 14 Nov 2008 13:37:00 +0100"
|
||||
)
|
||||
|
||||
def test_rfc3339_date(self):
|
||||
"""
|
||||
Test rfc3339_date() correctly formats datetime objects.
|
||||
"""
|
||||
self.assertEqual(
|
||||
feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0)),
|
||||
"2008-11-14T13:37:00Z"
|
||||
)
|
||||
|
||||
def test_rfc3339_date_with_timezone(self):
|
||||
"""
|
||||
Test rfc3339_date() correctly formats datetime objects with tzinfo.
|
||||
"""
|
||||
self.assertEqual(
|
||||
feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=120)))),
|
||||
"2008-11-14T13:37:00+02:00"
|
||||
)
|
||||
|
|
@ -31,6 +31,7 @@ __test__ = {
|
|||
}
|
||||
|
||||
from dateformat import *
|
||||
from feedgenerator import *
|
||||
from termcolors import *
|
||||
|
||||
class TestUtilsHtml(TestCase):
|
||||
|
|
Loading…
Reference in New Issue