Code

Ticket #12403: syndication-views-5.diff

File syndication-views-5.diff, 75.4 KB (added by bfirsh, 4 years ago)

Applies cleanly against r12313 and improved documentation a little

Line 
1diff --git a/AUTHORS b/AUTHORS
2index 56ab83d..ae6a090 100644
3--- a/AUTHORS
4+++ b/AUTHORS
5@@ -166,6 +166,7 @@ answer newbie questions, and generally made Django that much better:
6     Afonso Fernández Nogueira <fonzzo.django@gmail.com>
7     J. Pablo Fernandez <pupeno@pupeno.com>
8     Maciej Fijalkowski
9+    Ben Firshman <ben@firshman.co.uk>
10     Matthew Flanagan <http://wadofstuff.blogspot.com>
11     Eric Floehr <eric@intellovations.com>
12     Eric Florenzano <floguy@gmail.com>
13diff --git a/django/contrib/comments/feeds.py b/django/contrib/comments/feeds.py
14index 24b10d4..d4269e9 100644
15--- a/django/contrib/comments/feeds.py
16+++ b/django/contrib/comments/feeds.py
17@@ -1,5 +1,5 @@
18 from django.conf import settings
19-from django.contrib.syndication.feeds import Feed
20+from django.contrib.syndication.views import Feed
21 from django.contrib.sites.models import Site
22 from django.contrib import comments
23 from django.utils.translation import ugettext as _
24diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py
25index e5e0877..7a99660 100644
26--- a/django/contrib/syndication/feeds.py
27+++ b/django/contrib/syndication/feeds.py
28@@ -1,82 +1,26 @@
29-from datetime import datetime, timedelta
30+from django.contrib.syndication import views
31+from django.core.exceptions import ObjectDoesNotExist
32+import warnings
33 
34-from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
35-from django.template import loader, Template, TemplateDoesNotExist
36-from django.contrib.sites.models import Site, RequestSite
37-from django.utils import feedgenerator
38-from django.utils.tzinfo import FixedOffset
39-from django.utils.encoding import smart_unicode, iri_to_uri
40-from django.conf import settings         
41-from django.template import RequestContext
42-
43-def add_domain(domain, url):
44-    if not (url.startswith('http://') or url.startswith('https://')):
45-        # 'url' must already be ASCII and URL-quoted, so no need for encoding
46-        # conversions here.
47-        url = iri_to_uri(u'http://%s%s' % (domain, url))
48-    return url
49-
50-class FeedDoesNotExist(ObjectDoesNotExist):
51-    pass
52-
53-class Feed(object):
54-    item_pubdate = None
55-    item_enclosure_url = None
56-    feed_type = feedgenerator.DefaultFeed
57-    feed_url = None
58-    title_template = None
59-    description_template = None
60+# This is part of the deprecated API
61+from django.contrib.syndication.views import FeedDoesNotExist
62 
63+class Feed(views.Feed):
64+    """Provided for backwards compatibility."""
65     def __init__(self, slug, request):
66+        warnings.warn('The syndication feeds.Feed class is deprecated. Please '
67+                      'use the new class based view API.',
68+                      category=PendingDeprecationWarning)
69+       
70         self.slug = slug
71         self.request = request
72-        self.feed_url = self.feed_url or request.path
73-        self.title_template_name = self.title_template or ('feeds/%s_title.html' % slug)
74-        self.description_template_name = self.description_template or ('feeds/%s_description.html' % slug)
75-
76-    def item_link(self, item):
77-        try:
78-            return item.get_absolute_url()
79-        except AttributeError:
80-            raise ImproperlyConfigured("Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__)
81-
82-    def __get_dynamic_attr(self, attname, obj, default=None):
83-        try:
84-            attr = getattr(self, attname)
85-        except AttributeError:
86-            return default
87-        if callable(attr):
88-            # Check func_code.co_argcount rather than try/excepting the
89-            # function and catching the TypeError, because something inside
90-            # the function may raise the TypeError. This technique is more
91-            # accurate.
92-            if hasattr(attr, 'func_code'):
93-                argcount = attr.func_code.co_argcount
94-            else:
95-                argcount = attr.__call__.func_code.co_argcount
96-            if argcount == 2: # one argument is 'self'
97-                return attr(obj)
98-            else:
99-                return attr()
100-        return attr
101-
102-    def feed_extra_kwargs(self, obj):
103-        """
104-        Returns an extra keyword arguments dictionary that is used when
105-        initializing the feed generator.
106-        """
107-        return {}
108-
109-    def item_extra_kwargs(self, item):
110-        """
111-        Returns an extra keyword arguments dictionary that is used with
112-        the `add_item` call of the feed generator.
113-        """
114-        return {}
115-
116+        self.feed_url = getattr(self, 'feed_url', None) or request.path
117+        self.title_template = self.title_template or ('feeds/%s_title.html' % slug)
118+        self.description_template = self.description_template or ('feeds/%s_description.html' % slug)
119+   
120     def get_object(self, bits):
121         return None
122-
123+   
124     def get_feed(self, url=None):
125         """
126         Returns a feedgenerator.DefaultFeed object, fully populated, for
127@@ -86,94 +30,9 @@ class Feed(object):
128             bits = url.split('/')
129         else:
130             bits = []
131-
132         try:
133             obj = self.get_object(bits)
134         except ObjectDoesNotExist:
135             raise FeedDoesNotExist
136+        return super(Feed, self).get_feed(obj, self.request)
137 
138-        if Site._meta.installed:
139-            current_site = Site.objects.get_current()
140-        else:
141-            current_site = RequestSite(self.request)
142-       
143-        link = self.__get_dynamic_attr('link', obj)
144-        link = add_domain(current_site.domain, link)
145-
146-        feed = self.feed_type(
147-            title = self.__get_dynamic_attr('title', obj),
148-            subtitle = self.__get_dynamic_attr('subtitle', obj),
149-            link = link,
150-            description = self.__get_dynamic_attr('description', obj),
151-            language = settings.LANGUAGE_CODE.decode(),
152-            feed_url = add_domain(current_site.domain,
153-                                  self.__get_dynamic_attr('feed_url', obj)),
154-            author_name = self.__get_dynamic_attr('author_name', obj),
155-            author_link = self.__get_dynamic_attr('author_link', obj),
156-            author_email = self.__get_dynamic_attr('author_email', obj),
157-            categories = self.__get_dynamic_attr('categories', obj),
158-            feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
159-            feed_guid = self.__get_dynamic_attr('feed_guid', obj),
160-            ttl = self.__get_dynamic_attr('ttl', obj),
161-            **self.feed_extra_kwargs(obj)
162-        )
163-
164-        try:
165-            title_tmp = loader.get_template(self.title_template_name)
166-        except TemplateDoesNotExist:
167-            title_tmp = Template('{{ obj }}')
168-        try:
169-            description_tmp = loader.get_template(self.description_template_name)
170-        except TemplateDoesNotExist:
171-            description_tmp = Template('{{ obj }}')
172-
173-        for item in self.__get_dynamic_attr('items', obj):
174-            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
175-            enc = None
176-            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
177-            if enc_url:
178-                enc = feedgenerator.Enclosure(
179-                    url = smart_unicode(enc_url),
180-                    length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
181-                    mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
182-                )
183-            author_name = self.__get_dynamic_attr('item_author_name', item)
184-            if author_name is not None:
185-                author_email = self.__get_dynamic_attr('item_author_email', item)
186-                author_link = self.__get_dynamic_attr('item_author_link', item)
187-            else:
188-                author_email = author_link = None
189-
190-            pubdate = self.__get_dynamic_attr('item_pubdate', item)
191-            if pubdate and not pubdate.tzinfo:
192-                now = datetime.now()
193-                utcnow = datetime.utcnow()
194-
195-                # Must always subtract smaller time from larger time here.
196-                if utcnow > now:
197-                    sign = -1
198-                    tzDifference = (utcnow - now)
199-                else:
200-                    sign = 1
201-                    tzDifference = (now - utcnow)
202-
203-                # Round the timezone offset to the nearest half hour.
204-                tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
205-                tzOffset = timedelta(minutes=tzOffsetMinutes)
206-                pubdate = pubdate.replace(tzinfo=FixedOffset(tzOffset))
207-
208-            feed.add_item(
209-                title = title_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
210-                link = link,
211-                description = description_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
212-                unique_id = self.__get_dynamic_attr('item_guid', item, link),
213-                enclosure = enc,
214-                pubdate = pubdate,
215-                author_name = author_name,
216-                author_email = author_email,
217-                author_link = author_link,
218-                categories = self.__get_dynamic_attr('item_categories', item),
219-                item_copyright = self.__get_dynamic_attr('item_copyright', item),
220-                **self.item_extra_kwargs(item)
221-            )
222-        return feed
223diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py
224index d422036..1c43730 100644
225--- a/django/contrib/syndication/views.py
226+++ b/django/contrib/syndication/views.py
227@@ -1,25 +1,220 @@
228-from django.contrib.syndication import feeds
229+import datetime
230+from django.conf import settings
231+from django.contrib.sites.models import Site, RequestSite
232+from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
233 from django.http import HttpResponse, Http404
234+from django.template import loader, Template, TemplateDoesNotExist, RequestContext
235+from django.utils import feedgenerator, tzinfo
236+from django.utils.encoding import force_unicode, iri_to_uri, smart_unicode
237+from django.utils.html import escape
238+
239+def add_domain(domain, url):
240+    if not (url.startswith('http://') or url.startswith('https://')):
241+        # 'url' must already be ASCII and URL-quoted, so no need for encoding
242+        # conversions here.
243+        url = iri_to_uri(u'http://%s%s' % (domain, url))
244+    return url
245+
246+class FeedDoesNotExist(ObjectDoesNotExist):
247+    pass
248+
249+
250+class Feed(object):
251+    feed_type = feedgenerator.DefaultFeed
252+    title_template = None
253+    description_template = None
254+   
255+    def __call__(self, request, *args, **kwargs):
256+        try:
257+            obj = self.get_object(request, *args, **kwargs)
258+        except ObjectDoesNotExist:
259+            raise Http404('Feed object does not exist.')
260+        feedgen = self.get_feed(obj, request)
261+        response = HttpResponse(mimetype=feedgen.mime_type)
262+        feedgen.write(response, 'utf-8')
263+        return response
264+   
265+    def item_title(self, item):
266+        # Titles should be double escaped by default (see #6533)
267+        return escape(force_unicode(item))
268+   
269+    def item_description(self, item):
270+        return force_unicode(item)
271+   
272+    def item_link(self, item):
273+        try:
274+            return item.get_absolute_url()
275+        except AttributeError:
276+            raise ImproperlyConfigured('Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class.' % item.__class__.__name__)
277+   
278+    def __get_dynamic_attr(self, attname, obj, default=None):
279+        try:
280+            attr = getattr(self, attname)
281+        except AttributeError:
282+            return default
283+        if callable(attr):
284+            # Check func_code.co_argcount rather than try/excepting the
285+            # function and catching the TypeError, because something inside
286+            # the function may raise the TypeError. This technique is more
287+            # accurate.
288+            if hasattr(attr, 'func_code'):
289+                argcount = attr.func_code.co_argcount
290+            else:
291+                argcount = attr.__call__.func_code.co_argcount
292+            if argcount == 2: # one argument is 'self'
293+                return attr(obj)
294+            else:
295+                return attr()
296+        return attr
297+   
298+    def feed_extra_kwargs(self, obj):
299+        """
300+        Returns an extra keyword arguments dictionary that is used when
301+        initializing the feed generator.
302+        """
303+        return {}
304+   
305+    def item_extra_kwargs(self, item):
306+        """
307+        Returns an extra keyword arguments dictionary that is used with
308+        the `add_item` call of the feed generator.
309+        """
310+        return {}
311+   
312+    def get_object(self, request, *args, **kwargs):
313+        return None
314+   
315+    def get_feed(self, obj, request):
316+        """
317+        Returns a feedgenerator.DefaultFeed object, fully populated, for
318+        this feed. Raises FeedDoesNotExist for invalid parameters.
319+        """
320+        if Site._meta.installed:
321+            current_site = Site.objects.get_current()
322+        else:
323+            current_site = RequestSite(request)
324+       
325+        link = self.__get_dynamic_attr('link', obj)
326+        link = add_domain(current_site.domain, link)
327+       
328+        feed = self.feed_type(
329+            title = self.__get_dynamic_attr('title', obj),
330+            subtitle = self.__get_dynamic_attr('subtitle', obj),
331+            link = link,
332+            description = self.__get_dynamic_attr('description', obj),
333+            language = settings.LANGUAGE_CODE.decode(),
334+            feed_url = add_domain(current_site.domain,
335+                    self.__get_dynamic_attr('feed_url', obj) or request.path),
336+            author_name = self.__get_dynamic_attr('author_name', obj),
337+            author_link = self.__get_dynamic_attr('author_link', obj),
338+            author_email = self.__get_dynamic_attr('author_email', obj),
339+            categories = self.__get_dynamic_attr('categories', obj),
340+            feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
341+            feed_guid = self.__get_dynamic_attr('feed_guid', obj),
342+            ttl = self.__get_dynamic_attr('ttl', obj),
343+            **self.feed_extra_kwargs(obj)
344+        )
345+       
346+        title_tmp = None
347+        if self.title_template is not None:
348+            try:
349+                title_tmp = loader.get_template(self.title_template)
350+            except TemplateDoesNotExist:
351+                pass
352+       
353+        description_tmp = None
354+        if self.description_template is not None:
355+            try:
356+                description_tmp = loader.get_template(self.description_template)
357+            except TemplateDoesNotExist:
358+                pass
359+       
360+        for item in self.__get_dynamic_attr('items', obj):
361+            if title_tmp is not None:
362+                title = title_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
363+            else:
364+                title = self.__get_dynamic_attr('item_title', item)
365+            if description_tmp is not None:
366+                description = description_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
367+            else:
368+                description = self.__get_dynamic_attr('item_description', item)
369+            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
370+            enc = None
371+            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
372+            if enc_url:
373+                enc = feedgenerator.Enclosure(
374+                    url = smart_unicode(enc_url),
375+                    length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
376+                    mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
377+                )
378+            author_name = self.__get_dynamic_attr('item_author_name', item)
379+            if author_name is not None:
380+                author_email = self.__get_dynamic_attr('item_author_email', item)
381+                author_link = self.__get_dynamic_attr('item_author_link', item)
382+            else:
383+                author_email = author_link = None
384+           
385+            pubdate = self.__get_dynamic_attr('item_pubdate', item)
386+            if pubdate and not pubdate.tzinfo:
387+                now = datetime.datetime.now()
388+                utcnow = datetime.datetime.utcnow()
389+               
390+                # Must always subtract smaller time from larger time here.
391+                if utcnow > now:
392+                    sign = -1
393+                    tzDifference = (utcnow - now)
394+                else:
395+                    sign = 1
396+                    tzDifference = (now - utcnow)
397+
398+                # Round the timezone offset to the nearest half hour.
399+                tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
400+                tzOffset = datetime.timedelta(minutes=tzOffsetMinutes)
401+                pubdate = pubdate.replace(tzinfo=tzinfo.FixedOffset(tzOffset))
402+           
403+            feed.add_item(
404+                title = title,
405+                link = link,
406+                description = description,
407+                unique_id = self.__get_dynamic_attr('item_guid', item, link),
408+                enclosure = enc,
409+                pubdate = pubdate,
410+                author_name = author_name,
411+                author_email = author_email,
412+                author_link = author_link,
413+                categories = self.__get_dynamic_attr('item_categories', item),
414+                item_copyright = self.__get_dynamic_attr('item_copyright', item),
415+                **self.item_extra_kwargs(item)
416+            )
417+        return feed
418+
419 
420 def feed(request, url, feed_dict=None):
421+    """Provided for backwards compatibility."""
422+    import warnings
423+    warnings.warn('The syndication feed() view is deprecated. Please use the '
424+                  'new class based view API.',
425+                  category=PendingDeprecationWarning)
426+   
427     if not feed_dict:
428         raise Http404("No feeds are registered.")
429-
430+   
431     try:
432         slug, param = url.split('/', 1)
433     except ValueError:
434         slug, param = url, ''
435-
436+   
437     try:
438         f = feed_dict[slug]
439     except KeyError:
440         raise Http404("Slug %r isn't registered." % slug)
441-
442+   
443     try:
444         feedgen = f(slug, request).get_feed(param)
445-    except feeds.FeedDoesNotExist:
446+    except FeedDoesNotExist:
447         raise Http404("Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug)
448-
449+   
450     response = HttpResponse(mimetype=feedgen.mime_type)
451     feedgen.write(response, 'utf-8')
452     return response
453+
454diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
455index c9445f9..bb74a8e 100644
456--- a/django/utils/feedgenerator.py
457+++ b/django/utils/feedgenerator.py
458@@ -19,8 +19,8 @@ For definitions of the different versions of RSS, see:
459 http://diveintomark.org/archives/2004/02/04/incompatible-rss
460 """
461 
462-import re
463 import datetime
464+import urlparse
465 from django.utils.xmlutils import SimplerXMLGenerator
466 from django.utils.encoding import force_unicode, iri_to_uri
467 
468@@ -46,12 +46,16 @@ def rfc3339_date(date):
469         return date.strftime('%Y-%m-%dT%H:%M:%SZ')
470 
471 def get_tag_uri(url, date):
472-    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
473-    tag = re.sub('^http://', '', url)
474+    """
475+    Creates a TagURI.
476+   
477+    See http://diveintomark.org/archives/2004/05/28/howto-atom-id
478+    """
479+    url_split = urlparse.urlparse(url)
480+    d = ''
481     if date is not None:
482-        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
483-    tag = re.sub('#', '/', tag)
484-    return u'tag:' + tag
485+        d = ',%s' % date.strftime('%Y-%m-%d')
486+    return u'tag:%s%s:%s/%s' % (url_split.hostname, d, url_split.path, url_split.fragment)
487 
488 class SyndicationFeed(object):
489     "Base class for all syndication feeds. Subclasses should provide write()"
490@@ -61,6 +65,9 @@ class SyndicationFeed(object):
491         to_unicode = lambda s: force_unicode(s, strings_only=True)
492         if categories:
493             categories = [force_unicode(c) for c in categories]
494+        if ttl is not None:
495+            # Force ints to unicode
496+            ttl = force_unicode(ttl)
497         self.feed = {
498             'title': to_unicode(title),
499             'link': iri_to_uri(link),
500@@ -91,6 +98,9 @@ class SyndicationFeed(object):
501         to_unicode = lambda s: force_unicode(s, strings_only=True)
502         if categories:
503             categories = [to_unicode(c) for c in categories]
504+        if ttl is not None:
505+            # Force ints to unicode
506+            ttl = force_unicode(ttl)
507         item = {
508             'title': to_unicode(title),
509             'link': iri_to_uri(link),
510@@ -186,7 +196,8 @@ class RssFeed(SyndicationFeed):
511         handler.endElement(u"rss")
512 
513     def rss_attributes(self):
514-        return {u"version": self._version}
515+        return {u"version": self._version,
516+                u"xmlns:atom": u"http://www.w3.org/2005/Atom"}
517 
518     def write_items(self, handler):
519         for item in self.items:
520@@ -198,6 +209,7 @@ class RssFeed(SyndicationFeed):
521         handler.addQuickElement(u"title", self.feed['title'])
522         handler.addQuickElement(u"link", self.feed['link'])
523         handler.addQuickElement(u"description", self.feed['description'])
524+        handler.addQuickElement(u"atom:link", None, {u"rel": u"self", u"href": self.feed['feed_url']})
525         if self.feed['language'] is not None:
526             handler.addQuickElement(u"language", self.feed['language'])
527         for cat in self.feed['categories']:
528@@ -235,7 +247,7 @@ class Rss201rev2Feed(RssFeed):
529         elif item["author_email"]:
530             handler.addQuickElement(u"author", item["author_email"])
531         elif item["author_name"]:
532-            handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
533+            handler.addQuickElement(u"dc:creator", item["author_name"], {u"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
534 
535         if item['pubdate'] is not None:
536             handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
537diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
538index e2d4b6c..3b03ecc 100644
539--- a/docs/internals/deprecation.txt
540+++ b/docs/internals/deprecation.txt
541@@ -77,6 +77,10 @@ their deprecation, as per the :ref:`Django deprecation policy
542         * The ability to use a function-based test runners will be removed,
543           along with the ``django.test.simple.run_tests()`` test runner.
544 
545+        * The ``views.feed()`` view and ``feeds.Feed`` class in
546+          ``django.contrib.syndication`` have been deprecated since the 1.2
547+          release. The class-based view ``views.Feed`` should be used instead.
548+
549     * 2.0
550         * ``django.views.defaults.shortcut()``. This function has been moved
551           to ``django.contrib.contenttypes.views.shortcut()`` as part of the
552diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt
553index c276663..9b97ed1 100644
554--- a/docs/ref/contrib/syndication.txt
555+++ b/docs/ref/contrib/syndication.txt
556@@ -23,95 +23,68 @@ to generate feeds outside of a Web context, or in some other lower-level way.
557 The high-level framework
558 ========================
559 
560+.. versionchanged:: 1.2
561+
562 Overview
563 --------
564 
565-The high-level feed-generating framework is a view that's hooked to ``/feeds/``
566-by default. Django uses the remainder of the URL (everything after ``/feeds/``)
567-to determine which feed to output.
568-
569-To create a feed, just write a :class:`~django.contrib.syndication.feeds.Feed`
570-class and point to it in your :ref:`URLconf <topics-http-urls>`.
571-
572-Initialization
573---------------
574-
575-To activate syndication feeds on your Django site, add this line to your
576-:ref:`URLconf <topics-http-urls>`::
577-
578-   (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}),
579-
580-This tells Django to use the RSS framework to handle all URLs starting with
581-:file:`"feeds/"`. (You can change that :file:`"feeds/"` prefix to fit your own
582-needs.)
583-
584-This URLconf line has an extra argument: ``{'feed_dict': feeds}``. Use this
585-extra argument to pass the syndication framework the feeds that should be
586-published under that URL.
587-
588-Specifically, :data:`feed_dict` should be a dictionary that maps a feed's slug
589-(short URL label) to its :class:`~django.contrib.syndication.feeds.Feed` class.
590-
591-You can define the ``feed_dict`` in the URLconf itself. Here's a full example
592-URLconf::
593-
594-    from django.conf.urls.defaults import *
595-    from myproject.feeds import LatestEntries, LatestEntriesByCategory
596-
597-    feeds = {
598-        'latest': LatestEntries,
599-        'categories': LatestEntriesByCategory,
600-    }
601-
602-    urlpatterns = patterns('',
603-        # ...
604-        (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
605-            {'feed_dict': feeds}),
606-        # ...
607-    )
608-
609-The above example registers two feeds:
610-
611-    * The feed represented by ``LatestEntries`` will live at ``feeds/latest/``.
612-    * The feed represented by ``LatestEntriesByCategory`` will live at
613-      ``feeds/categories/``.
614-
615-Once that's set up, you just need to define the
616-:class:`~django.contrib.syndication.feeds.Feed` classes themselves.
617+The high-level feed-generating framework is supplied by the
618+:class:`~django.contrib.syndication.views.Feed` class. To create a feed, just write a :class:`~django.contrib.syndication.views.Feed`
619+class and point to an instance of it in your :ref:`URLconf <topics-http-urls>`.
620 
621 Feed classes
622 ------------
623 
624-A :class:`~django.contrib.syndication.feeds.Feed` class is a simple Python class
625+A :class:`~django.contrib.syndication.views.Feed` class is a Python class
626 that represents a syndication feed. A feed can be simple (e.g., a "site news"
627 feed, or a basic feed displaying the latest entries of a blog) or more complex
628 (e.g., a feed displaying all the blog entries in a particular category, where
629 the category is variable).
630 
631-:class:`~django.contrib.syndication.feeds.Feed` classes must subclass
632-``django.contrib.syndication.feeds.Feed``. They can live anywhere in your
633+:class:`~django.contrib.syndication.views.Feed` classes subclass
634+``django.contrib.syndication.views.Feed``. They can live anywhere in your
635 codebase.
636 
637+Instances of :class:`~django.contrib.syndication.views.Feed` classes are views
638+which can be used in your :ref:`URLconf <topics-http-urls>`.
639+
640 A simple example
641 ----------------
642 
643 This simple example, taken from `chicagocrime.org`_, describes a feed of the
644 latest five news items::
645 
646-    from django.contrib.syndication.feeds import Feed
647+    from django.contrib.syndication.views import Feed
648     from chicagocrime.models import NewsItem
649 
650-    class LatestEntries(Feed):
651+    class LatestEntriesFeed(Feed):
652         title = "Chicagocrime.org site news"
653         link = "/sitenews/"
654         description = "Updates on changes and additions to chicagocrime.org."
655-
656+       
657         def items(self):
658             return NewsItem.objects.order_by('-pub_date')[:5]
659+       
660+        def item_title(self, item):
661+            return item.title
662+       
663+        def item_description(self, item):
664+            return item.description
665+
666+To connect a URL to this feed, it needs to be put in your :ref:`URLconf <topics-http-urls>`. Here is a full example::
667+
668+    from django.conf.urls.defaults import *
669+    from myproject.feeds import LatestEntriesFeed
670+
671+    urlpatterns = patterns('',
672+        # ...
673+        (r'^latest/feed/$', LatestEntriesFeed()),
674+        # ...
675+    )
676 
677 Note:
678 
679-* The class subclasses ``django.contrib.syndication.feeds.Feed``.
680+* The class subclasses ``django.contrib.syndication.views.Feed``.
681 
682 * :attr:`title`, :attr:`link` and :attr:`description` correspond to the
683   standard RSS ``<title>``, ``<link>`` and ``<description>`` elements,
684@@ -133,13 +106,19 @@ One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``,
685 ``<link>`` and ``<description>``. We need to tell the framework what data to put
686 into those elements.
687 
688-    * To specify the contents of ``<title>`` and ``<description>``, create
689-      :ref:`Django templates <topics-templates>` called
690-      :file:`feeds/latest_title.html` and
691-      :file:`feeds/latest_description.html`, where :attr:`latest` is the
692-      :attr:`slug` specified in the URLconf for the given feed. Note the
693-      ``.html`` extension is required. The RSS system renders that template for
694-      each item, passing it two template context variables:
695+    * For the contents of ``<title>`` and ``<description>``, Django tries
696+      calling the methods :meth:`item_title()` and :meth:`item_description()` on
697+      the :class:`~django.contrib.syndication.views.Feed` class. They are passed
698+      a single parameter, :attr:`item`, which is the object itself. These are
699+      optional; by default, the unicode representation of the object is used for
700+      both.
701+     
702+      If you want to do any special formatting for either the title or
703+      description, :ref:`Django templates <topics-templates>` can be used
704+      instead. Their paths can be specified with the ``title_template`` and
705+      ``description_template`` attributes on the
706+      :class:`~django.contrib.syndication.views.Feed` class. The templates are
707+      rendered for each item and are passed two template context variables:
708 
709          * ``{{ obj }}`` -- The current object (one of whichever objects you
710            returned in :meth:`items()`).
711@@ -152,152 +131,102 @@ into those elements.
712            :ref:`RequestSite section of the sites framework documentation
713            <requestsite-objects>` for more.
714 
715-      If you don't create a template for either the title or description, the
716-      framework will use the template ``"{{ obj }}"`` by default -- that is, the
717-      normal string representation of the object. You can also change the names
718-      of these two templates by specifying ``title_template`` and
719-      ``description_template`` as attributes of your
720-      :class:`~django.contrib.syndication.feeds.Feed` class.
721+      See `a complex example`_ below that uses a description template.
722 
723     * To specify the contents of ``<link>``, you have two options. For each item
724-      in :meth:`items()`, Django first tries calling a method
725-      :meth:`item_link()` in the :class:`~django.contrib.syndication.feeds.Feed`
726-      class, passing it a single parameter, :attr:`item`, which is the object
727-      itself. If that method doesn't exist, Django tries executing a
728-      ``get_absolute_url()`` method on that object. . Both
729-      ``get_absolute_url()`` and :meth:`item_link()` should return the item's
730-      URL as a normal Python string. As with ``get_absolute_url()``, the result
731-      of :meth:`item_link()` will be included directly in the URL, so you are
732-      responsible for doing all necessary URL quoting and conversion to ASCII
733-      inside the method itself.
734-
735-    * For the LatestEntries example above, we could have very simple feed
736-      templates:
737-
738-      * latest_title.html:
739-
740-        .. code-block:: html+django
741-
742-            {{ obj.title }}
743-
744-      * latest_description.html:
745-
746-        .. code-block:: html+django
747-
748-            {{ obj.description }}
749+      in :meth:`items()`, Django first tries calling the
750+      :meth:`item_link()` method on the
751+      :class:`~django.contrib.syndication.views.Feed` class. In a similar way to
752+      the title and description, it is passed it a single parameter,
753+      :attr:`item`. If that method doesn't exist, Django tries executing a
754+      ``get_absolute_url()`` method on that object. Both
755+      :meth:`get_absolute_url()` and :meth:`item_link()` should return the
756+      item's URL as a normal Python string. As with ``get_absolute_url()``, the
757+      result of :meth:`item_link()` will be included directly in the URL, so you
758+      are responsible for doing all necessary URL quoting and conversion to
759+      ASCII inside the method itself.
760 
761 .. _chicagocrime.org: http://www.chicagocrime.org/
762 
763 A complex example
764 -----------------
765 
766-The framework also supports more complex feeds, via parameters.
767+The framework also supports more complex feeds, via arguments.
768 
769 For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every
770 police beat in Chicago. It'd be silly to create a separate
771-:class:`~django.contrib.syndication.feeds.Feed` class for each police beat; that
772+:class:`~django.contrib.syndication.views.Feed` class for each police beat; that
773 would violate the :ref:`DRY principle <dry>` and would couple data to
774-programming logic. Instead, the syndication framework lets you make generic
775-feeds that output items based on information in the feed's URL.
776+programming logic. Instead, the syndication framework lets you access the
777+arguments passed from your :ref:`URLconf <topics-http-urls>` so feeds can output
778+items based on information in the feed's URL.
779 
780 On chicagocrime.org, the police-beat feeds are accessible via URLs like this:
781 
782-    * :file:`/rss/beats/0613/` -- Returns recent crimes for beat 0613.
783-    * :file:`/rss/beats/1424/` -- Returns recent crimes for beat 1424.
784+    * :file:`/beats/613/rss/` -- Returns recent crimes for beat 613.
785+    * :file:`/beats/1424/rss/` -- Returns recent crimes for beat 1424.
786+
787+These can be matched with a :ref:`URLconf <topics-http-urls>` line such as::
788 
789-The slug here is ``"beats"``. The syndication framework sees the extra URL bits
790-after the slug -- ``0613`` and ``1424`` -- and gives you a hook to tell it what
791-those URL bits mean, and how they should influence which items get published in
792-the feed.
793+    (r'^beats/(?P<beat_id>\d+)/rss/$', BeatFeed()),
794 
795-An example makes this clear. Here's the code for these beat-specific feeds::
796+.. versionchanged:: 1.2
797+   Instead of just taking the ``bits`` argument, ``get_object()`` now takes a
798+   ``request`` object and the URL arguments.
799 
800-    from django.contrib.syndication.feeds import FeedDoesNotExist
801-    from django.core.exceptions import ObjectDoesNotExist
802+Like a view, the arguments in the URL are passed to the :meth:`get_object()`
803+method along with the request object. Here's the code for these beat-specific
804+feeds::
805+
806+    from django.contrib.syndication.views import FeedDoesNotExist
807+    from django.shortcuts import get_object_or_404
808 
809     class BeatFeed(Feed):
810-        def get_object(self, bits):
811-            # In case of "/rss/beats/0613/foo/bar/baz/", or other such clutter,
812-            # check that bits has only one member.
813-            if len(bits) != 1:
814-                raise ObjectDoesNotExist
815-            return Beat.objects.get(beat__exact=bits[0])
816+        description_template = 'feeds/beat_description.html'
817+   
818+        def get_object(self, request, beat_id):
819+            return get_object_or_404(Beat, pk=beat_id)
820 
821         def title(self, obj):
822             return "Chicagocrime.org: Crimes for beat %s" % obj.beat
823 
824         def link(self, obj):
825-            if not obj:
826-                raise FeedDoesNotExist
827             return obj.get_absolute_url()
828 
829         def description(self, obj):
830             return "Crimes recently reported in police beat %s" % obj.beat
831 
832         def items(self, obj):
833-           return Crime.objects.filter(beat__id__exact=obj.id).order_by('-crime_date')[:30]
834-
835-Here's the basic algorithm the RSS framework follows, given this class and a
836-request to the URL :file:`/rss/beats/0613/`:
837-
838-    * The framework gets the URL :file:`/rss/beats/0613/` and notices there's an
839-      extra bit of URL after the slug. It splits that remaining string by the
840-      slash character (``"/"``) and calls the
841-      :class:`~django.contrib.syndication.feeds.Feed` class'
842-      :meth:`get_object()` method, passing it the bits. In this case, bits is
843-      ``['0613']``. For a request to :file:`/rss/beats/0613/foo/bar/`, bits
844-      would be ``['0613', 'foo', 'bar']``.
845-
846-    * :meth:`get_object()` is responsible for retrieving the given beat, from
847-      the given ``bits``. In this case, it uses the Django database API to
848-      retrieve the beat. Note that :meth:`get_object()` should raise
849-      :exc:`django.core.exceptions.ObjectDoesNotExist` if given invalid
850-      parameters. There's no ``try``/``except`` around the
851-      ``Beat.objects.get()`` call, because it's not necessary; that function
852-      raises :exc:`Beat.DoesNotExist` on failure, and :exc:`Beat.DoesNotExist`
853-      is a subclass of :exc:`ObjectDoesNotExist`. Raising
854-      :exc:`ObjectDoesNotExist` in :meth:`get_object()` tells Django to produce
855-      a 404 error for that request.
856-
857-      .. versionadded:: 1.0
858-         :meth:`get_object()` can handle the :file:`/rss/beats/` url.
859-
860-      The :meth:`get_object()` method also has a chance to handle the
861-      :file:`/rss/beats/` url. In this case, :data:`bits` will be an
862-      empty list. In our example, ``len(bits) != 1`` and an
863-      :exc:`ObjectDoesNotExist` exception will be raised, so
864-      :file:`/rss/beats/` will generate a 404 page. But you can handle this case
865-      however you like. For example, you could generate a combined feed for all
866-      beats.
867-
868-    * To generate the feed's ``<title>``, ``<link>`` and ``<description>``,
869-      Django uses the :meth:`title()`, :meth:`link()` and :meth:`description()`
870-      methods. In the previous example, they were simple string class
871-      attributes, but this example illustrates that they can be either strings
872-      *or* methods. For each of :attr:`title`, :attr:`link` and
873-      :attr:`description`, Django follows this algorithm:
874-
875-        * First, it tries to call a method, passing the ``obj`` argument, where
876-          ``obj`` is the object returned by :meth:`get_object()`.
877-
878-        * Failing that, it tries to call a method with no arguments.
879-
880-        * Failing that, it uses the class attribute.
881-
882-      Inside the :meth:`link()` method, we handle the possibility that ``obj``
883-      might be ``None``, which can occur when the URL isn't fully specified. In
884-      some cases, you might want to do something else in this case, which would
885-      mean you'd need to check for ``obj`` existing in other methods as well.
886-      (The :meth:`link()` method is called very early in the feed generation
887-      process, so it's a good place to bail out early.)
888-
889-    * Finally, note that :meth:`items()` in this example also takes the ``obj``
890-      argument. The algorithm for :attr:`items` is the same as described in the
891-      previous step -- first, it tries :meth:`items(obj)`, then :meth:`items()`,
892-      then finally an :attr:`items` class attribute (which should be a list).
893+            return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30]
894+
895+To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django
896+uses the :meth:`title()`, :meth:`link()` and :meth:`description()` methods. In
897+the previous example, they were simple string class attributes, but this example
898+illustrates that they can be either strings *or* methods. For each of
899+:attr:`title`, :attr:`link` and :attr:`description`, Django follows this
900+algorithm:
901+
902+    * First, it tries to call a method, passing the ``obj`` argument, where
903+      ``obj`` is the object returned by :meth:`get_object()`.
904+
905+    * Failing that, it tries to call a method with no arguments.
906+
907+    * Failing that, it uses the class attribute.
908+
909+Also note that :meth:`items()` also follows the same algorithm -- first, it
910+tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items`
911+class attribute (which should be a list).
912+
913+We are using a template for the item descriptions. It can be very simple:
914+
915+.. code-block:: html+django
916+
917+    {{ obj.description }}
918+
919+However, you are free to add formatting as required.
920 
921 The ``ExampleFeed`` class below gives full documentation on methods and
922-attributes of :class:`~django.contrib.syndication.feeds.Feed` classes.
923+attributes of :class:`~django.contrib.syndication.views.Feed` classes.
924 
925 Specifying the type of feed
926 ---------------------------
927@@ -305,7 +234,7 @@ Specifying the type of feed
928 By default, feeds produced in this framework use RSS 2.0.
929 
930 To change that, add a ``feed_type`` attribute to your
931-:class:`~django.contrib.syndication.feeds.Feed` class, like so::
932+:class:`~django.contrib.syndication.views.Feed` class, like so::
933 
934     from django.utils.feedgenerator import Atom1Feed
935 
936@@ -353,13 +282,13 @@ Publishing Atom and RSS feeds in tandem
937 
938 Some developers like to make available both Atom *and* RSS versions of their
939 feeds. That's easy to do with Django: Just create a subclass of your
940-:class:`~django.contrib.syndication.feeds.Feed`
941+:class:`~django.contrib.syndication.views.Feed`
942 class and set the :attr:`feed_type` to something different. Then update your
943 URLconf to add the extra versions.
944 
945 Here's a full example::
946 
947-    from django.contrib.syndication.feeds import Feed
948+    from django.contrib.syndication.views import Feed
949     from chicagocrime.models import NewsItem
950     from django.utils.feedgenerator import Atom1Feed
951 
952@@ -381,7 +310,7 @@ Here's a full example::
953     a feed-level "description," but they *do* provide for a "subtitle."
954 
955     If you provide a :attr:`description` in your
956-    :class:`~django.contrib.syndication.feeds.Feed` class, Django will *not*
957+    :class:`~django.contrib.syndication.views.Feed` class, Django will *not*
958     automatically put that into the :attr:`subtitle` element, because a
959     subtitle and description are not necessarily the same thing. Instead, you
960     should define a :attr:`subtitle` attribute.
961@@ -394,27 +323,22 @@ And the accompanying URLconf::
962     from django.conf.urls.defaults import *
963     from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
964 
965-    feeds = {
966-        'rss': RssSiteNewsFeed,
967-        'atom': AtomSiteNewsFeed,
968-    }
969-
970     urlpatterns = patterns('',
971         # ...
972-        (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
973-            {'feed_dict': feeds}),
974+        (r'^sitenews/rss/$', RssSiteNewsFeed()),
975+        (r'^sitenews/atom/$', AtomSiteNewsFeed()),
976         # ...
977     )
978 
979 Feed class reference
980 --------------------
981 
982-.. class:: django.contrib.syndication.feeds.Feed
983+.. class:: django.contrib.syndication.views.Feed
984 
985 This example illustrates all possible attributes and methods for a
986-:class:`~django.contrib.syndication.feeds.Feed` class::
987+:class:`~django.contrib.syndication.views.Feed` class::
988 
989-    from django.contrib.syndication.feeds import Feed
990+    from django.contrib.syndication.views import Feed
991     from django.utils import feedgenerator
992 
993     class ExampleFeed(Feed):
994@@ -430,9 +354,8 @@ This example illustrates all possible attributes and methods for a
995         # TEMPLATE NAMES -- Optional. These should be strings representing
996         # names of Django templates that the system should use in rendering the
997         # title and description of your feed items. Both are optional.
998-        # If you don't specify one, or either, Django will use the template
999-        # 'feeds/SLUG_title.html' and 'feeds/SLUG_description.html', where SLUG
1000-        # is the slug you specify in the URL.
1001+        # If one is not specified, the item_title() or item_description()
1002+        # methods are used instead.
1003 
1004         title_template = None
1005         description_template = None
1006@@ -572,18 +495,18 @@ This example illustrates all possible attributes and methods for a
1007         # COPYRIGHT NOTICE -- One of the following three is optional. The
1008         # framework looks for them in this order.
1009 
1010-        def copyright(self, obj):
1011+        def feed_copyright(self, obj):
1012             """
1013             Takes the object returned by get_object() and returns the feed's
1014             copyright notice as a normal Python string.
1015             """
1016 
1017-        def copyright(self):
1018+        def feed_copyright(self):
1019             """
1020             Returns the feed's copyright notice as a normal Python string.
1021             """
1022 
1023-        copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
1024+        feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
1025 
1026         # TTL -- One of the following three is optional. The framework looks
1027         # for them in this order. Ignored for Atom feeds.
1028@@ -620,13 +543,44 @@ This example illustrates all possible attributes and methods for a
1029         # GET_OBJECT -- This is required for feeds that publish different data
1030         # for different URL parameters. (See "A complex example" above.)
1031 
1032-        def get_object(self, bits):
1033+        def get_object(self, request, *args, **kwargs):
1034             """
1035-            Takes a list of strings gleaned from the URL and returns an object
1036-            represented by this feed. Raises
1037+            Takes the current request and the arguments from the URL, and
1038+            returns an object represented by this feed. Raises
1039             django.core.exceptions.ObjectDoesNotExist on error.
1040             """
1041+       
1042+        # ITEM TITLE AND DESCRIPTION -- If title_template or
1043+        # description_template are not defined, these are used instead. Both are
1044+        # optional, by default they will use the unicode representation of the
1045+        # item.
1046+       
1047+        def item_title(self, item):
1048+            """
1049+            Takes an item, as returned by items(), and returns the item's
1050+            title as a normal Python string.
1051+            """
1052+
1053+        def item_title(self):
1054+            """
1055+            Returns the title for every item in the feed.
1056+            """
1057+
1058+        item_title = 'Breaking News: Nothing Happening' # Hard-coded title.
1059+       
1060+        def item_description(self, item):
1061+            """
1062+            Takes an item, as returned by items(), and returns the item's
1063+            description as a normal Python string.
1064+            """
1065+
1066+        def item_description(self):
1067+            """
1068+            Returns the description for every item in the feed.
1069+            """
1070 
1071+        item_description = 'A description of the item.' # Hard-coded description.
1072+       
1073         # ITEM LINK -- One of these three is required. The framework looks for
1074         # them in this order.
1075 
1076@@ -686,7 +640,7 @@ This example illustrates all possible attributes and methods for a
1077 
1078         item_author_email = 'test@example.com' # Hard-coded author e-mail.
1079 
1080-        # ITEM AUTHOR LINK --One of the following three is optional. The
1081+        # ITEM AUTHOR LINK -- One of the following three is optional. The
1082         # framework looks for them in this order. In each case, the URL should
1083         # include the "http://" and domain name.
1084         #
1085diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt
1086index f83e4dc..1b08486 100644
1087--- a/docs/releases/1.2.txt
1088+++ b/docs/releases/1.2.txt
1089@@ -386,6 +386,84 @@ approach. Old style function-based test runners will still work, but
1090 should be updated to use the new :ref:`class-based runners
1091 <topics-testing-test_runner>`.
1092 
1093+``Feed`` in ``django.contrib.syndication.feeds``
1094+------------------------------------------------
1095+
1096+The ``Feed`` class in ``syndication.feeds`` has been replaced by
1097+the ``Feed`` class in ``syndication.views``, and will be removed in Django 1.4.
1098+
1099+The new class has an almost identical API, but allows instances to be used as
1100+views. For example, consider the use of the old framework in the following
1101+:ref:`URLconf <topics-http-urls>`::
1102+
1103+    from django.conf.urls.defaults import *
1104+    from myproject.feeds import LatestEntries, LatestEntriesByCategory
1105+
1106+    feeds = {
1107+        'latest': LatestEntries,
1108+        'categories': LatestEntriesByCategory,
1109+    }
1110+
1111+    urlpatterns = patterns('',
1112+        # ...
1113+        (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
1114+            {'feed_dict': feeds}),
1115+        # ...
1116+    )
1117+
1118+This should be replaced by::
1119+
1120+    from django.conf.urls.defaults import *
1121+    from myproject.feeds import LatestEntries, LatestEntriesByCategory
1122+
1123+    urlpatterns = patterns('',
1124+        # ...
1125+        (r'^feeds/latest/$', LatestEntries()),
1126+        (r'^feeds/categories/(?P<category_id>\d+)/$', LatestEntriesByCategory()),
1127+        # ...
1128+    )
1129+
1130+If you used the ``feed()`` view, the ``LatestEntries`` class would not need to
1131+be modified apart from subclassing the new ``Feed`` class. However,
1132+``LatestEntriesByCategory`` used the ``get_object()`` method with the ``bits``
1133+argument to specify a specific category to show. Much like a view, the
1134+``get_object()`` method now takes a ``request`` and arguments from the URL, so
1135+it would look like this::
1136+
1137+    from django.contrib.syndication.views import Feed
1138+    from django.shortcuts import get_object_or_404
1139+    from myproject.models import Category
1140+
1141+    class LatestEntriesByCategory(Feed):
1142+        def get_object(self, request, category_id):
1143+            return get_object_or_404(Category, id=category_id)
1144+       
1145+        # ...
1146+
1147+Additionally, the ``get_feed()`` method on ``Feed`` classes now take different
1148+arguments, which may impact you if you use the ``Feed`` classes directly.
1149+Instead of just taking an optional ``url`` argument, it now takes two arguments:
1150+the object returned by its own ``get_object()`` method, and the current
1151+``request`` object.
1152+
1153+To take into account ``Feed`` classes not being initialized for each
1154+request, the ``__init__()`` method now takes no arguments by default.
1155+Previously it would have taken the ``slug`` from the URL and the ``request``
1156+object.
1157+
1158+In previous versions of Django, the ``feed_copyright()`` method was incorrectly 
1159+documented as ``copyright()``. Change any ``copyright()`` methods you have on
1160+your ``Feed`` classes to ``feed_copyright()``.
1161+
1162+In accordance with `RSS best practices`_, RSS feeds will now include an
1163+``atom:link`` element. You may need to update your tests to take this into
1164+account.
1165+
1166+For more information, see the full :ref:`syndication framework documentation
1167+<ref-contrib-syndication>`.
1168+
1169+.. _RSS best practices: http://www.rssboard.org/rss-profile
1170+
1171 What's new in Django 1.2
1172 ========================
1173 
1174@@ -549,3 +627,23 @@ reusable, encapsulated validation behavior. Note, however, that
1175 validation must still be performed explicitly. Simply invoking a model
1176 instance's ``save()`` method will not perform any validation of the
1177 instance's data.
1178+
1179+Syndication feeds as views
1180+--------------------------
1181+
1182+:ref:`Syndication feeds <ref-contrib-syndication>` can now be used directly as
1183+views in your :ref:`URLconf <topics-http-urls>`. Previously, all the feeds on a
1184+site had to live beneath a single URL, which produced ugly, unnatural URLs.
1185+
1186+For example, suppose you listed blog posts for the "django" tag under
1187+``/blog/django/``. In previous versions of Django, a feed for those posts would
1188+have had to be something like ``/feeds/blog-tag/django/``. In Django 1.2, it
1189+can simply be ``/blog/django/feed/``.
1190+
1191+Like any other view, feeds are now passed a ``request`` object, so you can
1192+do user based access control amongst other things.
1193+
1194+Also, you no longer need to create templates for your feed item titles and
1195+descriptions. You can set these with the ``item_title()`` and
1196+``item_description()`` methods.
1197+
1198diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py
1199index 79837f9..8648cec 100644
1200--- a/tests/regressiontests/syndication/feeds.py
1201+++ b/tests/regressiontests/syndication/feeds.py
1202@@ -1,29 +1,102 @@
1203+from django.contrib.syndication import feeds, views
1204 from django.core.exceptions import ObjectDoesNotExist
1205-from django.contrib.syndication import feeds
1206-from django.utils.feedgenerator import Atom1Feed
1207-from django.utils import tzinfo
1208+from django.utils import feedgenerator, tzinfo
1209+from models import Article, Entry
1210 
1211-class ComplexFeed(feeds.Feed):
1212-    def get_object(self, bits):
1213-        if len(bits) != 1:
1214+
1215+class ComplexFeed(views.Feed):
1216+    def get_object(self, request, foo=None):
1217+        if foo is not None:
1218             raise ObjectDoesNotExist
1219         return None
1220 
1221-class TestRssFeed(feeds.Feed):
1222-    link = "/blog/"
1223+
1224+class TestRss2Feed(views.Feed):
1225     title = 'My blog'
1226+    description = 'A more thorough description of my blog.'
1227+    link = '/blog/'
1228+    feed_guid = '/foo/bar/1234'
1229+    author_name = 'Sally Smith'
1230+    author_email = 'test@example.com'
1231+    author_link = 'http://www.example.com/'
1232+    categories = ('python', 'django')
1233+    feed_copyright = 'Copyright (c) 2007, Sally Smith'
1234+    ttl = 600
1235     
1236     def items(self):
1237-        from models import Entry
1238         return Entry.objects.all()
1239-       
1240-    def item_link(self, item):
1241-        return "/blog/%s/" % item.pk
1242+   
1243+    def item_description(self, item):
1244+        return "Overridden description: %s" % item
1245+   
1246+    def item_pubdate(self, item):
1247+        return item.date
1248+   
1249+    item_author_name = 'Sally Smith'
1250+    item_author_email = 'test@example.com'
1251+    item_author_link = 'http://www.example.com/'
1252+    item_categories = ('python', 'testing')
1253+    item_copyright = 'Copyright (c) 2007, Sally Smith'
1254+
1255+
1256+class TestRss091Feed(TestRss2Feed):
1257+    feed_type = feedgenerator.RssUserland091Feed
1258+
1259 
1260-class TestAtomFeed(TestRssFeed):
1261-    feed_type = Atom1Feed
1262+class TestAtomFeed(TestRss2Feed):
1263+    feed_type = feedgenerator.Atom1Feed
1264+    subtitle = TestRss2Feed.description
1265 
1266-class MyCustomAtom1Feed(Atom1Feed):
1267+
1268+class ArticlesFeed(TestRss2Feed):
1269+    """
1270+    A feed to test no link being defined. Articles have no get_absolute_url()
1271+    method, and item_link() is not defined.
1272+    """
1273+    def items(self):
1274+        return Article.objects.all()
1275+
1276+
1277+class TestEnclosureFeed(TestRss2Feed):
1278+    pass
1279+
1280+
1281+class TemplateFeed(TestRss2Feed):
1282+    """
1283+    A feed to test defining item titles and descriptions with templates.
1284+    """
1285+    title_template = 'syndication/title.html'
1286+    description_template = 'syndication/description.html'
1287+   
1288+    # Defining a template overrides any item_title definition
1289+    def item_title(self):
1290+        return "Not in a template"
1291+
1292+
1293+class NaiveDatesFeed(TestAtomFeed):
1294+    """
1295+    A feed with naive (non-timezone-aware) dates.
1296+    """
1297+    def item_pubdate(self, item):
1298+        return item.date
1299+
1300+
1301+class TZAwareDatesFeed(TestAtomFeed):
1302+    """
1303+    A feed with timezone-aware dates.
1304+    """
1305+    def item_pubdate(self, item):
1306+        # Provide a weird offset so that the test can know it's getting this
1307+        # specific offset and not accidentally getting on from
1308+        # settings.TIME_ZONE.
1309+        return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
1310+
1311+
1312+class TestFeedUrlFeed(TestAtomFeed):
1313+    feed_url = 'http://example.com/customfeedurl/'
1314+
1315+
1316+class MyCustomAtom1Feed(feedgenerator.Atom1Feed):
1317     """
1318     Test of a custom feed generator class.
1319     """   
1320@@ -44,23 +117,26 @@ class MyCustomAtom1Feed(Atom1Feed):
1321     def add_item_elements(self, handler, item):
1322         super(MyCustomAtom1Feed, self).add_item_elements(handler, item)
1323         handler.addQuickElement(u'ministry', u'silly walks')
1324-   
1325+
1326+
1327 class TestCustomFeed(TestAtomFeed):
1328     feed_type = MyCustomAtom1Feed
1329+
1330+
1331+class DeprecatedComplexFeed(feeds.Feed):
1332+    def get_object(self, bits):
1333+        if len(bits) != 1:
1334+            raise ObjectDoesNotExist
1335+        return None
1336+
1337+
1338+class DeprecatedRssFeed(feeds.Feed):
1339+    link = "/blog/"
1340+    title = 'My blog'
1341     
1342-class NaiveDatesFeed(TestAtomFeed):
1343-    """
1344-    A feed with naive (non-timezone-aware) dates.
1345-    """
1346-    def item_pubdate(self, item):
1347-        return item.date
1348+    def items(self):
1349+        return Entry.objects.all()
1350         
1351-class TZAwareDatesFeed(TestAtomFeed):
1352-    """
1353-    A feed with timezone-aware dates.
1354-    """
1355-    def item_pubdate(self, item):
1356-        # Provide a weird offset so that the test can know it's getting this
1357-        # specific offset and not accidentally getting on from
1358-        # settings.TIME_ZONE.
1359-        return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
1360\ No newline at end of file
1361+    def item_link(self, item):
1362+        return "/blog/%s/" % item.pk
1363+
1364diff --git a/tests/regressiontests/syndication/fixtures/feeddata.json b/tests/regressiontests/syndication/fixtures/feeddata.json
1365index 375ee16..4a5c022 100644
1366--- a/tests/regressiontests/syndication/fixtures/feeddata.json
1367+++ b/tests/regressiontests/syndication/fixtures/feeddata.json
1368@@ -30,5 +30,13 @@
1369       "title": "A & B < C > D",
1370       "date": "2008-01-03 13:30:00"
1371     }
1372+  },
1373+  {
1374+    "model": "syndication.article",
1375+    "pk": 1,
1376+    "fields": {
1377+      "title": "My first article",
1378+      "entry": "1"
1379+    }
1380   }
1381-]
1382\ No newline at end of file
1383+]
1384diff --git a/tests/regressiontests/syndication/models.py b/tests/regressiontests/syndication/models.py
1385index 99e14ad..19e645a 100644
1386--- a/tests/regressiontests/syndication/models.py
1387+++ b/tests/regressiontests/syndication/models.py
1388@@ -1,8 +1,23 @@
1389 from django.db import models
1390-
1391+
1392 class Entry(models.Model):
1393     title = models.CharField(max_length=200)
1394     date = models.DateTimeField()
1395     
1396+    class Meta:
1397+        ordering = ('date',)
1398+   
1399     def __unicode__(self):
1400-        return self.title
1401\ No newline at end of file
1402+        return self.title
1403+   
1404+    def get_absolute_url(self):
1405+        return "/blog/%s/" % self.pk
1406+
1407+
1408+class Article(models.Model):
1409+    title = models.CharField(max_length=200)
1410+    entry = models.ForeignKey(Entry)
1411+
1412+    def __unicode__(self):
1413+        return self.title
1414+
1415diff --git a/tests/regressiontests/syndication/templates/syndication/description.html b/tests/regressiontests/syndication/templates/syndication/description.html
1416new file mode 100644
1417index 0000000..85ec82c
1418--- /dev/null
1419+++ b/tests/regressiontests/syndication/templates/syndication/description.html
1420@@ -0,0 +1 @@
1421+Description in your templates: {{ obj }}
1422\ No newline at end of file
1423diff --git a/tests/regressiontests/syndication/templates/syndication/title.html b/tests/regressiontests/syndication/templates/syndication/title.html
1424new file mode 100644
1425index 0000000..eb17969
1426--- /dev/null
1427+++ b/tests/regressiontests/syndication/templates/syndication/title.html
1428@@ -0,0 +1 @@
1429+Title in your templates: {{ obj }}
1430\ No newline at end of file
1431diff --git a/tests/regressiontests/syndication/tests.py b/tests/regressiontests/syndication/tests.py
1432index 816cb44..a35e6e2 100644
1433--- a/tests/regressiontests/syndication/tests.py
1434+++ b/tests/regressiontests/syndication/tests.py
1435@@ -1,26 +1,46 @@
1436-# -*- coding: utf-8 -*-
1437-
1438 import datetime
1439-from xml.dom import minidom
1440+from django.contrib.syndication import feeds, views
1441+from django.core.exceptions import ImproperlyConfigured
1442 from django.test import TestCase
1443-from django.test.client import Client
1444 from django.utils import tzinfo
1445 from models import Entry
1446+from xml.dom import minidom
1447+
1448 try:
1449     set
1450 except NameError:
1451     from sets import Set as set
1452 
1453-class SyndicationFeedTest(TestCase):
1454+class FeedTestCase(TestCase):
1455     fixtures = ['feeddata.json']
1456 
1457     def assertChildNodes(self, elem, expected):
1458         actual = set([n.nodeName for n in elem.childNodes])
1459         expected = set(expected)
1460         self.assertEqual(actual, expected)
1461+   
1462+    def assertChildNodeContent(self, elem, expected):
1463+        for k, v in expected.items():
1464+            self.assertEqual(
1465+                elem.getElementsByTagName(k)[0].firstChild.wholeText, v)
1466+   
1467+    def assertCategories(self, elem, expected):
1468+        self.assertEqual(set(i.firstChild.wholeText for i in elem.childNodes if i.nodeName == 'category'), set(expected));
1469 
1470-    def test_rss_feed(self):
1471-        response = self.client.get('/syndication/feeds/rss/')
1472+######################################
1473+# Feed view
1474+######################################
1475+
1476+class SyndicationFeedTest(FeedTestCase):
1477+    """
1478+    Tests for the high-level syndication feed framework.
1479+    """
1480+   
1481+    def test_rss2_feed(self):
1482+        """
1483+        Test the structure and content of feeds generated by Rss201rev2Feed.
1484+        """
1485+        response = self.client.get('/syndication/rss2/')
1486         doc = minidom.parseString(response.content)
1487         
1488         # Making sure there's only 1 `rss` element and that the correct
1489@@ -35,59 +55,133 @@ class SyndicationFeedTest(TestCase):
1490         chan_elem = feed.getElementsByTagName('channel')
1491         self.assertEqual(len(chan_elem), 1)
1492         chan = chan_elem[0]
1493-        self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item'])
1494+        self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category'])
1495+        self.assertChildNodeContent(chan, {
1496+            'title': 'My blog',
1497+            'description': 'A more thorough description of my blog.',
1498+            'link': 'http://example.com/blog/',
1499+            'language': 'en',
1500+            'lastBuildDate': 'Thu, 03 Jan 2008 13:30:00 -0600',
1501+            #'atom:link': '',
1502+            'ttl': '600',
1503+            'copyright': 'Copyright (c) 2007, Sally Smith',
1504+        })
1505+        self.assertCategories(chan, ['python', 'django']);
1506+       
1507+        # Ensure the content of the channel is correct
1508+        self.assertChildNodeContent(chan, {
1509+            'title': 'My blog',
1510+            'link': 'http://example.com/blog/',
1511+        })
1512+       
1513+        # Check feed_url is passed
1514+        self.assertEqual(
1515+            chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
1516+            'http://example.com/syndication/rss2/'
1517+        )
1518+       
1519+        items = chan.getElementsByTagName('item')
1520+        self.assertEqual(len(items), Entry.objects.count())
1521+        self.assertChildNodeContent(items[0], {
1522+            'title': 'My first entry',
1523+            'description': 'Overridden description: My first entry',
1524+            'link': 'http://example.com/blog/1/',
1525+            'guid': 'http://example.com/blog/1/',
1526+            'pubDate': 'Tue, 01 Jan 2008 12:30:00 -0600',
1527+            'author': 'test@example.com (Sally Smith)',
1528+        })
1529+        self.assertCategories(items[0], ['python', 'testing']);
1530+       
1531+        for item in items:
1532+            self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'category', 'pubDate', 'author'])
1533     
1534+    def test_rss091_feed(self):
1535+        """
1536+        Test the structure and content of feeds generated by RssUserland091Feed.
1537+        """
1538+        response = self.client.get('/syndication/rss091/')
1539+        doc = minidom.parseString(response.content)
1540+       
1541+        # Making sure there's only 1 `rss` element and that the correct
1542+        # RSS version was specified.
1543+        feed_elem = doc.getElementsByTagName('rss')
1544+        self.assertEqual(len(feed_elem), 1)
1545+        feed = feed_elem[0]
1546+        self.assertEqual(feed.getAttribute('version'), '0.91')
1547+       
1548+        # Making sure there's only one `channel` element w/in the
1549+        # `rss` element.
1550+        chan_elem = feed.getElementsByTagName('channel')
1551+        self.assertEqual(len(chan_elem), 1)
1552+        chan = chan_elem[0]
1553+        self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category'])
1554+       
1555+        # Ensure the content of the channel is correct
1556+        self.assertChildNodeContent(chan, {
1557+            'title': 'My blog',
1558+            'link': 'http://example.com/blog/',
1559+        })
1560+        self.assertCategories(chan, ['python', 'django'])
1561+       
1562+        # Check feed_url is passed
1563+        self.assertEqual(
1564+            chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
1565+            'http://example.com/syndication/rss091/'
1566+        )
1567+       
1568         items = chan.getElementsByTagName('item')
1569         self.assertEqual(len(items), Entry.objects.count())
1570+        self.assertChildNodeContent(items[0], {
1571+            'title': 'My first entry',
1572+            'description': 'Overridden description: My first entry',
1573+            'link': 'http://example.com/blog/1/',
1574+        })
1575         for item in items:
1576-            self.assertChildNodes(item, ['title', 'link', 'description', 'guid'])
1577+            self.assertChildNodes(item, ['title', 'link', 'description'])
1578+            self.assertCategories(item, [])
1579     
1580     def test_atom_feed(self):
1581-        response = self.client.get('/syndication/feeds/atom/')
1582-        doc = minidom.parseString(response.content)
1583+        """
1584+        Test the structure and content of feeds generated by Atom1Feed.
1585+        """
1586+        response = self.client.get('/syndication/atom/')
1587+        feed = minidom.parseString(response.content).firstChild
1588         
1589-        feed = doc.firstChild
1590         self.assertEqual(feed.nodeName, 'feed')
1591         self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom')
1592-        self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry'])       
1593+        self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'rights', 'category', 'author'])
1594+        for link in feed.getElementsByTagName('link'):
1595+            if link.getAttribute('rel') == 'self':
1596+                self.assertEqual(link.getAttribute('href'), 'http://example.com/syndication/atom/')
1597         
1598         entries = feed.getElementsByTagName('entry')
1599         self.assertEqual(len(entries), Entry.objects.count())
1600         for entry in entries:
1601-            self.assertChildNodes(entry, ['title', 'link', 'id', 'summary'])
1602+            self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'category', 'updated', 'rights', 'author'])
1603             summary = entry.getElementsByTagName('summary')[0]
1604             self.assertEqual(summary.getAttribute('type'), 'html')
1605     
1606     def test_custom_feed_generator(self):
1607-        response = self.client.get('/syndication/feeds/custom/')
1608-        doc = minidom.parseString(response.content)
1609+        response = self.client.get('/syndication/custom/')
1610+        feed = minidom.parseString(response.content).firstChild
1611         
1612-        feed = doc.firstChild
1613         self.assertEqual(feed.nodeName, 'feed')
1614         self.assertEqual(feed.getAttribute('django'), 'rocks')
1615-        self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry', 'spam'])       
1616+        self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'spam', 'rights', 'category', 'author'])
1617         
1618         entries = feed.getElementsByTagName('entry')
1619         self.assertEqual(len(entries), Entry.objects.count())
1620         for entry in entries:
1621             self.assertEqual(entry.getAttribute('bacon'), 'yum')
1622-            self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry'])
1623+            self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry', 'rights', 'author', 'updated', 'category'])
1624             summary = entry.getElementsByTagName('summary')[0]
1625             self.assertEqual(summary.getAttribute('type'), 'html')
1626-       
1627-    def test_complex_base_url(self):
1628-        """
1629-        Tests that that the base url for a complex feed doesn't raise a 500
1630-        exception.
1631-        """
1632-        response = self.client.get('/syndication/feeds/complex/')
1633-        self.assertEquals(response.status_code, 404)
1634-
1635+   
1636     def test_title_escaping(self):
1637         """
1638         Tests that titles are escaped correctly in RSS feeds.
1639         """
1640-        response = self.client.get('/syndication/feeds/rss/')
1641+        response = self.client.get('/syndication/rss2/')
1642         doc = minidom.parseString(response.content)
1643         for item in doc.getElementsByTagName('item'):
1644             link = item.getElementsByTagName('link')[0]
1645@@ -101,7 +195,7 @@ class SyndicationFeedTest(TestCase):
1646         """
1647         # Naive date times passed in get converted to the local time zone, so
1648         # check the recived zone offset against the local offset.
1649-        response = self.client.get('/syndication/feeds/naive-dates/')
1650+        response = self.client.get('/syndication/naive-dates/')
1651         doc = minidom.parseString(response.content)
1652         updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText       
1653         tz = tzinfo.LocalTimezone(datetime.datetime.now())
1654@@ -112,8 +206,109 @@ class SyndicationFeedTest(TestCase):
1655         """
1656         Test that datetimes with timezones don't get trodden on.
1657         """
1658-        response = self.client.get('/syndication/feeds/aware-dates/')
1659+        response = self.client.get('/syndication/aware-dates/')
1660         doc = minidom.parseString(response.content)
1661         updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
1662         self.assertEqual(updated[-6:], '+00:42')
1663-       
1664\ No newline at end of file
1665+   
1666+    def test_feed_url(self):
1667+        """
1668+        Test that the feed_url can be overridden.
1669+        """
1670+        response = self.client.get('/syndication/feedurl/')
1671+        doc = minidom.parseString(response.content)
1672+        for link in doc.getElementsByTagName('link'):
1673+            if link.getAttribute('rel') == 'self':
1674+                self.assertEqual(link.getAttribute('href'), 'http://example.com/customfeedurl/')
1675+   
1676+    def test_item_link_error(self):
1677+        """
1678+        Test that a ImproperlyConfigured is raised if no link could be found
1679+        for the item(s).
1680+        """
1681+        self.assertRaises(ImproperlyConfigured,
1682+                          self.client.get,
1683+                          '/syndication/articles/')
1684+   
1685+    def test_template_feed(self):
1686+        """
1687+        Test that the item title and description can be overridden with
1688+        templates.
1689+        """
1690+        response = self.client.get('/syndication/template/')
1691+        doc = minidom.parseString(response.content)
1692+        feed = doc.getElementsByTagName('rss')[0]
1693+        chan = feed.getElementsByTagName('channel')[0]
1694+        items = chan.getElementsByTagName('item')
1695+       
1696+        self.assertChildNodeContent(items[0], {
1697+            'title': 'Title in your templates: My first entry',
1698+            'description': 'Description in your templates: My first entry',
1699+            'link': 'http://example.com/blog/1/',
1700+        })
1701+   
1702+    def test_add_domain(self):
1703+        """
1704+        Test add_domain() prefixes domains onto the correct URLs.
1705+        """
1706+        self.assertEqual(
1707+            views.add_domain('example.com', '/foo/?arg=value'),
1708+            'http://example.com/foo/?arg=value'
1709+        )
1710+        self.assertEqual(
1711+            views.add_domain('example.com', 'http://djangoproject.com/doc/'),
1712+            'http://djangoproject.com/doc/'
1713+        )
1714+        self.assertEqual(
1715+            views.add_domain('example.com', 'https://djangoproject.com/doc/'),
1716+            'https://djangoproject.com/doc/'
1717+        )
1718+
1719+
1720+######################################
1721+# Deprecated feeds
1722+######################################
1723+
1724+class DeprecatedSyndicationFeedTest(FeedTestCase):
1725+    """
1726+    Tests for the deprecated API (feed() view and the feed_dict etc).
1727+    """
1728+   
1729+    def test_empty_feed_dict(self):
1730+        """
1731+        Test that an empty feed_dict raises a 404.
1732+        """
1733+        response = self.client.get('/syndication/depr-feeds-empty/aware-dates/')
1734+        self.assertEquals(response.status_code, 404)
1735+
1736+    def test_nonexistent_slug(self):
1737+        """
1738+        Test that a non-existent slug raises a 404.
1739+        """
1740+        response = self.client.get('/syndication/depr-feeds/foobar/')
1741+        self.assertEquals(response.status_code, 404)
1742+   
1743+    def test_rss_feed(self):
1744+        """
1745+        A simple test for Rss201rev2Feed feeds generated by the deprecated
1746+        system.
1747+        """
1748+        response = self.client.get('/syndication/depr-feeds/rss/')
1749+        doc = minidom.parseString(response.content)
1750+        feed = doc.getElementsByTagName('rss')[0]
1751+        self.assertEqual(feed.getAttribute('version'), '2.0')
1752+       
1753+        chan = feed.getElementsByTagName('channel')[0]
1754+        self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link'])
1755+   
1756+        items = chan.getElementsByTagName('item')
1757+        self.assertEqual(len(items), Entry.objects.count())
1758+   
1759+    def test_complex_base_url(self):
1760+        """
1761+        Tests that the base url for a complex feed doesn't raise a 500
1762+        exception.
1763+        """
1764+        response = self.client.get('/syndication/depr-feeds/complex/')
1765+        self.assertEquals(response.status_code, 404)
1766+
1767diff --git a/tests/regressiontests/syndication/urls.py b/tests/regressiontests/syndication/urls.py
1768index ec45026..b4e0bcb 100644
1769--- a/tests/regressiontests/syndication/urls.py
1770+++ b/tests/regressiontests/syndication/urls.py
1771@@ -1,14 +1,24 @@
1772-import feeds
1773-from django.conf.urls.defaults import patterns
1774+from django.conf.urls.defaults import *
1775 
1776+import feeds
1777+
1778 feed_dict = {
1779-    'complex': feeds.ComplexFeed,
1780-    'rss': feeds.TestRssFeed,
1781-    'atom': feeds.TestAtomFeed,
1782-    'custom': feeds.TestCustomFeed,
1783-    'naive-dates': feeds.NaiveDatesFeed,
1784-    'aware-dates': feeds.TZAwareDatesFeed,   
1785+    'complex': feeds.DeprecatedComplexFeed,
1786+    'rss': feeds.DeprecatedRssFeed,
1787 }
1788-urlpatterns = patterns('',
1789-    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feed_dict})
1790+
1791+urlpatterns = patterns('django.contrib.syndication.views',
1792+    (r'^complex/(?P<foo>.*)/$', feeds.ComplexFeed()),
1793+    (r'^rss2/$', feeds.TestRss2Feed()),
1794+    (r'^rss091/$', feeds.TestRss091Feed()),
1795+    (r'^atom/$', feeds.TestAtomFeed()),
1796+    (r'^custom/$', feeds.TestCustomFeed()),
1797+    (r'^naive-dates/$', feeds.NaiveDatesFeed()),
1798+    (r'^aware-dates/$', feeds.TZAwareDatesFeed()),
1799+    (r'^feedurl/$', feeds.TestFeedUrlFeed()),
1800+    (r'^articles/$', feeds.ArticlesFeed()),
1801+    (r'^template/$', feeds.TemplateFeed()),
1802+   
1803+    (r'^depr-feeds/(?P<url>.*)/$', 'feed', {'feed_dict': feed_dict}),
1804+    (r'^depr-feeds-empty/(?P<url>.*)/$', 'feed', {'feed_dict': None}),
1805 )
1806diff --git a/tests/regressiontests/utils/feedgenerator.py b/tests/regressiontests/utils/feedgenerator.py
1807new file mode 100644
1808index 0000000..3274d69
1809--- /dev/null
1810+++ b/tests/regressiontests/utils/feedgenerator.py
1811@@ -0,0 +1,61 @@
1812+import datetime
1813+from django.utils import feedgenerator, tzinfo
1814+
1815+class FeedgeneratorTest(TestCase):
1816+    """
1817+    Tests for the low-level syndication feed framework.
1818+    """
1819+   
1820+    def test_get_tag_uri(self):
1821+        """
1822+        Test get_tag_uri() correctly generates TagURIs.
1823+        """
1824+        self.assertEqual(
1825+            feedgenerator.get_tag_uri('http://example.org/foo/bar#headline', datetime.date(2004, 10, 25)),
1826+            u'tag:example.org,2004-10-25:/foo/bar/headline')
1827+       
1828+    def test_get_tag_uri_with_port(self):
1829+        """
1830+        Test that get_tag_uri() correctly generates TagURIs from URLs with port
1831+        numbers.
1832+        """
1833+        self.assertEqual(
1834+            feedgenerator.get_tag_uri('http://www.example.org:8000/2008/11/14/django#headline', datetime.datetime(2008, 11, 14, 13, 37, 0)),
1835+            u'tag:www.example.org,2008-11-14:/2008/11/14/django/headline')
1836+   
1837+    def test_rfc2822_date(self):
1838+        """
1839+        Test rfc2822_date() correctly formats datetime objects.
1840+        """
1841+        self.assertEqual(
1842+            feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0)),
1843+            "Fri, 14 Nov 2008 13:37:00 -0000"
1844+        )
1845+       
1846+    def test_rfc2822_date_with_timezone(self):
1847+        """
1848+        Test rfc2822_date() correctly formats datetime objects with tzinfo.
1849+        """
1850+        self.assertEqual(
1851+            feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=60)))),
1852+            "Fri, 14 Nov 2008 13:37:00 +0100"
1853+        )
1854+   
1855+    def test_rfc3339_date(self):
1856+        """
1857+        Test rfc3339_date() correctly formats datetime objects.
1858+        """
1859+        self.assertEqual(
1860+            feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0)),
1861+            "2008-11-14T13:37:00Z"
1862+        )
1863+   
1864+    def test_rfc3339_date_with_timezone(self):
1865+        """
1866+        Test rfc3339_date() correctly formats datetime objects with tzinfo.
1867+        """
1868+        self.assertEqual(
1869+            feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=120)))),
1870+            "2008-11-14T13:37:00+02:00"
1871+        )
1872+