diff --git a/AUTHORS b/AUTHORS
index c8d91f7..d3fb3bc 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -162,6 +162,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>
diff --git a/django/contrib/comments/feeds.py b/django/contrib/comments/feeds.py
index 24b10d4..d4269e9 100644
--- a/django/contrib/comments/feeds.py
+++ b/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 _
diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py
index ade43d7..06b5f73 100644
--- a/django/contrib/syndication/feeds.py
+++ b/django/contrib/syndication/feeds.py
@@ -1,82 +1,26 @@
-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
 
+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
-
+        
     def get_feed(self, url=None):
         """
         Returns a feedgenerator.DefaultFeed object, fully populated, for
@@ -86,94 +30,9 @@ class Feed(object):
             bits = url.split('/')
         else:
             bits = []
-
         try:
             obj = self.get_object(bits)
         except ObjectDoesNotExist:
             raise FeedDoesNotExist
+        return super(Feed, self).get_feed(obj, self.request)
 
-        if Site._meta.installed:
-            current_site = Site.objects.get_current()
-        else:
-            current_site = RequestSite(self.request)
-        
-        link = self.__get_dynamic_attr('link', obj)
-        link = add_domain(current_site.domain, link)
-
-        feed = self.feed_type(
-            title = self.__get_dynamic_attr('title', obj),
-            subtitle = self.__get_dynamic_attr('subtitle', obj),
-            link = link,
-            description = self.__get_dynamic_attr('description', obj),
-            language = settings.LANGUAGE_CODE.decode(),
-            feed_url = add_domain(current_site.domain,
-                                  self.__get_dynamic_attr('feed_url', obj)),
-            author_name = self.__get_dynamic_attr('author_name', obj),
-            author_link = self.__get_dynamic_attr('author_link', obj),
-            author_email = self.__get_dynamic_attr('author_email', obj),
-            categories = self.__get_dynamic_attr('categories', obj),
-            feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
-            feed_guid = self.__get_dynamic_attr('feed_guid', obj),
-            ttl = self.__get_dynamic_attr('ttl', obj),
-            **self.feed_extra_kwargs(obj)
-        )
-
-        try:
-            title_tmp = loader.get_template(self.title_template_name)
-        except TemplateDoesNotExist:
-            title_tmp = Template('{{ obj }}')
-        try:
-            description_tmp = loader.get_template(self.description_template_name)
-        except TemplateDoesNotExist:
-            description_tmp = Template('{{ obj }}')
-
-        for item in self.__get_dynamic_attr('items', obj):
-            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
-            enc = None
-            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
-            if enc_url:
-                enc = feedgenerator.Enclosure(
-                    url = smart_unicode(enc_url),
-                    length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
-                    mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
-                )
-            author_name = self.__get_dynamic_attr('item_author_name', item)
-            if author_name is not None:
-                author_email = self.__get_dynamic_attr('item_author_email', item)
-                author_link = self.__get_dynamic_attr('item_author_link', item)
-            else:
-                author_email = author_link = None
-
-            pubdate = self.__get_dynamic_attr('item_pubdate', item)
-            if pubdate and not pubdate.tzinfo:
-                now = datetime.now()
-                utcnow = datetime.utcnow()
-
-                # Must always subtract smaller time from larger time here.
-                if utcnow > now:
-                    sign = -1
-                    tzDifference = (utcnow - now)
-                else:
-                    sign = 1
-                    tzDifference = (now - utcnow)
-
-                # Round the timezone offset to the nearest half hour.
-                tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
-                tzOffset = timedelta(minutes=tzOffsetMinutes)
-                pubdate = pubdate.replace(tzinfo=FixedOffset(tzOffset))
-
-            feed.add_item(
-                title = title_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
-                link = link,
-                description = description_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
-                unique_id = self.__get_dynamic_attr('item_guid', item, link),
-                enclosure = enc,
-                pubdate = pubdate,
-                author_name = author_name,
-                author_email = author_email,
-                author_link = author_link,
-                categories = self.__get_dynamic_attr('item_categories', item),
-                item_copyright = self.__get_dynamic_attr('item_copyright', item),
-                **self.item_extra_kwargs(item)
-            )
-        return feed
diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py
index 423d333..be374e1 100644
--- a/django/contrib/syndication/views.py
+++ b/django/contrib/syndication/views.py
@@ -1,7 +1,202 @@
-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://')):
+        # '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
+        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 +212,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
+
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
index c9445f9..bb74a8e 100644
--- a/django/utils/feedgenerator.py
+++ b/django/utils/feedgenerator.py
@@ -19,8 +19,8 @@ For definitions of the different versions of RSS, see:
 http://diveintomark.org/archives/2004/02/04/incompatible-rss
 """
 
-import re
 import datetime
+import urlparse
 from django.utils.xmlutils import SimplerXMLGenerator
 from django.utils.encoding import force_unicode, iri_to_uri
 
@@ -46,12 +46,16 @@ def rfc3339_date(date):
         return date.strftime('%Y-%m-%dT%H:%M:%SZ')
 
 def get_tag_uri(url, date):
-    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
-    tag = re.sub('^http://', '', url)
+    """
+    Creates a TagURI. 
+    
+    See http://diveintomark.org/archives/2004/05/28/howto-atom-id
+    """
+    url_split = urlparse.urlparse(url)
+    d = ''
     if date is not None:
-        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
-    tag = re.sub('#', '/', tag)
-    return u'tag:' + tag
+        d = ',%s' % date.strftime('%Y-%m-%d')
+    return u'tag:%s%s:%s/%s' % (url_split.hostname, d, url_split.path, url_split.fragment)
 
 class SyndicationFeed(object):
     "Base class for all syndication feeds. Subclasses should provide write()"
@@ -61,6 +65,9 @@ class SyndicationFeed(object):
         to_unicode = lambda s: force_unicode(s, strings_only=True)
         if categories:
             categories = [force_unicode(c) for c in categories]
+        if ttl is not None:
+            # Force ints to unicode
+            ttl = force_unicode(ttl)
         self.feed = {
             'title': to_unicode(title),
             'link': iri_to_uri(link),
@@ -91,6 +98,9 @@ class SyndicationFeed(object):
         to_unicode = lambda s: force_unicode(s, strings_only=True)
         if categories:
             categories = [to_unicode(c) for c in categories]
+        if ttl is not None:
+            # Force ints to unicode
+            ttl = force_unicode(ttl)
         item = {
             'title': to_unicode(title),
             'link': iri_to_uri(link),
@@ -186,7 +196,8 @@ class RssFeed(SyndicationFeed):
         handler.endElement(u"rss")
 
     def rss_attributes(self):
-        return {u"version": self._version}
+        return {u"version": self._version,
+                u"xmlns:atom": u"http://www.w3.org/2005/Atom"}
 
     def write_items(self, handler):
         for item in self.items:
@@ -198,6 +209,7 @@ class RssFeed(SyndicationFeed):
         handler.addQuickElement(u"title", self.feed['title'])
         handler.addQuickElement(u"link", self.feed['link'])
         handler.addQuickElement(u"description", self.feed['description'])
+        handler.addQuickElement(u"atom:link", None, {u"rel": u"self", u"href": self.feed['feed_url']})
         if self.feed['language'] is not None:
             handler.addQuickElement(u"language", self.feed['language'])
         for cat in self.feed['categories']:
@@ -235,7 +247,7 @@ class Rss201rev2Feed(RssFeed):
         elif item["author_email"]:
             handler.addQuickElement(u"author", item["author_email"])
         elif item["author_name"]:
-            handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
+            handler.addQuickElement(u"dc:creator", item["author_name"], {u"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
 
         if item['pubdate'] is not None:
             handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index b54ae6e..b9ccc72 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -50,6 +50,10 @@ their deprecation, as per the :ref:`Django deprecation policy
           backwards compatibility. These have been deprecated since the 1.2
           release.
 
+        * 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
diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt
index cb9c22b..6f09f36 100644
--- a/docs/ref/contrib/syndication.txt
+++ b/docs/ref/contrib/syndication.txt
@@ -23,95 +23,68 @@ to generate feeds outside of a Web context, or in some other lower-level way.
 The high-level framework
 ========================
 
+.. versionchanged:: 1.2
+
 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, just 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
+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
+:class:`~django.contrib.syndication.views.Feed` classes subclass
+``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
 ----------------
 
 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."
-
+        
         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, it needs to be put in your :ref:`URLconf <topics-http-urls>`. Here is a full 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 class subclasses ``django.contrib.syndication.views.Feed``.
 
 * :attr:`title`, :attr:`link` and :attr:`description` correspond to the
   standard RSS ``<title>``, ``<link>`` and ``<description>`` elements,
@@ -133,13 +106,19 @@ One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``,
 ``<link>`` and ``<description>``. We need to tell the framework what data to put
 into those elements.
 
-    * To specify the contents of ``<title>`` and ``<description>``, create
-      :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 +131,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::
+.. versionchanged:: 1.2
+   Instead of just taking the ``bits`` argument, ``get_object()`` now takes a 
+   ``request`` object and the URL arguments.
 
-    from django.contrib.syndication.feeds import FeedDoesNotExist
-    from django.core.exceptions import ObjectDoesNotExist
+Like a view, the arguments in the URL are passed to the :meth:`get_object()` 
+method along with the request object. 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 required.
 
 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 +234,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 +282,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 +310,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,27 +323,22 @@ 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):
@@ -430,9 +354,8 @@ This example illustrates all possible attributes and methods for a
         # 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.
+        # If one is not specified, the item_title() or item_description() 
+        # methods are used instead.
 
         title_template = None
         description_template = None
@@ -572,18 +495,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 +543,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 +640,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.
         #
diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt
index 9e8816e..b8a1890 100644
--- a/docs/releases/1.2.txt
+++ b/docs/releases/1.2.txt
@@ -176,6 +176,78 @@ For more information, see the full
 :ref:`messages documentation <ref-contrib-messages>`. You should begin to
 update your code to use the new API immediately.
 
+``django.contrib.syndication.feeds.Feed``
+-----------------------------------------
+
+The syndication ``Feed`` class has been replaced by 
+``django.contrib.syndication.views.Feed``, 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}),
+        # ...
+    )
+
+This should be replaced by::
+
+    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 used the ``feed()`` view, the ``LatestEntries`` class would not need to 
+be modified apart from subclassing the new ``Feed`` class. However, 
+``LatestEntriesByCategory`` used the ``get_object()`` method with the ``bits`` 
+argument to specify a specific category to show. Much like a view, the 
+``get_object()`` method now 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)
+        
+        # ...
+
+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.
+
+In previous versions of Django, the ``feed_copyright()`` method was incorrectly  
+documented as ``copyright()``. Change any ``copyright()`` methods you have on 
+your ``Feed`` classes to ``feed_copyright()``.
+
+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
 ========================
 
@@ -277,3 +349,22 @@ alternative to the normal primary-key based object references in a
 fixture, improving readability, and resolving problems referring to
 objects whose primary key value may not be predictable or known.
 
+Syndication feeds as views
+--------------------------
+
+:ref:`Syndication feeds <ref-contrib-syndication>` can now be used directly as 
+views in your :ref:`URLconf <topics-http-urls>`. Previously, all the feeds on a 
+site had to live beneath a single URL, which produced ugly, unnatural URLs.
+
+For example, suppose you listed blog posts for the "django" tag under 
+``/blog/django/``. In previous versions of Django, a feed for those posts would 
+have had to be something like ``/feeds/blog-tag/django/``. In Django 1.2, it 
+can simply be ``/blog/django/feed/``.
+
+Like any other view, feeds are now passed a ``request`` object, so you can 
+do user based access control amongst other things.
+
+Also, you no longer need to create templates for your feed item titles and 
+descriptions. You can set these with the ``item_title()`` and 
+``item_description()`` methods.
+
diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py
index 79837f9..8648cec 100644
--- a/tests/regressiontests/syndication/feeds.py
+++ b/tests/regressiontests/syndication/feeds.py
@@ -1,29 +1,102 @@
+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
+    
+    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(TestRssFeed):
-    feed_type = Atom1Feed
+class TestAtomFeed(TestRss2Feed):
+    feed_type = feedgenerator.Atom1Feed
+    subtitle = TestRss2Feed.description
 
-class MyCustomAtom1Feed(Atom1Feed):
+
+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(feedgenerator.Atom1Feed):
     """
     Test of a custom feed generator class.
     """    
@@ -44,23 +117,26 @@ class MyCustomAtom1Feed(Atom1Feed):
     def add_item_elements(self, handler, item):
         super(MyCustomAtom1Feed, self).add_item_elements(handler, item)
         handler.addQuickElement(u'ministry', u'silly walks')
-    
+
+
 class TestCustomFeed(TestAtomFeed):
     feed_type = MyCustomAtom1Feed
+
+
+class DeprecatedComplexFeed(feeds.Feed):
+    def get_object(self, bits):
+        if len(bits) != 1:
+            raise ObjectDoesNotExist
+        return None
+
+
+class DeprecatedRssFeed(feeds.Feed):
+    link = "/blog/"
+    title = 'My blog'
     
-class NaiveDatesFeed(TestAtomFeed):
-    """
-    A feed with naive (non-timezone-aware) dates.
-    """
-    def item_pubdate(self, item):
-        return item.date
+    def items(self):
+        return Entry.objects.all()
         
-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))
\ No newline at end of file
+    def item_link(self, item):
+        return "/blog/%s/" % item.pk
+
diff --git a/tests/regressiontests/syndication/fixtures/feeddata.json b/tests/regressiontests/syndication/fixtures/feeddata.json
index 375ee16..fdc373d 100644
--- a/tests/regressiontests/syndication/fixtures/feeddata.json
+++ b/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"
+    }
   }
 ]
\ No newline at end of file
diff --git a/tests/regressiontests/syndication/models.py b/tests/regressiontests/syndication/models.py
index 99e14ad..b6cf9e5 100644
--- a/tests/regressiontests/syndication/models.py
+++ b/tests/regressiontests/syndication/models.py
@@ -1,8 +1,22 @@
 from django.db import models
-
+ 
 class Entry(models.Model):
     title = models.CharField(max_length=200)
     date = models.DateTimeField()
     
+    class Meta:
+        ordering = ('date',)
+    
+    def __unicode__(self):
+        return self.title
+    
+    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
\ No newline at end of file
+        return self.title
diff --git a/tests/regressiontests/syndication/templates/syndication/description.html b/tests/regressiontests/syndication/templates/syndication/description.html
new file mode 100644
index 0000000..85ec82c
--- /dev/null
+++ b/tests/regressiontests/syndication/templates/syndication/description.html
@@ -0,0 +1 @@
+Description in your templates: {{ obj }}
\ No newline at end of file
diff --git a/tests/regressiontests/syndication/templates/syndication/title.html b/tests/regressiontests/syndication/templates/syndication/title.html
new file mode 100644
index 0000000..eb17969
--- /dev/null
+++ b/tests/regressiontests/syndication/templates/syndication/title.html
@@ -0,0 +1 @@
+Title in your templates: {{ obj }}
\ No newline at end of file
diff --git a/tests/regressiontests/syndication/tests.py b/tests/regressiontests/syndication/tests.py
index 816cb44..1861e88 100644
--- a/tests/regressiontests/syndication/tests.py
+++ b/tests/regressiontests/syndication/tests.py
@@ -1,26 +1,46 @@
-# -*- 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):
         actual = set([n.nodeName for n in elem.childNodes])
         expected = set(expected)
         self.assertEqual(actual, expected)
+    
+    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));
 
-    def test_rss_feed(self):
-        response = self.client.get('/syndication/feeds/rss/')
+######################################
+# 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
@@ -35,59 +55,133 @@ class SyndicationFeedTest(TestCase):
         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', '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)
+        
+        # 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', 'guid'])
+            self.assertChildNodes(item, ['title', 'link', 'description'])
+            self.assertCategories(item, [])
     
     def test_atom_feed(self):
-        response = self.client.get('/syndication/feeds/atom/')
-        doc = minidom.parseString(response.content)
+        """
+        Test the structure and content of feeds generated by Atom1Feed.
+        """
+        response = self.client.get('/syndication/atom/')
+        feed = minidom.parseString(response.content).firstChild
         
-        feed = doc.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.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)
+        response = self.client.get('/syndication/custom/')
+        feed = minidom.parseString(response.content).firstChild
         
-        feed = doc.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]
@@ -101,7 +195,7 @@ class SyndicationFeedTest(TestCase):
         """
         # 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        
         tz = tzinfo.LocalTimezone(datetime.datetime.now())
@@ -112,8 +206,113 @@ class SyndicationFeedTest(TestCase):
         """
         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')
-        
\ No newline at end of file
+    
+    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'), 
+            'http://example.commailto:uhoh%40djangoproject.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)
+
diff --git a/tests/regressiontests/syndication/urls.py b/tests/regressiontests/syndication/urls.py
index ec45026..b4e0bcb 100644
--- a/tests/regressiontests/syndication/urls.py
+++ b/tests/regressiontests/syndication/urls.py
@@ -1,14 +1,24 @@
-import feeds
-from django.conf.urls.defaults import patterns
+from django.conf.urls.defaults import *
 
+import feeds
+ 
 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}),
 )
diff --git a/tests/regressiontests/utils/feedgenerator.py b/tests/regressiontests/utils/feedgenerator.py
new file mode 100644
index 0000000..0e50118
--- /dev/null
+++ b/tests/regressiontests/utils/feedgenerator.py
@@ -0,0 +1,60 @@
+import datetime
+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"
+        )
