Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #6188, #6304, #6618, #6969, #8758, #8989, #10334, #11069, #1197…

…3 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
  • Loading branch information...
commit c4c27d8a04c9125cfbc5c3611557d8e5d3845b0d 1 parent 3f68d25
@freakboy3742 freakboy3742 authored
View
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>
View
4 django/contrib/comments/feeds.py
@@ -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 @@ def items(self):
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
View
171 django/contrib/syndication/feeds.py
@@ -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 @@ def get_feed(self, url=None):
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
View
201 django/contrib/syndication/views.py
@@ -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
+
View
28 django/utils/feedgenerator.py
@@ -19,8 +19,8 @@
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 @@ def __init__(self, title, link, description, language=None, author_email=None,
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 @@ def add_item(self, title, link, description, author_email=None,
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 @@ def write(self, outfile, encoding):
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 @@ def add_root_elements(self, handler):
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 @@ def add_item_elements(self, handler, item):
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'))
View
4 docs/internals/deprecation.txt
@@ -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
View
421 docs/ref/contrib/syndication.txt
@@ -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.
+
+These can be matched with a :ref:`URLconf <topics-http-urls>` line such as::
-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.
+ (r'^beats/(?P<beat_id>\d+)/rss/$', BeatFeed()),
-An example makes this clear. Here's the code for these beat-specific feeds::
+Like a view, the arguments in the URL are passed to the :meth:`get_object()`
+method along with the request object.
- from django.contrib.syndication.feeds import FeedDoesNotExist
- from django.core.exceptions import ObjectDoesNotExist
+.. 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]
-
-Here's the basic algorithm the RSS framework follows, given this class and a
-request to the URL :file:`/rss/beats/0613/`:
-
- * 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']``.
-
- * :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.
-
- .. versionadded:: 1.0
- :meth:`get_object()` can handle the :file:`/rss/beats/` url.
-
- 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.
-
- * 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:
-
- * First, it tries to call a method, passing the ``obj`` argument, where
- ``obj`` is the object returned by :meth:`get_object()`.
-
- * Failing that, it tries to call a method with no arguments.
-
- * 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).
+ return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30]
+
+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:
+
+ * First, it tries to call a method, passing the ``obj`` argument, where
+ ``obj`` is the object returned by :meth:`get_object()`.
+
+ * Failing that, it tries to call a method with no arguments.
+
+ * Failing that, it uses the class attribute.
+
+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).
+
+We are using a template for the item descriptions. It can be very simple:
+
+.. code-block:: html+django
+
+ {{ obj.description }}
+
+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.
#
View
92 docs/releases/1.2.txt
@@ -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.
View
152 tests/regressiontests/syndication/feeds.py
@@ -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
+
+ 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):
+ """
+ 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
+ # settings.TIME_ZONE.
+ return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
+
+
+class TestFeedUrlFeed(TestAtomFeed):
+ feed_url = 'http://example.com/customfeedurl/'
+
-class MyCustomAtom1Feed(Atom1Feed):
+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 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
- # settings.TIME_ZONE.
- return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
+
+
+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
+
View
10 tests/regressiontests/syndication/fixtures/feeddata.json
@@ -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"
+ }
}
-]
+]
View
19 tests/regressiontests/syndication/models.py
@@ -3,6 +3,21 @@
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
+
View
1  tests/regressiontests/syndication/templates/syndication/description.html
@@ -0,0 +1 @@
+Description in your templates: {{ obj }}
View
1  tests/regressiontests/syndication/templates/syndication/title.html
@@ -0,0 +1 @@
+Title in your templates: {{ obj }}
View
287 tests/regressiontests/syndication/tests.py
@@ -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 @@ def assertChildNodes(self, elem, expected):
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 &amp; B &lt; C &gt; 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)
+
View
28 tests/regressiontests/syndication/urls.py
@@ -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}),
)
View
63 tests/regressiontests/utils/feedgenerator.py
@@ -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"
+ )
+
View
1  tests/regressiontests/utils/tests.py
@@ -31,6 +31,7 @@
}
from dateformat import *
+from feedgenerator import *
from termcolors import *
class TestUtilsHtml(TestCase):
Please sign in to comment.
Something went wrong with that request. Please try again.