Ticket #12403: syndication-views-3.diff

File syndication-views-3.diff, 73.8 KB (added by Ben Firshman, 14 years ago)

Release notes and new deprecation warnings

  • AUTHORS

    diff --git a/AUTHORS b/AUTHORS
    index c8d91f7..d3fb3bc 100644
    a b answer newbie questions, and generally made Django that much better:  
    162162    Afonso Fernández Nogueira <fonzzo.django@gmail.com>
    163163    J. Pablo Fernandez <pupeno@pupeno.com>
    164164    Maciej Fijalkowski
     165    Ben Firshman <ben@firshman.co.uk>
    165166    Matthew Flanagan <http://wadofstuff.blogspot.com>
    166167    Eric Floehr <eric@intellovations.com>
    167168    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 ade43d7..06b5f73 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 {}
     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)
    7620
    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 423d333..be374e1 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://') or url.startswith('https://')):
     13        # 'url' must already be ASCII and URL-quoted, so no need for encoding
     14        # conversions here.
     15        url = iri_to_uri(u'http://%s%s' % (domain, url))
     16    return url
     17
     18class FeedDoesNotExist(ObjectDoesNotExist):
     19    pass
     20
     21
     22class Feed(object):
     23    feed_type = feedgenerator.DefaultFeed
     24    title_template = None
     25    description_template = None
     26       
     27    def __call__(self, request, *args, **kwargs):
     28        try:
     29            obj = self.get_object(request, *args, **kwargs)
     30        except ObjectDoesNotExist:
     31            raise Http404
     32        feedgen = self.get_feed(obj, request)
     33        response = HttpResponse(mimetype=feedgen.mime_type)
     34        feedgen.write(response, 'utf-8')
     35        return response
     36   
     37    def item_title(self, item):
     38        # Titles should be double escaped by default (see #6533)
     39        return escape(force_unicode(item))
     40   
     41    def item_description(self, item):
     42        return force_unicode(item)
     43   
     44    def item_link(self, item):
     45        try:
     46            return item.get_absolute_url()
     47        except AttributeError:
     48            raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__
     49 
     50    def __get_dynamic_attr(self, attname, obj, default=None):
     51        try:
     52            attr = getattr(self, attname)
     53        except AttributeError:
     54            return default
     55        if callable(attr):
     56            # Check func_code.co_argcount rather than try/excepting the
     57            # function and catching the TypeError, because something inside
     58            # the function may raise the TypeError. This technique is more
     59            # accurate.
     60            if hasattr(attr, 'func_code'):
     61                argcount = attr.func_code.co_argcount
     62            else:
     63                argcount = attr.__call__.func_code.co_argcount
     64            if argcount == 2: # one argument is 'self'
     65                return attr(obj)
     66            else:
     67                return attr()
     68        return attr
     69       
     70       
     71    def feed_extra_kwargs(self, obj):
     72        """
     73        Returns an extra keyword arguments dictionary that is used when
     74        initializing the feed generator.
     75        """
     76        return {}
     77 
     78    def item_extra_kwargs(self, item):
     79        """
     80        Returns an extra keyword arguments dictionary that is used with
     81        the `add_item` call of the feed generator.
     82        """
     83        return {}
     84 
     85    def get_object(self, request, *args, **kwargs):
     86        return None
     87 
     88    def get_feed(self, obj, request):
     89        """
     90        Returns a feedgenerator.DefaultFeed object, fully populated, for
     91        this feed. Raises FeedDoesNotExist for invalid parameters.
     92        """
     93        if Site._meta.installed:
     94            current_site = Site.objects.get_current()
     95        else:
     96            current_site = RequestSite(request)
     97       
     98        link = self.__get_dynamic_attr('link', obj)
     99        link = add_domain(current_site.domain, link)
     100 
     101        feed = self.feed_type(
     102            title = self.__get_dynamic_attr('title', obj),
     103            subtitle = self.__get_dynamic_attr('subtitle', obj),
     104            link = link,
     105            description = self.__get_dynamic_attr('description', obj),
     106            language = settings.LANGUAGE_CODE.decode(),
     107            feed_url = add_domain(current_site.domain,
     108                    self.__get_dynamic_attr('feed_url', obj) or request.path),
     109            author_name = self.__get_dynamic_attr('author_name', obj),
     110            author_link = self.__get_dynamic_attr('author_link', obj),
     111            author_email = self.__get_dynamic_attr('author_email', obj),
     112            categories = self.__get_dynamic_attr('categories', obj),
     113            feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
     114            feed_guid = self.__get_dynamic_attr('feed_guid', obj),
     115            ttl = self.__get_dynamic_attr('ttl', obj),
     116            **self.feed_extra_kwargs(obj)
     117        )
     118       
     119        title_tmp = None
     120        if self.title_template is not None:
     121            try:
     122                title_tmp = loader.get_template(self.title_template)
     123            except TemplateDoesNotExist:
     124                pass
     125           
     126        description_tmp = None
     127        if self.description_template is not None:
     128            try:
     129                description_tmp = loader.get_template(self.description_template)
     130            except TemplateDoesNotExist:
     131                pass
     132 
     133        for item in self.__get_dynamic_attr('items', obj):
     134            if title_tmp is not None:
     135                title = title_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
     136            else:
     137                title = self.__get_dynamic_attr('item_title', item)
     138            if description_tmp is not None:
     139                description = description_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
     140            else:
     141                description = self.__get_dynamic_attr('item_description', item)
     142            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
     143            enc = None
     144            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
     145            if enc_url:
     146                enc = feedgenerator.Enclosure(
     147                    url = smart_unicode(enc_url),
     148                    length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
     149                    mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
     150                )
     151            author_name = self.__get_dynamic_attr('item_author_name', item)
     152            if author_name is not None:
     153                author_email = self.__get_dynamic_attr('item_author_email', item)
     154                author_link = self.__get_dynamic_attr('item_author_link', item)
     155            else:
     156                author_email = author_link = None
     157 
     158            pubdate = self.__get_dynamic_attr('item_pubdate', item)
     159            if pubdate and not pubdate.tzinfo:
     160                now = datetime.datetime.now()
     161                utcnow = datetime.datetime.utcnow()
     162 
     163                # Must always subtract smaller time from larger time here.
     164                if utcnow > now:
     165                    sign = -1
     166                    tzDifference = (utcnow - now)
     167                else:
     168                    sign = 1
     169                    tzDifference = (now - utcnow)
     170 
     171                # Round the timezone offset to the nearest half hour.
     172                tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
     173                tzOffset = datetime.timedelta(minutes=tzOffsetMinutes)
     174                pubdate = pubdate.replace(tzinfo=tzinfo.FixedOffset(tzOffset))
     175           
     176            feed.add_item(
     177                title = title,
     178                link = link,
     179                description = description,
     180                unique_id = self.__get_dynamic_attr('item_guid', item, link),
     181                enclosure = enc,
     182                pubdate = pubdate,
     183                author_name = author_name,
     184                author_email = author_email,
     185                author_link = author_link,
     186                categories = self.__get_dynamic_attr('item_categories', item),
     187                item_copyright = self.__get_dynamic_attr('item_copyright', item),
     188                **self.item_extra_kwargs(item)
     189            )
     190        return feed
     191
    3192
    4193def feed(request, url, feed_dict=None):
     194    """Provided for backwards compatibility."""
     195    import warnings
     196    warnings.warn('The syndication feed() view is deprecated. Please use the '
     197                  'new class based view API.',
     198                  category=PendingDeprecationWarning)
     199   
    5200    if not feed_dict:
    6201        raise Http404, "No feeds are registered."
    7202
    def feed(request, url, feed_dict=None):  
    17212
    18213    try:
    19214        feedgen = f(slug, request).get_feed(param)
    20     except feeds.FeedDoesNotExist:
     215    except FeedDoesNotExist:
    21216        raise Http404, "Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug
    22217
    23218    response = HttpResponse(mimetype=feedgen.mime_type)
    24219    feedgen.write(response, 'utf-8')
    25220    return response
     221
  • 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 b54ae6e..b9ccc72 100644
    a b their deprecation, as per the :ref:`Django deprecation policy  
    5050          backwards compatibility. These have been deprecated since the 1.2
    5151          release.
    5252
     53        * The ``views.feed()`` view and ``feeds.Feed`` class in
     54          ``django.contrib.syndication`` have been deprecated since the 1.2
     55          release. The class-based view ``views.Feed`` should be used instead.
     56
    5357    * 2.0
    5458        * ``django.views.defaults.shortcut()``. This function has been moved
    5559          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 cb9c22b..79088b0 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/0613/rss/` -- Returns recent crimes for beat 0613.
     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>\d+)/rss/$', BeatFeed()),
    212172
    213 An example makes this clear. Here's the code for these beat-specific feeds::
     173Like a view, the arguments in the URL are passed to the :meth:`get_object()`
     174method along with the request object. Here's the code for these beat-specific
     175feeds::
    214176
    215     from django.contrib.syndication.feeds import FeedDoesNotExist
    216     from django.core.exceptions import ObjectDoesNotExist
     177    from django.contrib.syndication.views import FeedDoesNotExist
    217178
    218179    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])
     180        description_template = 'feeds/beat_description.html'
     181   
     182        def get_object(self, request, beat):
     183            return Beat.objects.get(beat__exact=beat)
    225184
    226185        def title(self, obj):
    227186            return "Chicagocrime.org: Crimes for beat %s" % obj.beat
    228187
    229188        def link(self, obj):
    230             if not obj:
    231                 raise FeedDoesNotExist
    232189            return obj.get_absolute_url()
    233190
    234191        def description(self, obj):
    235192            return "Crimes recently reported in police beat %s" % obj.beat
    236193
    237194        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).
     195            return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30]
     196
     197To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django
     198uses the :meth:`title()`, :meth:`link()` and :meth:`description()` methods. In
     199the previous example, they were simple string class attributes, but this example
     200illustrates that they can be either strings *or* methods. For each of
     201:attr:`title`, :attr:`link` and :attr:`description`, Django follows this
     202algorithm:
     203
     204    * First, it tries to call a method, passing the ``obj`` argument, where
     205      ``obj`` is the object returned by :meth:`get_object()`.
     206
     207    * Failing that, it tries to call a method with no arguments.
     208
     209    * Failing that, it uses the class attribute.
     210
     211Also note that :meth:`items()` also follows the same algorithm -- first, it
     212tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items`
     213class attribute (which should be a list).
     214
     215We are using a template for the item descriptions. It can be very simple:
     216
     217.. code-block:: html+django
     218
     219    {{ obj.description }}
     220
     221However, you are free to add formatting as required.
    298222
    299223The ``ExampleFeed`` class below gives full documentation on methods and
    300 attributes of :class:`~django.contrib.syndication.feeds.Feed` classes.
     224attributes of :class:`~django.contrib.syndication.views.Feed` classes.
    301225
    302226Specifying the type of feed
    303227---------------------------
    Specifying the type of feed  
    305229By default, feeds produced in this framework use RSS 2.0.
    306230
    307231To change that, add a ``feed_type`` attribute to your
    308 :class:`~django.contrib.syndication.feeds.Feed` class, like so::
     232:class:`~django.contrib.syndication.views.Feed` class, like so::
    309233
    310234    from django.utils.feedgenerator import Atom1Feed
    311235
    Publishing Atom and RSS feeds in tandem  
    353277
    354278Some developers like to make available both Atom *and* RSS versions of their
    355279feeds. That's easy to do with Django: Just create a subclass of your
    356 :class:`~django.contrib.syndication.feeds.Feed`
     280:class:`~django.contrib.syndication.views.Feed`
    357281class and set the :attr:`feed_type` to something different. Then update your
    358282URLconf to add the extra versions.
    359283
    360284Here's a full example::
    361285
    362     from django.contrib.syndication.feeds import Feed
     286    from django.contrib.syndication.views import Feed
    363287    from chicagocrime.models import NewsItem
    364288    from django.utils.feedgenerator import Atom1Feed
    365289
    Here's a full example::  
    381305    a feed-level "description," but they *do* provide for a "subtitle."
    382306
    383307    If you provide a :attr:`description` in your
    384     :class:`~django.contrib.syndication.feeds.Feed` class, Django will *not*
     308    :class:`~django.contrib.syndication.views.Feed` class, Django will *not*
    385309    automatically put that into the :attr:`subtitle` element, because a
    386310    subtitle and description are not necessarily the same thing. Instead, you
    387311    should define a :attr:`subtitle` attribute.
    And the accompanying URLconf::  
    394318    from django.conf.urls.defaults import *
    395319    from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
    396320
    397     feeds = {
    398         'rss': RssSiteNewsFeed,
    399         'atom': AtomSiteNewsFeed,
    400     }
    401 
    402321    urlpatterns = patterns('',
    403322        # ...
    404         (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
    405             {'feed_dict': feeds}),
     323        (r'^sitenews/rss/$', RssSiteNewsFeed()),
     324        (r'^sitenews/atom/$', AtomSiteNewsFeed()),
    406325        # ...
    407326    )
    408327
    409328Feed class reference
    410329--------------------
    411330
    412 .. class:: django.contrib.syndication.feeds.Feed
     331.. class:: django.contrib.syndication.views.Feed
    413332
    414333This example illustrates all possible attributes and methods for a
    415 :class:`~django.contrib.syndication.feeds.Feed` class::
     334:class:`~django.contrib.syndication.views.Feed` class::
    416335
    417     from django.contrib.syndication.feeds import Feed
     336    from django.contrib.syndication.views import Feed
    418337    from django.utils import feedgenerator
    419338
    420339    class ExampleFeed(Feed):
    This example illustrates all possible attributes and methods for a  
    430349        # TEMPLATE NAMES -- Optional. These should be strings representing
    431350        # names of Django templates that the system should use in rendering the
    432351        # 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.
     352        # If one is not specified, the item_title() or item_description()
     353        # methods are used instead.
    436354
    437355        title_template = None
    438356        description_template = None
    This example illustrates all possible attributes and methods for a  
    572490        # COPYRIGHT NOTICE -- One of the following three is optional. The
    573491        # framework looks for them in this order.
    574492
    575         def copyright(self, obj):
     493        def feed_copyright(self, obj):
    576494            """
    577495            Takes the object returned by get_object() and returns the feed's
    578496            copyright notice as a normal Python string.
    579497            """
    580498
    581         def copyright(self):
     499        def feed_copyright(self):
    582500            """
    583501            Returns the feed's copyright notice as a normal Python string.
    584502            """
    585503
    586         copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
     504        feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
    587505
    588506        # TTL -- One of the following three is optional. The framework looks
    589507        # for them in this order. Ignored for Atom feeds.
    This example illustrates all possible attributes and methods for a  
    620538        # GET_OBJECT -- This is required for feeds that publish different data
    621539        # for different URL parameters. (See "A complex example" above.)
    622540
    623         def get_object(self, bits):
     541        def get_object(self, request, *args, **kwargs):
    624542            """
    625             Takes a list of strings gleaned from the URL and returns an object
    626             represented by this feed. Raises
     543            Takes the current request and the arguments from the URL, and
     544            returns an object represented by this feed. Raises
    627545            django.core.exceptions.ObjectDoesNotExist on error.
    628546            """
     547       
     548        # ITEM TITLE AND DESCRIPTION -- If title_template or
     549        # description_template are not defined, these are used instead. Both are
     550        # optional, by default they will use the unicode representation of the
     551        # item.
     552       
     553        def item_title(self, item):
     554            """
     555            Takes an item, as returned by items(), and returns the item's
     556            title as a normal Python string.
     557            """
     558
     559        def item_title(self):
     560            """
     561            Returns the title for every item in the feed.
     562            """
     563
     564        item_title = 'Breaking News: Nothing Happening' # Hard-coded title.
     565       
     566        def item_description(self, item):
     567            """
     568            Takes an item, as returned by items(), and returns the item's
     569            description as a normal Python string.
     570            """
     571
     572        def item_description(self):
     573            """
     574            Returns the description for every item in the feed.
     575            """
    629576
     577        item_description = 'A description of the item.' # Hard-coded description.
     578       
    630579        # ITEM LINK -- One of these three is required. The framework looks for
    631580        # them in this order.
    632581
    This example illustrates all possible attributes and methods for a  
    686635
    687636        item_author_email = 'test@example.com' # Hard-coded author e-mail.
    688637
    689         # ITEM AUTHOR LINK --One of the following three is optional. The
     638        # ITEM AUTHOR LINK -- One of the following three is optional. The
    690639        # framework looks for them in this order. In each case, the URL should
    691640        # include the "http://" and domain name.
    692641        #
  • docs/releases/1.2.txt

    diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt
    index 9e8816e..2ff78bd 100644
    a b For more information, see the full  
    176176:ref:`messages documentation <ref-contrib-messages>`. You should begin to
    177177update your code to use the new API immediately.
    178178
     179``django.contrib.syndication.feeds.Feed``
     180-----------------------------------------
     181
     182The syndication ``Feed`` class has been replaced by
     183``django.contrib.syndication.views.Feed``, and will be removed in Django 1.4.
     184
     185The new class has an almost identical API, but allows instances to be used as
     186views. For example, consider the feeds using the old framework in the following
     187:ref:`URLconf <topics-http-urls>`::
     188
     189    from django.conf.urls.defaults import *
     190    from myproject.feeds import LatestEntries, LatestEntriesByCategory
     191
     192    feeds = {
     193        'latest': LatestEntries,
     194        'categories': LatestEntriesByCategory,
     195    }
     196
     197    urlpatterns = patterns('',
     198        # ...
     199        (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
     200            {'feed_dict': feeds}),
     201        # ...
     202    )
     203
     204This should be replaced by::
     205
     206    from django.conf.urls.defaults import *
     207    from myproject.feeds import LatestEntries, LatestEntriesByCategory
     208
     209    urlpatterns = patterns('',
     210        # ...
     211        (r'^feeds/latest/$', LatestEntries()),
     212        (r'^feeds/categories/(?P<category_id>\d+)/$', LatestEntriesByCategory()),
     213        # ...
     214    )
     215
     216The ``LatestEntries`` class would be identical, apart from subclassing from the
     217new ``Feed`` class. However, ``LatestEntriesByCategory`` used the
     218``get_object()`` method with the ``bits`` argument to specify a specific
     219category to show. Much like a view, the ``get_object()`` method now takes a
     220``request`` and arguments from the URL, so it would look like this::
     221
     222    from django.contrib.syndication.views import Feed
     223    from django.shortcuts import get_object_or_404
     224    from myproject.models import Category
     225
     226    class LatestEntriesByCategory(Feed):
     227        def get_object(self, request, category_id):
     228            return get_object_or_404(Category, id=category_id)
     229       
     230        # ...
     231
     232For more information, see the full :ref:`syndication framework documentation
     233<ref-contrib-syndication>`.
     234
    179235What's new in Django 1.2
    180236========================
    181237
    alternative to the normal primary-key based object references in a  
    277333fixture, improving readability, and resolving problems referring to
    278334objects whose primary key value may not be predictable or known.
    279335
     336Syndication feeds as views
     337--------------------------
     338
     339:ref:`Syndication feeds <ref-contrib-syndication>` can now be used directly as
     340views in your :ref:`URLconf <topics-http-urls>`. Previously, all the feeds on a
     341site had to live beneath a single URL, which produced ugly, unnatural URLs.
     342
     343For example, suppose you listed blog posts for the "django" tag under
     344``/blog/django/``. In previous versions of Django, a feed for those posts would
     345have had to be something like ``/feeds/blog-tag/django/``. In Django 1.2, it
     346could simply be ``/blog/django/feed/``.
     347
     348Like any other view, feeds are now passed a ``request`` object, so you can
     349do user based access control amongst other things.
     350
     351
  • 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..fdc373d 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  }
    3442]
     43 No newline at end of file
  • tests/regressiontests/syndication/models.py

    diff --git a/tests/regressiontests/syndication/models.py b/tests/regressiontests/syndication/models.py
    index 99e14ad..b6cf9e5 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   
     10    def __unicode__(self):
     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
    721    def __unicode__(self):
    8         return self.title
    9  No newline at end of file
     22        return self.title
  • 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..1861e88 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            'http://example.commailto:uhoh%40djangoproject.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..0e50118
    - +  
     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        )
Back to Top