Ticket #12403: syndication-views-1.diff

File syndication-views-1.diff, 66.1 KB (added by Ben Firshman, 14 years ago)
  • 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..996f981 100644
    a b  
    1 from datetime import datetime, timedelta
     1from django.contrib.syndication import views
     2from django.core.exceptions import ObjectDoesNotExist
    23
    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
     4# This is part of the depreciated API
     5from django.contrib.syndication.views import FeedDoesNotExist
    296
     7class Feed(views.Feed):
     8    """Provided for backwards compatibility."""
    309    def __init__(self, slug, request):
    3110        self.slug = slug
    3211        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 {}
     12        self.feed_url = getattr(self, 'feed_url', None) or request.path
     13        self.title_template = self.title_template or ('feeds/%s_title.html' % slug)
     14        self.description_template = self.description_template or ('feeds/%s_description.html' % slug)
    7615
    7716    def get_object(self, bits):
    7817        return None
    79 
     18       
    8019    def get_feed(self, url=None):
    8120        """
    8221        Returns a feedgenerator.DefaultFeed object, fully populated, for
    class Feed(object):  
    8625            bits = url.split('/')
    8726        else:
    8827            bits = []
    89 
    9028        try:
    9129            obj = self.get_object(bits)
    9230        except ObjectDoesNotExist:
    9331            raise FeedDoesNotExist
     32        return super(Feed, self).get_feed(obj, self.request)
    9433
    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..7231557 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."""
    5195    if not feed_dict:
    6196        raise Http404, "No feeds are registered."
    7197
    def feed(request, url, feed_dict=None):  
    17207
    18208    try:
    19209        feedgen = f(slug, request).get_feed(param)
    20     except feeds.FeedDoesNotExist:
     210    except FeedDoesNotExist:
    21211        raise Http404, "Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug
    22212
    23213    response = HttpResponse(mimetype=feedgen.mime_type)
    24214    feedgen.write(response, 'utf-8')
    25215    return response
     216
  • 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/ref/contrib/syndication.txt

    diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt
    index cb9c22b..149f2ab 100644
    a b The high-level framework  
    2626Overview
    2727--------
    2828
    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.
     29The high-level feed-generating framework is supplied by the
     30:class:`~django.contrib.syndication.views.Feed` class. To create a feed, just write a :class:`~django.contrib.syndication.views.Feed`
     31class and point to an instance of it in your :ref:`URLconf <topics-http-urls>`.
    8132
    8233Feed classes
    8334------------
    8435
    85 A :class:`~django.contrib.syndication.feeds.Feed` class is a simple Python class
     36A :class:`~django.contrib.syndication.views.Feed` class is a Python class
    8637that represents a syndication feed. A feed can be simple (e.g., a "site news"
    8738feed, or a basic feed displaying the latest entries of a blog) or more complex
    8839(e.g., a feed displaying all the blog entries in a particular category, where
    8940the category is variable).
    9041
    91 :class:`~django.contrib.syndication.feeds.Feed` classes must subclass
    92 ``django.contrib.syndication.feeds.Feed``. They can live anywhere in your
     42:class:`~django.contrib.syndication.views.Feed` classes subclass
     43``django.contrib.syndication.views.Feed``. They can live anywhere in your
    9344codebase.
    9445
     46Instances of :class:`~django.contrib.syndication.views.Feed` classes are views
     47which can be used in your :ref:`URLconf <topics-http-urls>`.
     48
    9549A simple example
    9650----------------
    9751
    9852This simple example, taken from `chicagocrime.org`_, describes a feed of the
    9953latest five news items::
    10054
    101     from django.contrib.syndication.feeds import Feed
     55    from django.contrib.syndication.views import Feed
    10256    from chicagocrime.models import NewsItem
    10357
    104     class LatestEntries(Feed):
     58    class LatestEntriesFeed(Feed):
    10559        title = "Chicagocrime.org site news"
    10660        link = "/sitenews/"
    10761        description = "Updates on changes and additions to chicagocrime.org."
    108 
     62       
    10963        def items(self):
    11064            return NewsItem.objects.order_by('-pub_date')[:5]
     65       
     66        def item_title(self, item):
     67            return item.title
     68       
     69        def item_description(self, item):
     70            return item.description
     71
     72To connect a URL to this feed, it needs to be put in your :ref:`URLconf <topics-http-urls>`. Here is a full example::
     73
     74    from django.conf.urls.defaults import *
     75    from myproject.feeds import LatestEntriesFeed
     76
     77    urlpatterns = patterns('',
     78        # ...
     79        (r'^latest/feed/$', LatestEntriesFeed()),
     80        # ...
     81    )
    11182
    11283Note:
    11384
    114 * The class subclasses ``django.contrib.syndication.feeds.Feed``.
     85* The class subclasses ``django.contrib.syndication.views.Feed``.
    11586
    11687* :attr:`title`, :attr:`link` and :attr:`description` correspond to the
    11788  standard RSS ``<title>``, ``<link>`` and ``<description>`` elements,
    One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``,  
    133104``<link>`` and ``<description>``. We need to tell the framework what data to put
    134105into those elements.
    135106
    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:
     107    * For the contents of ``<title>`` and ``<description>``, Django tries
     108      calling the methods :meth:`item_title()` and :meth:`item_description()` on
     109      the :class:`~django.contrib.syndication.views.Feed` class. They are passed
     110      a single parameter, :attr:`item`, which is the object itself. These are
     111      optional; by default, the unicode representation of the object is used for
     112      both.
     113     
     114      If you want to do any special formatting for either the title or
     115      description, :ref:`Django templates <topics-templates>` can be used
     116      instead. Their paths can be specified with the ``title_template`` and
     117      ``description_template`` attributes on the
     118      :class:`~django.contrib.syndication.views.Feed` class. The templates are
     119      rendered for each item and are passed two template context variables:
    143120
    144121         * ``{{ obj }}`` -- The current object (one of whichever objects you
    145122           returned in :meth:`items()`).
    into those elements.  
    152129           :ref:`RequestSite section of the sites framework documentation
    153130           <requestsite-objects>` for more.
    154131
    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.
     132      See `a complex example`_ below that uses a description template.
    161133
    162134    * 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 }}
     135      in :meth:`items()`, Django first tries calling the
     136      :meth:`item_link()` method on the
     137      :class:`~django.contrib.syndication.views.Feed` class. In a similar way to
     138      the title and description, it is passed it a single parameter,
     139      :attr:`item`. If that method doesn't exist, Django tries executing a
     140      ``get_absolute_url()`` method on that object. Both
     141      :meth:`get_absolute_url()` and :meth:`item_link()` should return the
     142      item's URL as a normal Python string. As with ``get_absolute_url()``, the
     143      result of :meth:`item_link()` will be included directly in the URL, so you
     144      are responsible for doing all necessary URL quoting and conversion to
     145      ASCII inside the method itself.
    188146
    189147.. _chicagocrime.org: http://www.chicagocrime.org/
    190148
    191149A complex example
    192150-----------------
    193151
    194 The framework also supports more complex feeds, via parameters.
     152The framework also supports more complex feeds, via arguments.
    195153
    196154For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every
    197155police beat in Chicago. It'd be silly to create a separate
    198 :class:`~django.contrib.syndication.feeds.Feed` class for each police beat; that
     156:class:`~django.contrib.syndication.views.Feed` class for each police beat; that
    199157would 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.
     158programming logic. Instead, the syndication framework lets you access the
     159arguments passed from your :ref:`URLconf <topics-http-urls>` so feeds can output
     160items based on information in the feed's URL.
    202161
    203162On chicagocrime.org, the police-beat feeds are accessible via URLs like this:
    204163
    205     * :file:`/rss/beats/0613/` -- Returns recent crimes for beat 0613.
    206     * :file:`/rss/beats/1424/` -- Returns recent crimes for beat 1424.
     164    * :file:`/beats/0613/rss/` -- Returns recent crimes for beat 0613.
     165    * :file:`/beats/1424/rss/` -- Returns recent crimes for beat 1424.
     166
     167These can be matched with a :ref:`URLconf <topics-http-urls>` line such as::
    207168
    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.
     169    (r'^beats/(?P<beat>\d+)/rss/$', BeatFeed()),
    212170
    213 An example makes this clear. Here's the code for these beat-specific feeds::
     171Like a view, the arguments in the URL are passed to the :meth:`get_object()`
     172method along with the request object. Here's the code for these beat-specific
     173feeds::
    214174
    215     from django.contrib.syndication.feeds import FeedDoesNotExist
    216     from django.core.exceptions import ObjectDoesNotExist
     175    from django.contrib.syndication.views import FeedDoesNotExist
    217176
    218177    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])
     178        description_template = 'feeds/beat_description.html'
     179   
     180        def get_object(self, request, beat):
     181            return Beat.objects.get(beat__exact=beat)
    225182
    226183        def title(self, obj):
    227184            return "Chicagocrime.org: Crimes for beat %s" % obj.beat
    228185
    229186        def link(self, obj):
    230             if not obj:
    231                 raise FeedDoesNotExist
    232187            return obj.get_absolute_url()
    233188
    234189        def description(self, obj):
    235190            return "Crimes recently reported in police beat %s" % obj.beat
    236191
    237192        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).
     193            return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30]
     194
     195To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django
     196uses the :meth:`title()`, :meth:`link()` and :meth:`description()` methods. In
     197the previous example, they were simple string class attributes, but this example
     198illustrates that they can be either strings *or* methods. For each of
     199:attr:`title`, :attr:`link` and :attr:`description`, Django follows this
     200algorithm:
     201
     202    * First, it tries to call a method, passing the ``obj`` argument, where
     203      ``obj`` is the object returned by :meth:`get_object()`.
     204
     205    * Failing that, it tries to call a method with no arguments.
     206
     207    * Failing that, it uses the class attribute.
     208
     209Also note that :meth:`items()` also follows the same algorithm -- first, it
     210tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items`
     211class attribute (which should be a list).
     212
     213We are using a template for the item descriptions. It can be very simple:
     214
     215.. code-block:: html+django
     216
     217    {{ obj.description }}
     218
     219However, you are free to add formatting as required.
    298220
    299221The ``ExampleFeed`` class below gives full documentation on methods and
    300 attributes of :class:`~django.contrib.syndication.feeds.Feed` classes.
     222attributes of :class:`~django.contrib.syndication.views.Feed` classes.
    301223
    302224Specifying the type of feed
    303225---------------------------
    Specifying the type of feed  
    305227By default, feeds produced in this framework use RSS 2.0.
    306228
    307229To change that, add a ``feed_type`` attribute to your
    308 :class:`~django.contrib.syndication.feeds.Feed` class, like so::
     230:class:`~django.contrib.syndication.views.Feed` class, like so::
    309231
    310232    from django.utils.feedgenerator import Atom1Feed
    311233
    Publishing Atom and RSS feeds in tandem  
    353275
    354276Some developers like to make available both Atom *and* RSS versions of their
    355277feeds. That's easy to do with Django: Just create a subclass of your
    356 :class:`~django.contrib.syndication.feeds.Feed`
     278:class:`~django.contrib.syndication.views.Feed`
    357279class and set the :attr:`feed_type` to something different. Then update your
    358280URLconf to add the extra versions.
    359281
    360282Here's a full example::
    361283
    362     from django.contrib.syndication.feeds import Feed
     284    from django.contrib.syndication.views import Feed
    363285    from chicagocrime.models import NewsItem
    364286    from django.utils.feedgenerator import Atom1Feed
    365287
    Here's a full example::  
    381303    a feed-level "description," but they *do* provide for a "subtitle."
    382304
    383305    If you provide a :attr:`description` in your
    384     :class:`~django.contrib.syndication.feeds.Feed` class, Django will *not*
     306    :class:`~django.contrib.syndication.views.Feed` class, Django will *not*
    385307    automatically put that into the :attr:`subtitle` element, because a
    386308    subtitle and description are not necessarily the same thing. Instead, you
    387309    should define a :attr:`subtitle` attribute.
    And the accompanying URLconf::  
    394316    from django.conf.urls.defaults import *
    395317    from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
    396318
    397     feeds = {
    398         'rss': RssSiteNewsFeed,
    399         'atom': AtomSiteNewsFeed,
    400     }
    401 
    402319    urlpatterns = patterns('',
    403320        # ...
    404         (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
    405             {'feed_dict': feeds}),
     321        (r'^sitenews/rss/$', RssSiteNewsFeed()),
     322        (r'^sitenews/atom/$', AtomSiteNewsFeed()),
    406323        # ...
    407324    )
    408325
    409326Feed class reference
    410327--------------------
    411328
    412 .. class:: django.contrib.syndication.feeds.Feed
     329.. class:: django.contrib.syndication.views.Feed
    413330
    414331This example illustrates all possible attributes and methods for a
    415 :class:`~django.contrib.syndication.feeds.Feed` class::
     332:class:`~django.contrib.syndication.views.Feed` class::
    416333
    417     from django.contrib.syndication.feeds import Feed
     334    from django.contrib.syndication.views import Feed
    418335    from django.utils import feedgenerator
    419336
    420337    class ExampleFeed(Feed):
    This example illustrates all possible attributes and methods for a  
    430347        # TEMPLATE NAMES -- Optional. These should be strings representing
    431348        # names of Django templates that the system should use in rendering the
    432349        # 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.
     350        # If one is not specified, the item_title() or item_description()
     351        # methods are used instead.
    436352
    437353        title_template = None
    438354        description_template = None
    This example illustrates all possible attributes and methods for a  
    572488        # COPYRIGHT NOTICE -- One of the following three is optional. The
    573489        # framework looks for them in this order.
    574490
    575         def copyright(self, obj):
     491        def feed_copyright(self, obj):
    576492            """
    577493            Takes the object returned by get_object() and returns the feed's
    578494            copyright notice as a normal Python string.
    579495            """
    580496
    581         def copyright(self):
     497        def feed_copyright(self):
    582498            """
    583499            Returns the feed's copyright notice as a normal Python string.
    584500            """
    585501
    586         copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
     502        feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
    587503
    588504        # TTL -- One of the following three is optional. The framework looks
    589505        # for them in this order. Ignored for Atom feeds.
    This example illustrates all possible attributes and methods for a  
    620536        # GET_OBJECT -- This is required for feeds that publish different data
    621537        # for different URL parameters. (See "A complex example" above.)
    622538
    623         def get_object(self, bits):
     539        def get_object(self, request, *args, **kwargs):
    624540            """
    625             Takes a list of strings gleaned from the URL and returns an object
    626             represented by this feed. Raises
     541            Takes the current request and the arguments from the URL, and
     542            returns an object represented by this feed. Raises
    627543            django.core.exceptions.ObjectDoesNotExist on error.
    628544            """
     545       
     546        # ITEM TITLE AND DESCRIPTION -- If title_template or
     547        # description_template are not defined, these are used instead. Both are
     548        # optional, by default they will use the unicode representation of the
     549        # item.
     550       
     551        def item_title(self, item):
     552            """
     553            Takes an item, as returned by items(), and returns the item's
     554            title as a normal Python string.
     555            """
     556
     557        def item_title(self):
     558            """
     559            Returns the title for every item in the feed.
     560            """
     561
     562        item_title = 'Breaking News: Nothing Happening' # Hard-coded title.
     563       
     564        def item_description(self, item):
     565            """
     566            Takes an item, as returned by items(), and returns the item's
     567            description as a normal Python string.
     568            """
     569
     570        def item_description(self):
     571            """
     572            Returns the description for every item in the feed.
     573            """
    629574
     575        item_description = 'A description of the item.' # Hard-coded description.
     576       
    630577        # ITEM LINK -- One of these three is required. The framework looks for
    631578        # them in this order.
    632579
    This example illustrates all possible attributes and methods for a  
    686633
    687634        item_author_email = 'test@example.com' # Hard-coded author e-mail.
    688635
    689         # ITEM AUTHOR LINK --One of the following three is optional. The
     636        # ITEM AUTHOR LINK -- One of the following three is optional. The
    690637        # framework looks for them in this order. In each case, the URL should
    691638        # include the "http://" and domain name.
    692639        #
  • tests/regressiontests/syndication/feeds.py

    diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py
    index 79837f9..95924cd 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 DepreciatedComplexFeed(feeds.Feed):
     127    def get_object(self, bits):
     128        if len(bits) != 1:
     129            raise ObjectDoesNotExist
     130        return None
     131
     132
     133class DepreciatedRssFeed(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
  • tests/regressiontests/syndication/tests.py

    diff --git a/tests/regressiontests/syndication/tests.py b/tests/regressiontests/syndication/tests.py
    index 816cb44..3d641fd 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# Depreciated feeds
     274######################################
     275
     276class DepreciatedSyndicationFeedTest(FeedTestCase):
     277    """
     278    Tests for the depreciated 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 depreciated
     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..d1017fc 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.DepreciatedComplexFeed,
     7    'rss': feeds.DepreciatedRssFeed,
    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)
Back to Top