Ticket #12403: syndication-view-6.diff

File syndication-view-6.diff, 75.6 KB (added by bfirsh, 5 years ago)

Turns out "mailto:" test needs fixing and isn't against the RSS spec

  • AUTHORS

    diff --git a/AUTHORS b/AUTHORS
    index 56ab83d..ae6a090 100644
    a b answer newbie questions, and generally made Django that much better: 
    166166    Afonso Fernández Nogueira <fonzzo.django@gmail.com>
    167167    J. Pablo Fernandez <pupeno@pupeno.com>
    168168    Maciej Fijalkowski
     169    Ben Firshman <ben@firshman.co.uk>
    169170    Matthew Flanagan <http://wadofstuff.blogspot.com>
    170171    Eric Floehr <eric@intellovations.com>
    171172    Eric Florenzano <floguy@gmail.com>
  • django/contrib/comments/feeds.py

    diff --git a/django/contrib/comments/feeds.py b/django/contrib/comments/feeds.py
    index 24b10d4..d4269e9 100644
    a b  
    11from django.conf import settings
    2 from django.contrib.syndication.feeds import Feed
     2from django.contrib.syndication.views import Feed
    33from django.contrib.sites.models import Site
    44from django.contrib import comments
    55from django.utils.translation import ugettext as _
  • django/contrib/syndication/feeds.py

    diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py
    index e5e0877..7a99660 100644
    a b  
    1 from datetime import datetime, timedelta
     1from django.contrib.syndication import views
     2from django.core.exceptions import ObjectDoesNotExist
     3import warnings
    24
    3 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
    4 from django.template import loader, Template, TemplateDoesNotExist
    5 from django.contrib.sites.models import Site, RequestSite
    6 from django.utils import feedgenerator
    7 from django.utils.tzinfo import FixedOffset
    8 from django.utils.encoding import smart_unicode, iri_to_uri
    9 from django.conf import settings         
    10 from django.template import RequestContext
    11 
    12 def add_domain(domain, url):
    13     if not (url.startswith('http://') or url.startswith('https://')):
    14         # 'url' must already be ASCII and URL-quoted, so no need for encoding
    15         # conversions here.
    16         url = iri_to_uri(u'http://%s%s' % (domain, url))
    17     return url
    18 
    19 class FeedDoesNotExist(ObjectDoesNotExist):
    20     pass
    21 
    22 class Feed(object):
    23     item_pubdate = None
    24     item_enclosure_url = None
    25     feed_type = feedgenerator.DefaultFeed
    26     feed_url = None
    27     title_template = None
    28     description_template = None
     5# This is part of the deprecated API
     6from django.contrib.syndication.views import FeedDoesNotExist
    297
     8class Feed(views.Feed):
     9    """Provided for backwards compatibility."""
    3010    def __init__(self, slug, request):
     11        warnings.warn('The syndication feeds.Feed class is deprecated. Please '
     12                      'use the new class based view API.',
     13                      category=PendingDeprecationWarning)
     14       
    3115        self.slug = slug
    3216        self.request = request
    33         self.feed_url = self.feed_url or request.path
    34         self.title_template_name = self.title_template or ('feeds/%s_title.html' % slug)
    35         self.description_template_name = self.description_template or ('feeds/%s_description.html' % slug)
    36 
    37     def item_link(self, item):
    38         try:
    39             return item.get_absolute_url()
    40         except AttributeError:
    41             raise ImproperlyConfigured("Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__)
    42 
    43     def __get_dynamic_attr(self, attname, obj, default=None):
    44         try:
    45             attr = getattr(self, attname)
    46         except AttributeError:
    47             return default
    48         if callable(attr):
    49             # Check func_code.co_argcount rather than try/excepting the
    50             # function and catching the TypeError, because something inside
    51             # the function may raise the TypeError. This technique is more
    52             # accurate.
    53             if hasattr(attr, 'func_code'):
    54                 argcount = attr.func_code.co_argcount
    55             else:
    56                 argcount = attr.__call__.func_code.co_argcount
    57             if argcount == 2: # one argument is 'self'
    58                 return attr(obj)
    59             else:
    60                 return attr()
    61         return attr
    62 
    63     def feed_extra_kwargs(self, obj):
    64         """
    65         Returns an extra keyword arguments dictionary that is used when
    66         initializing the feed generator.
    67         """
    68         return {}
    69 
    70     def item_extra_kwargs(self, item):
    71         """
    72         Returns an extra keyword arguments dictionary that is used with
    73         the `add_item` call of the feed generator.
    74         """
    75         return {}
    76 
     17        self.feed_url = getattr(self, 'feed_url', None) or request.path
     18        self.title_template = self.title_template or ('feeds/%s_title.html' % slug)
     19        self.description_template = self.description_template or ('feeds/%s_description.html' % slug)
     20   
    7721    def get_object(self, bits):
    7822        return None
    79 
     23   
    8024    def get_feed(self, url=None):
    8125        """
    8226        Returns a feedgenerator.DefaultFeed object, fully populated, for
    class Feed(object): 
    8630            bits = url.split('/')
    8731        else:
    8832            bits = []
    89 
    9033        try:
    9134            obj = self.get_object(bits)
    9235        except ObjectDoesNotExist:
    9336            raise FeedDoesNotExist
     37        return super(Feed, self).get_feed(obj, self.request)
    9438
    95         if Site._meta.installed:
    96             current_site = Site.objects.get_current()
    97         else:
    98             current_site = RequestSite(self.request)
    99        
    100         link = self.__get_dynamic_attr('link', obj)
    101         link = add_domain(current_site.domain, link)
    102 
    103         feed = self.feed_type(
    104             title = self.__get_dynamic_attr('title', obj),
    105             subtitle = self.__get_dynamic_attr('subtitle', obj),
    106             link = link,
    107             description = self.__get_dynamic_attr('description', obj),
    108             language = settings.LANGUAGE_CODE.decode(),
    109             feed_url = add_domain(current_site.domain,
    110                                   self.__get_dynamic_attr('feed_url', obj)),
    111             author_name = self.__get_dynamic_attr('author_name', obj),
    112             author_link = self.__get_dynamic_attr('author_link', obj),
    113             author_email = self.__get_dynamic_attr('author_email', obj),
    114             categories = self.__get_dynamic_attr('categories', obj),
    115             feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
    116             feed_guid = self.__get_dynamic_attr('feed_guid', obj),
    117             ttl = self.__get_dynamic_attr('ttl', obj),
    118             **self.feed_extra_kwargs(obj)
    119         )
    120 
    121         try:
    122             title_tmp = loader.get_template(self.title_template_name)
    123         except TemplateDoesNotExist:
    124             title_tmp = Template('{{ obj }}')
    125         try:
    126             description_tmp = loader.get_template(self.description_template_name)
    127         except TemplateDoesNotExist:
    128             description_tmp = Template('{{ obj }}')
    129 
    130         for item in self.__get_dynamic_attr('items', obj):
    131             link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
    132             enc = None
    133             enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
    134             if enc_url:
    135                 enc = feedgenerator.Enclosure(
    136                     url = smart_unicode(enc_url),
    137                     length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
    138                     mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
    139                 )
    140             author_name = self.__get_dynamic_attr('item_author_name', item)
    141             if author_name is not None:
    142                 author_email = self.__get_dynamic_attr('item_author_email', item)
    143                 author_link = self.__get_dynamic_attr('item_author_link', item)
    144             else:
    145                 author_email = author_link = None
    146 
    147             pubdate = self.__get_dynamic_attr('item_pubdate', item)
    148             if pubdate and not pubdate.tzinfo:
    149                 now = datetime.now()
    150                 utcnow = datetime.utcnow()
    151 
    152                 # Must always subtract smaller time from larger time here.
    153                 if utcnow > now:
    154                     sign = -1
    155                     tzDifference = (utcnow - now)
    156                 else:
    157                     sign = 1
    158                     tzDifference = (now - utcnow)
    159 
    160                 # Round the timezone offset to the nearest half hour.
    161                 tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
    162                 tzOffset = timedelta(minutes=tzOffsetMinutes)
    163                 pubdate = pubdate.replace(tzinfo=FixedOffset(tzOffset))
    164 
    165             feed.add_item(
    166                 title = title_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
    167                 link = link,
    168                 description = description_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
    169                 unique_id = self.__get_dynamic_attr('item_guid', item, link),
    170                 enclosure = enc,
    171                 pubdate = pubdate,
    172                 author_name = author_name,
    173                 author_email = author_email,
    174                 author_link = author_link,
    175                 categories = self.__get_dynamic_attr('item_categories', item),
    176                 item_copyright = self.__get_dynamic_attr('item_copyright', item),
    177                 **self.item_extra_kwargs(item)
    178             )
    179         return feed
  • django/contrib/syndication/views.py

    diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py
    index d422036..55c435d 100644
    a b  
    1 from django.contrib.syndication import feeds
     1import datetime
     2from django.conf import settings
     3from django.contrib.sites.models import Site, RequestSite
     4from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
    25from django.http import HttpResponse, Http404
     6from django.template import loader, Template, TemplateDoesNotExist, RequestContext
     7from django.utils import feedgenerator, tzinfo
     8from django.utils.encoding import force_unicode, iri_to_uri, smart_unicode
     9from django.utils.html import escape
     10
     11def add_domain(domain, url):
     12    if not (url.startswith('http://')
     13            or url.startswith('https://')
     14            or url.startswith('mailto:')):
     15        # 'url' must already be ASCII and URL-quoted, so no need for encoding
     16        # conversions here.
     17        url = iri_to_uri(u'http://%s%s' % (domain, url))
     18    return url
     19
     20class FeedDoesNotExist(ObjectDoesNotExist):
     21    pass
     22
     23
     24class Feed(object):
     25    feed_type = feedgenerator.DefaultFeed
     26    title_template = None
     27    description_template = None
     28   
     29    def __call__(self, request, *args, **kwargs):
     30        try:
     31            obj = self.get_object(request, *args, **kwargs)
     32        except ObjectDoesNotExist:
     33            raise Http404('Feed object does not exist.')
     34        feedgen = self.get_feed(obj, request)
     35        response = HttpResponse(mimetype=feedgen.mime_type)
     36        feedgen.write(response, 'utf-8')
     37        return response
     38   
     39    def item_title(self, item):
     40        # Titles should be double escaped by default (see #6533)
     41        return escape(force_unicode(item))
     42   
     43    def item_description(self, item):
     44        return force_unicode(item)
     45   
     46    def item_link(self, item):
     47        try:
     48            return item.get_absolute_url()
     49        except AttributeError:
     50            raise ImproperlyConfigured('Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class.' % item.__class__.__name__)
     51   
     52    def __get_dynamic_attr(self, attname, obj, default=None):
     53        try:
     54            attr = getattr(self, attname)
     55        except AttributeError:
     56            return default
     57        if callable(attr):
     58            # Check func_code.co_argcount rather than try/excepting the
     59            # function and catching the TypeError, because something inside
     60            # the function may raise the TypeError. This technique is more
     61            # accurate.
     62            if hasattr(attr, 'func_code'):
     63                argcount = attr.func_code.co_argcount
     64            else:
     65                argcount = attr.__call__.func_code.co_argcount
     66            if argcount == 2: # one argument is 'self'
     67                return attr(obj)
     68            else:
     69                return attr()
     70        return attr
     71   
     72    def feed_extra_kwargs(self, obj):
     73        """
     74        Returns an extra keyword arguments dictionary that is used when
     75        initializing the feed generator.
     76        """
     77        return {}
     78   
     79    def item_extra_kwargs(self, item):
     80        """
     81        Returns an extra keyword arguments dictionary that is used with
     82        the `add_item` call of the feed generator.
     83        """
     84        return {}
     85   
     86    def get_object(self, request, *args, **kwargs):
     87        return None
     88   
     89    def get_feed(self, obj, request):
     90        """
     91        Returns a feedgenerator.DefaultFeed object, fully populated, for
     92        this feed. Raises FeedDoesNotExist for invalid parameters.
     93        """
     94        if Site._meta.installed:
     95            current_site = Site.objects.get_current()
     96        else:
     97            current_site = RequestSite(request)
     98       
     99        link = self.__get_dynamic_attr('link', obj)
     100        link = add_domain(current_site.domain, link)
     101       
     102        feed = self.feed_type(
     103            title = self.__get_dynamic_attr('title', obj),
     104            subtitle = self.__get_dynamic_attr('subtitle', obj),
     105            link = link,
     106            description = self.__get_dynamic_attr('description', obj),
     107            language = settings.LANGUAGE_CODE.decode(),
     108            feed_url = add_domain(current_site.domain,
     109                    self.__get_dynamic_attr('feed_url', obj) or request.path),
     110            author_name = self.__get_dynamic_attr('author_name', obj),
     111            author_link = self.__get_dynamic_attr('author_link', obj),
     112            author_email = self.__get_dynamic_attr('author_email', obj),
     113            categories = self.__get_dynamic_attr('categories', obj),
     114            feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
     115            feed_guid = self.__get_dynamic_attr('feed_guid', obj),
     116            ttl = self.__get_dynamic_attr('ttl', obj),
     117            **self.feed_extra_kwargs(obj)
     118        )
     119       
     120        title_tmp = None
     121        if self.title_template is not None:
     122            try:
     123                title_tmp = loader.get_template(self.title_template)
     124            except TemplateDoesNotExist:
     125                pass
     126       
     127        description_tmp = None
     128        if self.description_template is not None:
     129            try:
     130                description_tmp = loader.get_template(self.description_template)
     131            except TemplateDoesNotExist:
     132                pass
     133       
     134        for item in self.__get_dynamic_attr('items', obj):
     135            if title_tmp is not None:
     136                title = title_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
     137            else:
     138                title = self.__get_dynamic_attr('item_title', item)
     139            if description_tmp is not None:
     140                description = description_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
     141            else:
     142                description = self.__get_dynamic_attr('item_description', item)
     143            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
     144            enc = None
     145            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
     146            if enc_url:
     147                enc = feedgenerator.Enclosure(
     148                    url = smart_unicode(enc_url),
     149                    length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
     150                    mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
     151                )
     152            author_name = self.__get_dynamic_attr('item_author_name', item)
     153            if author_name is not None:
     154                author_email = self.__get_dynamic_attr('item_author_email', item)
     155                author_link = self.__get_dynamic_attr('item_author_link', item)
     156            else:
     157                author_email = author_link = None
     158           
     159            pubdate = self.__get_dynamic_attr('item_pubdate', item)
     160            if pubdate and not pubdate.tzinfo:
     161                now = datetime.datetime.now()
     162                utcnow = datetime.datetime.utcnow()
     163               
     164                # Must always subtract smaller time from larger time here.
     165                if utcnow > now:
     166                    sign = -1
     167                    tzDifference = (utcnow - now)
     168                else:
     169                    sign = 1
     170                    tzDifference = (now - utcnow)
     171 
     172                # Round the timezone offset to the nearest half hour.
     173                tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
     174                tzOffset = datetime.timedelta(minutes=tzOffsetMinutes)
     175                pubdate = pubdate.replace(tzinfo=tzinfo.FixedOffset(tzOffset))
     176           
     177            feed.add_item(
     178                title = title,
     179                link = link,
     180                description = description,
     181                unique_id = self.__get_dynamic_attr('item_guid', item, link),
     182                enclosure = enc,
     183                pubdate = pubdate,
     184                author_name = author_name,
     185                author_email = author_email,
     186                author_link = author_link,
     187                categories = self.__get_dynamic_attr('item_categories', item),
     188                item_copyright = self.__get_dynamic_attr('item_copyright', item),
     189                **self.item_extra_kwargs(item)
     190            )
     191        return feed
     192
    3193
    4194def feed(request, url, feed_dict=None):
     195    """Provided for backwards compatibility."""
     196    import warnings
     197    warnings.warn('The syndication feed() view is deprecated. Please use the '
     198                  'new class based view API.',
     199                  category=PendingDeprecationWarning)
     200   
    5201    if not feed_dict:
    6202        raise Http404("No feeds are registered.")
    7 
     203   
    8204    try:
    9205        slug, param = url.split('/', 1)
    10206    except ValueError:
    11207        slug, param = url, ''
    12 
     208   
    13209    try:
    14210        f = feed_dict[slug]
    15211    except KeyError:
    16212        raise Http404("Slug %r isn't registered." % slug)
    17 
     213   
    18214    try:
    19215        feedgen = f(slug, request).get_feed(param)
    20     except feeds.FeedDoesNotExist:
     216    except FeedDoesNotExist:
    21217        raise Http404("Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug)
    22 
     218   
    23219    response = HttpResponse(mimetype=feedgen.mime_type)
    24220    feedgen.write(response, 'utf-8')
    25221    return response
     222
  • django/utils/feedgenerator.py

    diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
    index c9445f9..bb74a8e 100644
    a b For definitions of the different versions of RSS, see: 
    1919http://diveintomark.org/archives/2004/02/04/incompatible-rss
    2020"""
    2121
    22 import re
    2322import datetime
     23import urlparse
    2424from django.utils.xmlutils import SimplerXMLGenerator
    2525from django.utils.encoding import force_unicode, iri_to_uri
    2626
    def rfc3339_date(date): 
    4646        return date.strftime('%Y-%m-%dT%H:%M:%SZ')
    4747
    4848def get_tag_uri(url, date):
    49     "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
    50     tag = re.sub('^http://', '', url)
     49    """
     50    Creates a TagURI.
     51   
     52    See http://diveintomark.org/archives/2004/05/28/howto-atom-id
     53    """
     54    url_split = urlparse.urlparse(url)
     55    d = ''
    5156    if date is not None:
    52         tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
    53     tag = re.sub('#', '/', tag)
    54     return u'tag:' + tag
     57        d = ',%s' % date.strftime('%Y-%m-%d')
     58    return u'tag:%s%s:%s/%s' % (url_split.hostname, d, url_split.path, url_split.fragment)
    5559
    5660class SyndicationFeed(object):
    5761    "Base class for all syndication feeds. Subclasses should provide write()"
    class SyndicationFeed(object): 
    6165        to_unicode = lambda s: force_unicode(s, strings_only=True)
    6266        if categories:
    6367            categories = [force_unicode(c) for c in categories]
     68        if ttl is not None:
     69            # Force ints to unicode
     70            ttl = force_unicode(ttl)
    6471        self.feed = {
    6572            'title': to_unicode(title),
    6673            'link': iri_to_uri(link),
    class SyndicationFeed(object): 
    9198        to_unicode = lambda s: force_unicode(s, strings_only=True)
    9299        if categories:
    93100            categories = [to_unicode(c) for c in categories]
     101        if ttl is not None:
     102            # Force ints to unicode
     103            ttl = force_unicode(ttl)
    94104        item = {
    95105            'title': to_unicode(title),
    96106            'link': iri_to_uri(link),
    class RssFeed(SyndicationFeed): 
    186196        handler.endElement(u"rss")
    187197
    188198    def rss_attributes(self):
    189         return {u"version": self._version}
     199        return {u"version": self._version,
     200                u"xmlns:atom": u"http://www.w3.org/2005/Atom"}
    190201
    191202    def write_items(self, handler):
    192203        for item in self.items:
    class RssFeed(SyndicationFeed): 
    198209        handler.addQuickElement(u"title", self.feed['title'])
    199210        handler.addQuickElement(u"link", self.feed['link'])
    200211        handler.addQuickElement(u"description", self.feed['description'])
     212        handler.addQuickElement(u"atom:link", None, {u"rel": u"self", u"href": self.feed['feed_url']})
    201213        if self.feed['language'] is not None:
    202214            handler.addQuickElement(u"language", self.feed['language'])
    203215        for cat in self.feed['categories']:
    class Rss201rev2Feed(RssFeed): 
    235247        elif item["author_email"]:
    236248            handler.addQuickElement(u"author", item["author_email"])
    237249        elif item["author_name"]:
    238             handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
     250            handler.addQuickElement(u"dc:creator", item["author_name"], {u"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
    239251
    240252        if item['pubdate'] is not None:
    241253            handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
  • docs/internals/deprecation.txt

    diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
    index e2d4b6c..3b03ecc 100644
    a b their deprecation, as per the :ref:`Django deprecation policy 
    7777        * The ability to use a function-based test runners will be removed,
    7878          along with the ``django.test.simple.run_tests()`` test runner.
    7979
     80        * The ``views.feed()`` view and ``feeds.Feed`` class in
     81          ``django.contrib.syndication`` have been deprecated since the 1.2
     82          release. The class-based view ``views.Feed`` should be used instead.
     83
    8084    * 2.0
    8185        * ``django.views.defaults.shortcut()``. This function has been moved
    8286          to ``django.contrib.contenttypes.views.shortcut()`` as part of the
  • docs/ref/contrib/syndication.txt

    diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt
    index c276663..9b97ed1 100644
    a b to generate feeds outside of a Web context, or in some other lower-level way. 
    2323The high-level framework
    2424========================
    2525
     26.. versionchanged:: 1.2
     27
    2628Overview
    2729--------
    2830
    29 The high-level feed-generating framework is a view that's hooked to ``/feeds/``
    30 by default. Django uses the remainder of the URL (everything after ``/feeds/``)
    31 to determine which feed to output.
    32 
    33 To create a feed, just write a :class:`~django.contrib.syndication.feeds.Feed`
    34 class and point to it in your :ref:`URLconf <topics-http-urls>`.
    35 
    36 Initialization
    37 --------------
    38 
    39 To activate syndication feeds on your Django site, add this line to your
    40 :ref:`URLconf <topics-http-urls>`::
    41 
    42    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}),
    43 
    44 This tells Django to use the RSS framework to handle all URLs starting with
    45 :file:`"feeds/"`. (You can change that :file:`"feeds/"` prefix to fit your own
    46 needs.)
    47 
    48 This URLconf line has an extra argument: ``{'feed_dict': feeds}``. Use this
    49 extra argument to pass the syndication framework the feeds that should be
    50 published under that URL.
    51 
    52 Specifically, :data:`feed_dict` should be a dictionary that maps a feed's slug
    53 (short URL label) to its :class:`~django.contrib.syndication.feeds.Feed` class.
    54 
    55 You can define the ``feed_dict`` in the URLconf itself. Here's a full example
    56 URLconf::
    57 
    58     from django.conf.urls.defaults import *
    59     from myproject.feeds import LatestEntries, LatestEntriesByCategory
    60 
    61     feeds = {
    62         'latest': LatestEntries,
    63         'categories': LatestEntriesByCategory,
    64     }
    65 
    66     urlpatterns = patterns('',
    67         # ...
    68         (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
    69             {'feed_dict': feeds}),
    70         # ...
    71     )
    72 
    73 The above example registers two feeds:
    74 
    75     * The feed represented by ``LatestEntries`` will live at ``feeds/latest/``.
    76     * The feed represented by ``LatestEntriesByCategory`` will live at
    77       ``feeds/categories/``.
    78 
    79 Once that's set up, you just need to define the
    80 :class:`~django.contrib.syndication.feeds.Feed` classes themselves.
     31The high-level feed-generating framework is supplied by the
     32:class:`~django.contrib.syndication.views.Feed` class. To create a feed, just write a :class:`~django.contrib.syndication.views.Feed`
     33class and point to an instance of it in your :ref:`URLconf <topics-http-urls>`.
    8134
    8235Feed classes
    8336------------
    8437
    85 A :class:`~django.contrib.syndication.feeds.Feed` class is a simple Python class
     38A :class:`~django.contrib.syndication.views.Feed` class is a Python class
    8639that represents a syndication feed. A feed can be simple (e.g., a "site news"
    8740feed, or a basic feed displaying the latest entries of a blog) or more complex
    8841(e.g., a feed displaying all the blog entries in a particular category, where
    8942the category is variable).
    9043
    91 :class:`~django.contrib.syndication.feeds.Feed` classes must subclass
    92 ``django.contrib.syndication.feeds.Feed``. They can live anywhere in your
     44:class:`~django.contrib.syndication.views.Feed` classes subclass
     45``django.contrib.syndication.views.Feed``. They can live anywhere in your
    9346codebase.
    9447
     48Instances of :class:`~django.contrib.syndication.views.Feed` classes are views
     49which can be used in your :ref:`URLconf <topics-http-urls>`.
     50
    9551A simple example
    9652----------------
    9753
    9854This simple example, taken from `chicagocrime.org`_, describes a feed of the
    9955latest five news items::
    10056
    101     from django.contrib.syndication.feeds import Feed
     57    from django.contrib.syndication.views import Feed
    10258    from chicagocrime.models import NewsItem
    10359
    104     class LatestEntries(Feed):
     60    class LatestEntriesFeed(Feed):
    10561        title = "Chicagocrime.org site news"
    10662        link = "/sitenews/"
    10763        description = "Updates on changes and additions to chicagocrime.org."
    108 
     64       
    10965        def items(self):
    11066            return NewsItem.objects.order_by('-pub_date')[:5]
     67       
     68        def item_title(self, item):
     69            return item.title
     70       
     71        def item_description(self, item):
     72            return item.description
     73
     74To connect a URL to this feed, it needs to be put in your :ref:`URLconf <topics-http-urls>`. Here is a full example::
     75
     76    from django.conf.urls.defaults import *
     77    from myproject.feeds import LatestEntriesFeed
     78
     79    urlpatterns = patterns('',
     80        # ...
     81        (r'^latest/feed/$', LatestEntriesFeed()),
     82        # ...
     83    )
    11184
    11285Note:
    11386
    114 * The class subclasses ``django.contrib.syndication.feeds.Feed``.
     87* The class subclasses ``django.contrib.syndication.views.Feed``.
    11588
    11689* :attr:`title`, :attr:`link` and :attr:`description` correspond to the
    11790  standard RSS ``<title>``, ``<link>`` and ``<description>`` elements,
    One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``, 
    133106``<link>`` and ``<description>``. We need to tell the framework what data to put
    134107into those elements.
    135108
    136     * To specify the contents of ``<title>`` and ``<description>``, create
    137       :ref:`Django templates <topics-templates>` called
    138       :file:`feeds/latest_title.html` and
    139       :file:`feeds/latest_description.html`, where :attr:`latest` is the
    140       :attr:`slug` specified in the URLconf for the given feed. Note the
    141       ``.html`` extension is required. The RSS system renders that template for
    142       each item, passing it two template context variables:
     109    * For the contents of ``<title>`` and ``<description>``, Django tries
     110      calling the methods :meth:`item_title()` and :meth:`item_description()` on
     111      the :class:`~django.contrib.syndication.views.Feed` class. They are passed
     112      a single parameter, :attr:`item`, which is the object itself. These are
     113      optional; by default, the unicode representation of the object is used for
     114      both.
     115     
     116      If you want to do any special formatting for either the title or
     117      description, :ref:`Django templates <topics-templates>` can be used
     118      instead. Their paths can be specified with the ``title_template`` and
     119      ``description_template`` attributes on the
     120      :class:`~django.contrib.syndication.views.Feed` class. The templates are
     121      rendered for each item and are passed two template context variables:
    143122
    144123         * ``{{ obj }}`` -- The current object (one of whichever objects you
    145124           returned in :meth:`items()`).
    into those elements. 
    152131           :ref:`RequestSite section of the sites framework documentation
    153132           <requestsite-objects>` for more.
    154133
    155       If you don't create a template for either the title or description, the
    156       framework will use the template ``"{{ obj }}"`` by default -- that is, the
    157       normal string representation of the object. You can also change the names
    158       of these two templates by specifying ``title_template`` and
    159       ``description_template`` as attributes of your
    160       :class:`~django.contrib.syndication.feeds.Feed` class.
     134      See `a complex example`_ below that uses a description template.
    161135
    162136    * To specify the contents of ``<link>``, you have two options. For each item
    163       in :meth:`items()`, Django first tries calling a method
    164       :meth:`item_link()` in the :class:`~django.contrib.syndication.feeds.Feed`
    165       class, passing it a single parameter, :attr:`item`, which is the object
    166       itself. If that method doesn't exist, Django tries executing a
    167       ``get_absolute_url()`` method on that object. . Both
    168       ``get_absolute_url()`` and :meth:`item_link()` should return the item's
    169       URL as a normal Python string. As with ``get_absolute_url()``, the result
    170       of :meth:`item_link()` will be included directly in the URL, so you are
    171       responsible for doing all necessary URL quoting and conversion to ASCII
    172       inside the method itself.
    173 
    174     * For the LatestEntries example above, we could have very simple feed
    175       templates:
    176 
    177       * latest_title.html:
    178 
    179         .. code-block:: html+django
    180 
    181             {{ obj.title }}
    182 
    183       * latest_description.html:
    184 
    185         .. code-block:: html+django
    186 
    187             {{ obj.description }}
     137      in :meth:`items()`, Django first tries calling the
     138      :meth:`item_link()` method on the
     139      :class:`~django.contrib.syndication.views.Feed` class. In a similar way to
     140      the title and description, it is passed it a single parameter,
     141      :attr:`item`. If that method doesn't exist, Django tries executing a
     142      ``get_absolute_url()`` method on that object. Both
     143      :meth:`get_absolute_url()` and :meth:`item_link()` should return the
     144      item's URL as a normal Python string. As with ``get_absolute_url()``, the
     145      result of :meth:`item_link()` will be included directly in the URL, so you
     146      are responsible for doing all necessary URL quoting and conversion to
     147      ASCII inside the method itself.
    188148
    189149.. _chicagocrime.org: http://www.chicagocrime.org/
    190150
    191151A complex example
    192152-----------------
    193153
    194 The framework also supports more complex feeds, via parameters.
     154The framework also supports more complex feeds, via arguments.
    195155
    196156For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every
    197157police beat in Chicago. It'd be silly to create a separate
    198 :class:`~django.contrib.syndication.feeds.Feed` class for each police beat; that
     158:class:`~django.contrib.syndication.views.Feed` class for each police beat; that
    199159would violate the :ref:`DRY principle <dry>` and would couple data to
    200 programming logic. Instead, the syndication framework lets you make generic
    201 feeds that output items based on information in the feed's URL.
     160programming logic. Instead, the syndication framework lets you access the
     161arguments passed from your :ref:`URLconf <topics-http-urls>` so feeds can output
     162items based on information in the feed's URL.
    202163
    203164On chicagocrime.org, the police-beat feeds are accessible via URLs like this:
    204165
    205     * :file:`/rss/beats/0613/` -- Returns recent crimes for beat 0613.
    206     * :file:`/rss/beats/1424/` -- Returns recent crimes for beat 1424.
     166    * :file:`/beats/613/rss/` -- Returns recent crimes for beat 613.
     167    * :file:`/beats/1424/rss/` -- Returns recent crimes for beat 1424.
     168
     169These can be matched with a :ref:`URLconf <topics-http-urls>` line such as::
    207170
    208 The slug here is ``"beats"``. The syndication framework sees the extra URL bits
    209 after the slug -- ``0613`` and ``1424`` -- and gives you a hook to tell it what
    210 those URL bits mean, and how they should influence which items get published in
    211 the feed.
     171    (r'^beats/(?P<beat_id>\d+)/rss/$', BeatFeed()),
    212172
    213 An example makes this clear. Here's the code for these beat-specific feeds::
     173.. versionchanged:: 1.2
     174   Instead of just taking the ``bits`` argument, ``get_object()`` now takes a
     175   ``request`` object and the URL arguments.
    214176
    215     from django.contrib.syndication.feeds import FeedDoesNotExist
    216     from django.core.exceptions import ObjectDoesNotExist
     177Like a view, the arguments in the URL are passed to the :meth:`get_object()`
     178method along with the request object. Here's the code for these beat-specific
     179feeds::
     180
     181    from django.contrib.syndication.views import FeedDoesNotExist
     182    from django.shortcuts import get_object_or_404
    217183
    218184    class BeatFeed(Feed):
    219         def get_object(self, bits):
    220             # In case of "/rss/beats/0613/foo/bar/baz/", or other such clutter,
    221             # check that bits has only one member.
    222             if len(bits) != 1:
    223                 raise ObjectDoesNotExist
    224             return Beat.objects.get(beat__exact=bits[0])
     185        description_template = 'feeds/beat_description.html'
     186   
     187        def get_object(self, request, beat_id):
     188            return get_object_or_404(Beat, pk=beat_id)
    225189
    226190        def title(self, obj):
    227191            return "Chicagocrime.org: Crimes for beat %s" % obj.beat
    228192
    229193        def link(self, obj):
    230             if not obj:
    231                 raise FeedDoesNotExist
    232194            return obj.get_absolute_url()
    233195
    234196        def description(self, obj):
    235197            return "Crimes recently reported in police beat %s" % obj.beat
    236198
    237199        def items(self, obj):
    238            return Crime.objects.filter(beat__id__exact=obj.id).order_by('-crime_date')[:30]
    239 
    240 Here's the basic algorithm the RSS framework follows, given this class and a
    241 request to the URL :file:`/rss/beats/0613/`:
    242 
    243     * The framework gets the URL :file:`/rss/beats/0613/` and notices there's an
    244       extra bit of URL after the slug. It splits that remaining string by the
    245       slash character (``"/"``) and calls the
    246       :class:`~django.contrib.syndication.feeds.Feed` class'
    247       :meth:`get_object()` method, passing it the bits. In this case, bits is
    248       ``['0613']``. For a request to :file:`/rss/beats/0613/foo/bar/`, bits
    249       would be ``['0613', 'foo', 'bar']``.
    250 
    251     * :meth:`get_object()` is responsible for retrieving the given beat, from
    252       the given ``bits``. In this case, it uses the Django database API to
    253       retrieve the beat. Note that :meth:`get_object()` should raise
    254       :exc:`django.core.exceptions.ObjectDoesNotExist` if given invalid
    255       parameters. There's no ``try``/``except`` around the
    256       ``Beat.objects.get()`` call, because it's not necessary; that function
    257       raises :exc:`Beat.DoesNotExist` on failure, and :exc:`Beat.DoesNotExist`
    258       is a subclass of :exc:`ObjectDoesNotExist`. Raising
    259       :exc:`ObjectDoesNotExist` in :meth:`get_object()` tells Django to produce
    260       a 404 error for that request.
    261 
    262       .. versionadded:: 1.0
    263          :meth:`get_object()` can handle the :file:`/rss/beats/` url.
    264 
    265       The :meth:`get_object()` method also has a chance to handle the
    266       :file:`/rss/beats/` url. In this case, :data:`bits` will be an
    267       empty list. In our example, ``len(bits) != 1`` and an
    268       :exc:`ObjectDoesNotExist` exception will be raised, so
    269       :file:`/rss/beats/` will generate a 404 page. But you can handle this case
    270       however you like. For example, you could generate a combined feed for all
    271       beats.
    272 
    273     * To generate the feed's ``<title>``, ``<link>`` and ``<description>``,
    274       Django uses the :meth:`title()`, :meth:`link()` and :meth:`description()`
    275       methods. In the previous example, they were simple string class
    276       attributes, but this example illustrates that they can be either strings
    277       *or* methods. For each of :attr:`title`, :attr:`link` and
    278       :attr:`description`, Django follows this algorithm:
    279 
    280         * First, it tries to call a method, passing the ``obj`` argument, where
    281           ``obj`` is the object returned by :meth:`get_object()`.
    282 
    283         * Failing that, it tries to call a method with no arguments.
    284 
    285         * Failing that, it uses the class attribute.
    286 
    287       Inside the :meth:`link()` method, we handle the possibility that ``obj``
    288       might be ``None``, which can occur when the URL isn't fully specified. In
    289       some cases, you might want to do something else in this case, which would
    290       mean you'd need to check for ``obj`` existing in other methods as well.
    291       (The :meth:`link()` method is called very early in the feed generation
    292       process, so it's a good place to bail out early.)
    293 
    294     * Finally, note that :meth:`items()` in this example also takes the ``obj``
    295       argument. The algorithm for :attr:`items` is the same as described in the
    296       previous step -- first, it tries :meth:`items(obj)`, then :meth:`items()`,
    297       then finally an :attr:`items` class attribute (which should be a list).
     200            return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30]
     201
     202To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django
     203uses the :meth:`title()`, :meth:`link()` and :meth:`description()` methods. In
     204the previous example, they were simple string class attributes, but this example
     205illustrates that they can be either strings *or* methods. For each of
     206:attr:`title`, :attr:`link` and :attr:`description`, Django follows this
     207algorithm:
     208
     209    * First, it tries to call a method, passing the ``obj`` argument, where
     210      ``obj`` is the object returned by :meth:`get_object()`.
     211
     212    * Failing that, it tries to call a method with no arguments.
     213
     214    * Failing that, it uses the class attribute.
     215
     216Also note that :meth:`items()` also follows the same algorithm -- first, it
     217tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items`
     218class attribute (which should be a list).
     219
     220We are using a template for the item descriptions. It can be very simple:
     221
     222.. code-block:: html+django
     223
     224    {{ obj.description }}
     225
     226However, you are free to add formatting as required.
    298227
    299228The ``ExampleFeed`` class below gives full documentation on methods and
    300 attributes of :class:`~django.contrib.syndication.feeds.Feed` classes.
     229attributes of :class:`~django.contrib.syndication.views.Feed` classes.
    301230
    302231Specifying the type of feed
    303232---------------------------
    Specifying the type of feed 
    305234By default, feeds produced in this framework use RSS 2.0.
    306235
    307236To change that, add a ``feed_type`` attribute to your
    308 :class:`~django.contrib.syndication.feeds.Feed` class, like so::
     237:class:`~django.contrib.syndication.views.Feed` class, like so::
    309238
    310239    from django.utils.feedgenerator import Atom1Feed
    311240
    Publishing Atom and RSS feeds in tandem 
    353282
    354283Some developers like to make available both Atom *and* RSS versions of their
    355284feeds. That's easy to do with Django: Just create a subclass of your
    356 :class:`~django.contrib.syndication.feeds.Feed`
     285:class:`~django.contrib.syndication.views.Feed`
    357286class and set the :attr:`feed_type` to something different. Then update your
    358287URLconf to add the extra versions.
    359288
    360289Here's a full example::
    361290
    362     from django.contrib.syndication.feeds import Feed
     291    from django.contrib.syndication.views import Feed
    363292    from chicagocrime.models import NewsItem
    364293    from django.utils.feedgenerator import Atom1Feed
    365294
    Here's a full example:: 
    381310    a feed-level "description," but they *do* provide for a "subtitle."
    382311
    383312    If you provide a :attr:`description` in your
    384     :class:`~django.contrib.syndication.feeds.Feed` class, Django will *not*
     313    :class:`~django.contrib.syndication.views.Feed` class, Django will *not*
    385314    automatically put that into the :attr:`subtitle` element, because a
    386315    subtitle and description are not necessarily the same thing. Instead, you
    387316    should define a :attr:`subtitle` attribute.
    And the accompanying URLconf:: 
    394323    from django.conf.urls.defaults import *
    395324    from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
    396325
    397     feeds = {
    398         'rss': RssSiteNewsFeed,
    399         'atom': AtomSiteNewsFeed,
    400     }
    401 
    402326    urlpatterns = patterns('',
    403327        # ...
    404         (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
    405             {'feed_dict': feeds}),
     328        (r'^sitenews/rss/$', RssSiteNewsFeed()),
     329        (r'^sitenews/atom/$', AtomSiteNewsFeed()),
    406330        # ...
    407331    )
    408332
    409333Feed class reference
    410334--------------------
    411335
    412 .. class:: django.contrib.syndication.feeds.Feed
     336.. class:: django.contrib.syndication.views.Feed
    413337
    414338This example illustrates all possible attributes and methods for a
    415 :class:`~django.contrib.syndication.feeds.Feed` class::
     339:class:`~django.contrib.syndication.views.Feed` class::
    416340
    417     from django.contrib.syndication.feeds import Feed
     341    from django.contrib.syndication.views import Feed
    418342    from django.utils import feedgenerator
    419343
    420344    class ExampleFeed(Feed):
    This example illustrates all possible attributes and methods for a 
    430354        # TEMPLATE NAMES -- Optional. These should be strings representing
    431355        # names of Django templates that the system should use in rendering the
    432356        # title and description of your feed items. Both are optional.
    433         # If you don't specify one, or either, Django will use the template
    434         # 'feeds/SLUG_title.html' and 'feeds/SLUG_description.html', where SLUG
    435         # is the slug you specify in the URL.
     357        # If one is not specified, the item_title() or item_description()
     358        # methods are used instead.
    436359
    437360        title_template = None
    438361        description_template = None
    This example illustrates all possible attributes and methods for a 
    572495        # COPYRIGHT NOTICE -- One of the following three is optional. The
    573496        # framework looks for them in this order.
    574497
    575         def copyright(self, obj):
     498        def feed_copyright(self, obj):
    576499            """
    577500            Takes the object returned by get_object() and returns the feed's
    578501            copyright notice as a normal Python string.
    579502            """
    580503
    581         def copyright(self):
     504        def feed_copyright(self):
    582505            """
    583506            Returns the feed's copyright notice as a normal Python string.
    584507            """
    585508
    586         copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
     509        feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
    587510
    588511        # TTL -- One of the following three is optional. The framework looks
    589512        # for them in this order. Ignored for Atom feeds.
    This example illustrates all possible attributes and methods for a 
    620543        # GET_OBJECT -- This is required for feeds that publish different data
    621544        # for different URL parameters. (See "A complex example" above.)
    622545
    623         def get_object(self, bits):
     546        def get_object(self, request, *args, **kwargs):
    624547            """
    625             Takes a list of strings gleaned from the URL and returns an object
    626             represented by this feed. Raises
     548            Takes the current request and the arguments from the URL, and
     549            returns an object represented by this feed. Raises
    627550            django.core.exceptions.ObjectDoesNotExist on error.
    628551            """
     552       
     553        # ITEM TITLE AND DESCRIPTION -- If title_template or
     554        # description_template are not defined, these are used instead. Both are
     555        # optional, by default they will use the unicode representation of the
     556        # item.
     557       
     558        def item_title(self, item):
     559            """
     560            Takes an item, as returned by items(), and returns the item's
     561            title as a normal Python string.
     562            """
     563
     564        def item_title(self):
     565            """
     566            Returns the title for every item in the feed.
     567            """
     568
     569        item_title = 'Breaking News: Nothing Happening' # Hard-coded title.
     570       
     571        def item_description(self, item):
     572            """
     573            Takes an item, as returned by items(), and returns the item's
     574            description as a normal Python string.
     575            """
     576
     577        def item_description(self):
     578            """
     579            Returns the description for every item in the feed.
     580            """
    629581
     582        item_description = 'A description of the item.' # Hard-coded description.
     583       
    630584        # ITEM LINK -- One of these three is required. The framework looks for
    631585        # them in this order.
    632586
    This example illustrates all possible attributes and methods for a 
    686640
    687641        item_author_email = 'test@example.com' # Hard-coded author e-mail.
    688642
    689         # ITEM AUTHOR LINK --One of the following three is optional. The
     643        # ITEM AUTHOR LINK -- One of the following three is optional. The
    690644        # framework looks for them in this order. In each case, the URL should
    691645        # include the "http://" and domain name.
    692646        #
  • docs/releases/1.2.txt

    diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt
    index f83e4dc..1b08486 100644
    a b approach. Old style function-based test runners will still work, but 
    386386should be updated to use the new :ref:`class-based runners
    387387<topics-testing-test_runner>`.
    388388
     389``Feed`` in ``django.contrib.syndication.feeds``
     390------------------------------------------------
     391
     392The ``Feed`` class in ``syndication.feeds`` has been replaced by
     393the ``Feed`` class in ``syndication.views``, and will be removed in Django 1.4.
     394
     395The new class has an almost identical API, but allows instances to be used as
     396views. For example, consider the use of the old framework in the following
     397:ref:`URLconf <topics-http-urls>`::
     398
     399    from django.conf.urls.defaults import *
     400    from myproject.feeds import LatestEntries, LatestEntriesByCategory
     401
     402    feeds = {
     403        'latest': LatestEntries,
     404        'categories': LatestEntriesByCategory,
     405    }
     406
     407    urlpatterns = patterns('',
     408        # ...
     409        (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
     410            {'feed_dict': feeds}),
     411        # ...
     412    )
     413
     414This should be replaced by::
     415
     416    from django.conf.urls.defaults import *
     417    from myproject.feeds import LatestEntries, LatestEntriesByCategory
     418
     419    urlpatterns = patterns('',
     420        # ...
     421        (r'^feeds/latest/$', LatestEntries()),
     422        (r'^feeds/categories/(?P<category_id>\d+)/$', LatestEntriesByCategory()),
     423        # ...
     424    )
     425
     426If you used the ``feed()`` view, the ``LatestEntries`` class would not need to
     427be modified apart from subclassing the new ``Feed`` class. However,
     428``LatestEntriesByCategory`` used the ``get_object()`` method with the ``bits``
     429argument to specify a specific category to show. Much like a view, the
     430``get_object()`` method now takes a ``request`` and arguments from the URL, so
     431it would look like this::
     432
     433    from django.contrib.syndication.views import Feed
     434    from django.shortcuts import get_object_or_404
     435    from myproject.models import Category
     436
     437    class LatestEntriesByCategory(Feed):
     438        def get_object(self, request, category_id):
     439            return get_object_or_404(Category, id=category_id)
     440       
     441        # ...
     442
     443Additionally, the ``get_feed()`` method on ``Feed`` classes now take different
     444arguments, which may impact you if you use the ``Feed`` classes directly.
     445Instead of just taking an optional ``url`` argument, it now takes two arguments:
     446the object returned by its own ``get_object()`` method, and the current
     447``request`` object.
     448
     449To take into account ``Feed`` classes not being initialized for each
     450request, the ``__init__()`` method now takes no arguments by default.
     451Previously it would have taken the ``slug`` from the URL and the ``request``
     452object.
     453
     454In previous versions of Django, the ``feed_copyright()`` method was incorrectly 
     455documented as ``copyright()``. Change any ``copyright()`` methods you have on
     456your ``Feed`` classes to ``feed_copyright()``.
     457
     458In accordance with `RSS best practices`_, RSS feeds will now include an
     459``atom:link`` element. You may need to update your tests to take this into
     460account.
     461
     462For more information, see the full :ref:`syndication framework documentation
     463<ref-contrib-syndication>`.
     464
     465.. _RSS best practices: http://www.rssboard.org/rss-profile
     466
    389467What's new in Django 1.2
    390468========================
    391469
    reusable, encapsulated validation behavior. Note, however, that 
    549627validation must still be performed explicitly. Simply invoking a model
    550628instance's ``save()`` method will not perform any validation of the
    551629instance's data.
     630
     631Syndication feeds as views
     632--------------------------
     633
     634:ref:`Syndication feeds <ref-contrib-syndication>` can now be used directly as
     635views in your :ref:`URLconf <topics-http-urls>`. Previously, all the feeds on a
     636site had to live beneath a single URL, which produced ugly, unnatural URLs.
     637
     638For example, suppose you listed blog posts for the "django" tag under
     639``/blog/django/``. In previous versions of Django, a feed for those posts would
     640have had to be something like ``/feeds/blog-tag/django/``. In Django 1.2, it
     641can simply be ``/blog/django/feed/``.
     642
     643Like any other view, feeds are now passed a ``request`` object, so you can
     644do user based access control amongst other things.
     645
     646Also, you no longer need to create templates for your feed item titles and
     647descriptions. You can set these with the ``item_title()`` and
     648``item_description()`` methods.
     649
  • tests/regressiontests/syndication/feeds.py

    diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py
    index 79837f9..8648cec 100644
    a b  
     1from django.contrib.syndication import feeds, views
    12from django.core.exceptions import ObjectDoesNotExist
    2 from django.contrib.syndication import feeds
    3 from django.utils.feedgenerator import Atom1Feed
    4 from django.utils import tzinfo
     3from django.utils import feedgenerator, tzinfo
     4from models import Article, Entry
    55
    6 class ComplexFeed(feeds.Feed):
    7     def get_object(self, bits):
    8         if len(bits) != 1:
     6
     7class ComplexFeed(views.Feed):
     8    def get_object(self, request, foo=None):
     9        if foo is not None:
    910            raise ObjectDoesNotExist
    1011        return None
    1112
    12 class TestRssFeed(feeds.Feed):
    13     link = "/blog/"
     13
     14class TestRss2Feed(views.Feed):
    1415    title = 'My blog'
     16    description = 'A more thorough description of my blog.'
     17    link = '/blog/'
     18    feed_guid = '/foo/bar/1234'
     19    author_name = 'Sally Smith'
     20    author_email = 'test@example.com'
     21    author_link = 'http://www.example.com/'
     22    categories = ('python', 'django')
     23    feed_copyright = 'Copyright (c) 2007, Sally Smith'
     24    ttl = 600
    1525   
    1626    def items(self):
    17         from models import Entry
    1827        return Entry.objects.all()
    19        
    20     def item_link(self, item):
    21         return "/blog/%s/" % item.pk
     28   
     29    def item_description(self, item):
     30        return "Overridden description: %s" % item
     31   
     32    def item_pubdate(self, item):
     33        return item.date
     34   
     35    item_author_name = 'Sally Smith'
     36    item_author_email = 'test@example.com'
     37    item_author_link = 'http://www.example.com/'
     38    item_categories = ('python', 'testing')
     39    item_copyright = 'Copyright (c) 2007, Sally Smith'
     40
     41
     42class TestRss091Feed(TestRss2Feed):
     43    feed_type = feedgenerator.RssUserland091Feed
     44
    2245
    23 class TestAtomFeed(TestRssFeed):
    24     feed_type = Atom1Feed
     46class TestAtomFeed(TestRss2Feed):
     47    feed_type = feedgenerator.Atom1Feed
     48    subtitle = TestRss2Feed.description
    2549
    26 class MyCustomAtom1Feed(Atom1Feed):
     50
     51class ArticlesFeed(TestRss2Feed):
     52    """
     53    A feed to test no link being defined. Articles have no get_absolute_url()
     54    method, and item_link() is not defined.
     55    """
     56    def items(self):
     57        return Article.objects.all()
     58
     59
     60class TestEnclosureFeed(TestRss2Feed):
     61    pass
     62
     63
     64class TemplateFeed(TestRss2Feed):
     65    """
     66    A feed to test defining item titles and descriptions with templates.
     67    """
     68    title_template = 'syndication/title.html'
     69    description_template = 'syndication/description.html'
     70   
     71    # Defining a template overrides any item_title definition
     72    def item_title(self):
     73        return "Not in a template"
     74
     75
     76class NaiveDatesFeed(TestAtomFeed):
     77    """
     78    A feed with naive (non-timezone-aware) dates.
     79    """
     80    def item_pubdate(self, item):
     81        return item.date
     82
     83
     84class TZAwareDatesFeed(TestAtomFeed):
     85    """
     86    A feed with timezone-aware dates.
     87    """
     88    def item_pubdate(self, item):
     89        # Provide a weird offset so that the test can know it's getting this
     90        # specific offset and not accidentally getting on from
     91        # settings.TIME_ZONE.
     92        return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
     93
     94
     95class TestFeedUrlFeed(TestAtomFeed):
     96    feed_url = 'http://example.com/customfeedurl/'
     97
     98
     99class MyCustomAtom1Feed(feedgenerator.Atom1Feed):
    27100    """
    28101    Test of a custom feed generator class.
    29102    """   
    class MyCustomAtom1Feed(Atom1Feed): 
    44117    def add_item_elements(self, handler, item):
    45118        super(MyCustomAtom1Feed, self).add_item_elements(handler, item)
    46119        handler.addQuickElement(u'ministry', u'silly walks')
    47    
     120
     121
    48122class TestCustomFeed(TestAtomFeed):
    49123    feed_type = MyCustomAtom1Feed
     124
     125
     126class DeprecatedComplexFeed(feeds.Feed):
     127    def get_object(self, bits):
     128        if len(bits) != 1:
     129            raise ObjectDoesNotExist
     130        return None
     131
     132
     133class DeprecatedRssFeed(feeds.Feed):
     134    link = "/blog/"
     135    title = 'My blog'
    50136   
    51 class NaiveDatesFeed(TestAtomFeed):
    52     """
    53     A feed with naive (non-timezone-aware) dates.
    54     """
    55     def item_pubdate(self, item):
    56         return item.date
     137    def items(self):
     138        return Entry.objects.all()
    57139       
    58 class TZAwareDatesFeed(TestAtomFeed):
    59     """
    60     A feed with timezone-aware dates.
    61     """
    62     def item_pubdate(self, item):
    63         # Provide a weird offset so that the test can know it's getting this
    64         # specific offset and not accidentally getting on from
    65         # settings.TIME_ZONE.
    66         return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
    67  No newline at end of file
     140    def item_link(self, item):
     141        return "/blog/%s/" % item.pk
     142
  • tests/regressiontests/syndication/fixtures/feeddata.json

    diff --git a/tests/regressiontests/syndication/fixtures/feeddata.json b/tests/regressiontests/syndication/fixtures/feeddata.json
    index 375ee16..4a5c022 100644
    a b  
    3030      "title": "A & B < C > D",
    3131      "date": "2008-01-03 13:30:00"
    3232    }
     33  },
     34  {
     35    "model": "syndication.article",
     36    "pk": 1,
     37    "fields": {
     38      "title": "My first article",
     39      "entry": "1"
     40    }
    3341  }
    34 ]
    35  No newline at end of file
     42]
  • tests/regressiontests/syndication/models.py

    diff --git a/tests/regressiontests/syndication/models.py b/tests/regressiontests/syndication/models.py
    index 99e14ad..19e645a 100644
    a b  
    11from django.db import models
    2 
     2 
    33class Entry(models.Model):
    44    title = models.CharField(max_length=200)
    55    date = models.DateTimeField()
    66   
     7    class Meta:
     8        ordering = ('date',)
     9   
    710    def __unicode__(self):
    8         return self.title
    9  No newline at end of file
     11        return self.title
     12   
     13    def get_absolute_url(self):
     14        return "/blog/%s/" % self.pk
     15
     16
     17class Article(models.Model):
     18    title = models.CharField(max_length=200)
     19    entry = models.ForeignKey(Entry)
     20
     21    def __unicode__(self):
     22        return self.title
     23
  • new file tests/regressiontests/syndication/templates/syndication/description.html

    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
    - +  
     1Description in your templates: {{ obj }}
     2 No newline at end of file
  • new file tests/regressiontests/syndication/templates/syndication/title.html

    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
    - +  
     1Title in your templates: {{ obj }}
     2 No newline at end of file
  • tests/regressiontests/syndication/tests.py

    diff --git a/tests/regressiontests/syndication/tests.py b/tests/regressiontests/syndication/tests.py
    index 816cb44..2c304c2 100644
    a b  
    1 # -*- coding: utf-8 -*-
    2 
    31import datetime
    4 from xml.dom import minidom
     2from django.contrib.syndication import feeds, views
     3from django.core.exceptions import ImproperlyConfigured
    54from django.test import TestCase
    6 from django.test.client import Client
    75from django.utils import tzinfo
    86from models import Entry
     7from xml.dom import minidom
     8
    99try:
    1010    set
    1111except NameError:
    1212    from sets import Set as set
    1313
    14 class SyndicationFeedTest(TestCase):
     14class FeedTestCase(TestCase):
    1515    fixtures = ['feeddata.json']
    1616
    1717    def assertChildNodes(self, elem, expected):
    1818        actual = set([n.nodeName for n in elem.childNodes])
    1919        expected = set(expected)
    2020        self.assertEqual(actual, expected)
     21   
     22    def assertChildNodeContent(self, elem, expected):
     23        for k, v in expected.items():
     24            self.assertEqual(
     25                elem.getElementsByTagName(k)[0].firstChild.wholeText, v)
     26   
     27    def assertCategories(self, elem, expected):
     28        self.assertEqual(set(i.firstChild.wholeText for i in elem.childNodes if i.nodeName == 'category'), set(expected));
    2129
    22     def test_rss_feed(self):
    23         response = self.client.get('/syndication/feeds/rss/')
     30######################################
     31# Feed view
     32######################################
     33
     34class SyndicationFeedTest(FeedTestCase):
     35    """
     36    Tests for the high-level syndication feed framework.
     37    """
     38   
     39    def test_rss2_feed(self):
     40        """
     41        Test the structure and content of feeds generated by Rss201rev2Feed.
     42        """
     43        response = self.client.get('/syndication/rss2/')
    2444        doc = minidom.parseString(response.content)
    2545       
    2646        # Making sure there's only 1 `rss` element and that the correct
    class SyndicationFeedTest(TestCase): 
    3555        chan_elem = feed.getElementsByTagName('channel')
    3656        self.assertEqual(len(chan_elem), 1)
    3757        chan = chan_elem[0]
    38         self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item'])
     58        self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category'])
     59        self.assertChildNodeContent(chan, {
     60            'title': 'My blog',
     61            'description': 'A more thorough description of my blog.',
     62            'link': 'http://example.com/blog/',
     63            'language': 'en',
     64            'lastBuildDate': 'Thu, 03 Jan 2008 13:30:00 -0600',
     65            #'atom:link': '',
     66            'ttl': '600',
     67            'copyright': 'Copyright (c) 2007, Sally Smith',
     68        })
     69        self.assertCategories(chan, ['python', 'django']);
     70       
     71        # Ensure the content of the channel is correct
     72        self.assertChildNodeContent(chan, {
     73            'title': 'My blog',
     74            'link': 'http://example.com/blog/',
     75        })
     76       
     77        # Check feed_url is passed
     78        self.assertEqual(
     79            chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
     80            'http://example.com/syndication/rss2/'
     81        )
     82       
     83        items = chan.getElementsByTagName('item')
     84        self.assertEqual(len(items), Entry.objects.count())
     85        self.assertChildNodeContent(items[0], {
     86            'title': 'My first entry',
     87            'description': 'Overridden description: My first entry',
     88            'link': 'http://example.com/blog/1/',
     89            'guid': 'http://example.com/blog/1/',
     90            'pubDate': 'Tue, 01 Jan 2008 12:30:00 -0600',
     91            'author': 'test@example.com (Sally Smith)',
     92        })
     93        self.assertCategories(items[0], ['python', 'testing']);
     94       
     95        for item in items:
     96            self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'category', 'pubDate', 'author'])
    3997   
     98    def test_rss091_feed(self):
     99        """
     100        Test the structure and content of feeds generated by RssUserland091Feed.
     101        """
     102        response = self.client.get('/syndication/rss091/')
     103        doc = minidom.parseString(response.content)
     104       
     105        # Making sure there's only 1 `rss` element and that the correct
     106        # RSS version was specified.
     107        feed_elem = doc.getElementsByTagName('rss')
     108        self.assertEqual(len(feed_elem), 1)
     109        feed = feed_elem[0]
     110        self.assertEqual(feed.getAttribute('version'), '0.91')
     111       
     112        # Making sure there's only one `channel` element w/in the
     113        # `rss` element.
     114        chan_elem = feed.getElementsByTagName('channel')
     115        self.assertEqual(len(chan_elem), 1)
     116        chan = chan_elem[0]
     117        self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category'])
     118       
     119        # Ensure the content of the channel is correct
     120        self.assertChildNodeContent(chan, {
     121            'title': 'My blog',
     122            'link': 'http://example.com/blog/',
     123        })
     124        self.assertCategories(chan, ['python', 'django'])
     125       
     126        # Check feed_url is passed
     127        self.assertEqual(
     128            chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
     129            'http://example.com/syndication/rss091/'
     130        )
     131       
    40132        items = chan.getElementsByTagName('item')
    41133        self.assertEqual(len(items), Entry.objects.count())
     134        self.assertChildNodeContent(items[0], {
     135            'title': 'My first entry',
     136            'description': 'Overridden description: My first entry',
     137            'link': 'http://example.com/blog/1/',
     138        })
    42139        for item in items:
    43             self.assertChildNodes(item, ['title', 'link', 'description', 'guid'])
     140            self.assertChildNodes(item, ['title', 'link', 'description'])
     141            self.assertCategories(item, [])
    44142   
    45143    def test_atom_feed(self):
    46         response = self.client.get('/syndication/feeds/atom/')
    47         doc = minidom.parseString(response.content)
     144        """
     145        Test the structure and content of feeds generated by Atom1Feed.
     146        """
     147        response = self.client.get('/syndication/atom/')
     148        feed = minidom.parseString(response.content).firstChild
    48149       
    49         feed = doc.firstChild
    50150        self.assertEqual(feed.nodeName, 'feed')
    51151        self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom')
    52         self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry'])       
     152        self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'rights', 'category', 'author'])
     153        for link in feed.getElementsByTagName('link'):
     154            if link.getAttribute('rel') == 'self':
     155                self.assertEqual(link.getAttribute('href'), 'http://example.com/syndication/atom/')
    53156       
    54157        entries = feed.getElementsByTagName('entry')
    55158        self.assertEqual(len(entries), Entry.objects.count())
    56159        for entry in entries:
    57             self.assertChildNodes(entry, ['title', 'link', 'id', 'summary'])
     160            self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'category', 'updated', 'rights', 'author'])
    58161            summary = entry.getElementsByTagName('summary')[0]
    59162            self.assertEqual(summary.getAttribute('type'), 'html')
    60163   
    61164    def test_custom_feed_generator(self):
    62         response = self.client.get('/syndication/feeds/custom/')
    63         doc = minidom.parseString(response.content)
     165        response = self.client.get('/syndication/custom/')
     166        feed = minidom.parseString(response.content).firstChild
    64167       
    65         feed = doc.firstChild
    66168        self.assertEqual(feed.nodeName, 'feed')
    67169        self.assertEqual(feed.getAttribute('django'), 'rocks')
    68         self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry', 'spam'])       
     170        self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'spam', 'rights', 'category', 'author'])
    69171       
    70172        entries = feed.getElementsByTagName('entry')
    71173        self.assertEqual(len(entries), Entry.objects.count())
    72174        for entry in entries:
    73175            self.assertEqual(entry.getAttribute('bacon'), 'yum')
    74             self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry'])
     176            self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry', 'rights', 'author', 'updated', 'category'])
    75177            summary = entry.getElementsByTagName('summary')[0]
    76178            self.assertEqual(summary.getAttribute('type'), 'html')
    77        
    78     def test_complex_base_url(self):
    79         """
    80         Tests that that the base url for a complex feed doesn't raise a 500
    81         exception.
    82         """
    83         response = self.client.get('/syndication/feeds/complex/')
    84         self.assertEquals(response.status_code, 404)
    85 
     179   
    86180    def test_title_escaping(self):
    87181        """
    88182        Tests that titles are escaped correctly in RSS feeds.
    89183        """
    90         response = self.client.get('/syndication/feeds/rss/')
     184        response = self.client.get('/syndication/rss2/')
    91185        doc = minidom.parseString(response.content)
    92186        for item in doc.getElementsByTagName('item'):
    93187            link = item.getElementsByTagName('link')[0]
    class SyndicationFeedTest(TestCase): 
    101195        """
    102196        # Naive date times passed in get converted to the local time zone, so
    103197        # check the recived zone offset against the local offset.
    104         response = self.client.get('/syndication/feeds/naive-dates/')
     198        response = self.client.get('/syndication/naive-dates/')
    105199        doc = minidom.parseString(response.content)
    106200        updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText       
    107201        tz = tzinfo.LocalTimezone(datetime.datetime.now())
    class SyndicationFeedTest(TestCase): 
    112206        """
    113207        Test that datetimes with timezones don't get trodden on.
    114208        """
    115         response = self.client.get('/syndication/feeds/aware-dates/')
     209        response = self.client.get('/syndication/aware-dates/')
    116210        doc = minidom.parseString(response.content)
    117211        updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
    118212        self.assertEqual(updated[-6:], '+00:42')
    119        
    120  No newline at end of file
     213   
     214    def test_feed_url(self):
     215        """
     216        Test that the feed_url can be overridden.
     217        """
     218        response = self.client.get('/syndication/feedurl/')
     219        doc = minidom.parseString(response.content)
     220        for link in doc.getElementsByTagName('link'):
     221            if link.getAttribute('rel') == 'self':
     222                self.assertEqual(link.getAttribute('href'), 'http://example.com/customfeedurl/')
     223   
     224    def test_item_link_error(self):
     225        """
     226        Test that a ImproperlyConfigured is raised if no link could be found
     227        for the item(s).
     228        """
     229        self.assertRaises(ImproperlyConfigured,
     230                          self.client.get,
     231                          '/syndication/articles/')
     232   
     233    def test_template_feed(self):
     234        """
     235        Test that the item title and description can be overridden with
     236        templates.
     237        """
     238        response = self.client.get('/syndication/template/')
     239        doc = minidom.parseString(response.content)
     240        feed = doc.getElementsByTagName('rss')[0]
     241        chan = feed.getElementsByTagName('channel')[0]
     242        items = chan.getElementsByTagName('item')
     243       
     244        self.assertChildNodeContent(items[0], {
     245            'title': 'Title in your templates: My first entry',
     246            'description': 'Description in your templates: My first entry',
     247            'link': 'http://example.com/blog/1/',
     248        })
     249   
     250    def test_add_domain(self):
     251        """
     252        Test add_domain() prefixes domains onto the correct URLs.
     253        """
     254        self.assertEqual(
     255            views.add_domain('example.com', '/foo/?arg=value'),
     256            'http://example.com/foo/?arg=value'
     257        )
     258        self.assertEqual(
     259            views.add_domain('example.com', 'http://djangoproject.com/doc/'),
     260            'http://djangoproject.com/doc/'
     261        )
     262        self.assertEqual(
     263            views.add_domain('example.com', 'https://djangoproject.com/doc/'),
     264            'https://djangoproject.com/doc/'
     265        )
     266        self.assertEqual(
     267            views.add_domain('example.com', 'mailto:uhoh@djangoproject.com'),
     268            'mailto:uhoh@djangoproject.com'
     269        )
     270
     271
     272######################################
     273# Deprecated feeds
     274######################################
     275
     276class DeprecatedSyndicationFeedTest(FeedTestCase):
     277    """
     278    Tests for the deprecated API (feed() view and the feed_dict etc).
     279    """
     280   
     281    def test_empty_feed_dict(self):
     282        """
     283        Test that an empty feed_dict raises a 404.
     284        """
     285        response = self.client.get('/syndication/depr-feeds-empty/aware-dates/')
     286        self.assertEquals(response.status_code, 404)
     287
     288    def test_nonexistent_slug(self):
     289        """
     290        Test that a non-existent slug raises a 404.
     291        """
     292        response = self.client.get('/syndication/depr-feeds/foobar/')
     293        self.assertEquals(response.status_code, 404)
     294   
     295    def test_rss_feed(self):
     296        """
     297        A simple test for Rss201rev2Feed feeds generated by the deprecated
     298        system.
     299        """
     300        response = self.client.get('/syndication/depr-feeds/rss/')
     301        doc = minidom.parseString(response.content)
     302        feed = doc.getElementsByTagName('rss')[0]
     303        self.assertEqual(feed.getAttribute('version'), '2.0')
     304       
     305        chan = feed.getElementsByTagName('channel')[0]
     306        self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link'])
     307   
     308        items = chan.getElementsByTagName('item')
     309        self.assertEqual(len(items), Entry.objects.count())
     310   
     311    def test_complex_base_url(self):
     312        """
     313        Tests that the base url for a complex feed doesn't raise a 500
     314        exception.
     315        """
     316        response = self.client.get('/syndication/depr-feeds/complex/')
     317        self.assertEquals(response.status_code, 404)
     318
  • tests/regressiontests/syndication/urls.py

    diff --git a/tests/regressiontests/syndication/urls.py b/tests/regressiontests/syndication/urls.py
    index ec45026..b4e0bcb 100644
    a b  
    1 import feeds
    2 from django.conf.urls.defaults import patterns
     1from django.conf.urls.defaults import *
    32
     3import feeds
     4 
    45feed_dict = {
    5     'complex': feeds.ComplexFeed,
    6     'rss': feeds.TestRssFeed,
    7     'atom': feeds.TestAtomFeed,
    8     'custom': feeds.TestCustomFeed,
    9     'naive-dates': feeds.NaiveDatesFeed,
    10     'aware-dates': feeds.TZAwareDatesFeed,   
     6    'complex': feeds.DeprecatedComplexFeed,
     7    'rss': feeds.DeprecatedRssFeed,
    118}
    12 urlpatterns = patterns('',
    13     (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feed_dict})
     9
     10urlpatterns = patterns('django.contrib.syndication.views',
     11    (r'^complex/(?P<foo>.*)/$', feeds.ComplexFeed()),
     12    (r'^rss2/$', feeds.TestRss2Feed()),
     13    (r'^rss091/$', feeds.TestRss091Feed()),
     14    (r'^atom/$', feeds.TestAtomFeed()),
     15    (r'^custom/$', feeds.TestCustomFeed()),
     16    (r'^naive-dates/$', feeds.NaiveDatesFeed()),
     17    (r'^aware-dates/$', feeds.TZAwareDatesFeed()),
     18    (r'^feedurl/$', feeds.TestFeedUrlFeed()),
     19    (r'^articles/$', feeds.ArticlesFeed()),
     20    (r'^template/$', feeds.TemplateFeed()),
     21   
     22    (r'^depr-feeds/(?P<url>.*)/$', 'feed', {'feed_dict': feed_dict}),
     23    (r'^depr-feeds-empty/(?P<url>.*)/$', 'feed', {'feed_dict': None}),
    1424)
  • new file tests/regressiontests/utils/feedgenerator.py

    diff --git a/tests/regressiontests/utils/feedgenerator.py b/tests/regressiontests/utils/feedgenerator.py
    new file mode 100644
    index 0000000..3274d69
    - +  
     1import datetime
     2from django.utils import feedgenerator, tzinfo
     3
     4class FeedgeneratorTest(TestCase):
     5    """
     6    Tests for the low-level syndication feed framework.
     7    """
     8   
     9    def test_get_tag_uri(self):
     10        """
     11        Test get_tag_uri() correctly generates TagURIs.
     12        """
     13        self.assertEqual(
     14            feedgenerator.get_tag_uri('http://example.org/foo/bar#headline', datetime.date(2004, 10, 25)),
     15            u'tag:example.org,2004-10-25:/foo/bar/headline')
     16       
     17    def test_get_tag_uri_with_port(self):
     18        """
     19        Test that get_tag_uri() correctly generates TagURIs from URLs with port
     20        numbers.
     21        """
     22        self.assertEqual(
     23            feedgenerator.get_tag_uri('http://www.example.org:8000/2008/11/14/django#headline', datetime.datetime(2008, 11, 14, 13, 37, 0)),
     24            u'tag:www.example.org,2008-11-14:/2008/11/14/django/headline')
     25   
     26    def test_rfc2822_date(self):
     27        """
     28        Test rfc2822_date() correctly formats datetime objects.
     29        """
     30        self.assertEqual(
     31            feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0)),
     32            "Fri, 14 Nov 2008 13:37:00 -0000"
     33        )
     34       
     35    def test_rfc2822_date_with_timezone(self):
     36        """
     37        Test rfc2822_date() correctly formats datetime objects with tzinfo.
     38        """
     39        self.assertEqual(
     40            feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=60)))),
     41            "Fri, 14 Nov 2008 13:37:00 +0100"
     42        )
     43   
     44    def test_rfc3339_date(self):
     45        """
     46        Test rfc3339_date() correctly formats datetime objects.
     47        """
     48        self.assertEqual(
     49            feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0)),
     50            "2008-11-14T13:37:00Z"
     51        )
     52   
     53    def test_rfc3339_date_with_timezone(self):
     54        """
     55        Test rfc3339_date() correctly formats datetime objects with tzinfo.
     56        """
     57        self.assertEqual(
     58            feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=120)))),
     59            "2008-11-14T13:37:00+02:00"
     60        )
     61
Back to Top