Commit c4c27d8a authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #6188, #6304, #6618, #6969, #8758, #8989, #10334, #11069, #11973 and...

Fixed #6188, #6304, #6618, #6969, #8758, #8989, #10334, #11069, #11973 and #12403 -- Modified the syndication framework to use class-based views. Thanks to Ben Firshman for his work on this patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12338 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent 3f68d255
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -166,6 +166,7 @@ answer newbie questions, and generally made Django that much better:
    Afonso Fernández Nogueira <fonzzo.django@gmail.com>
    J. Pablo Fernandez <pupeno@pupeno.com>
    Maciej Fijalkowski
    Ben Firshman <ben@firshman.co.uk>
    Matthew Flanagan <http://wadofstuff.blogspot.com>
    Eric Floehr <eric@intellovations.com>
    Eric Florenzano <floguy@gmail.com>
+2 −2
Original line number Diff line number Diff line
from django.conf import settings
from django.contrib.syndication.feeds import Feed
from django.contrib.syndication.views import Feed
from django.contrib.sites.models import Site
from django.contrib import comments
from django.utils.translation import ugettext as _
+15 −156
Original line number Diff line number Diff line
from datetime import datetime, timedelta
from django.contrib.syndication import views
from django.core.exceptions import ObjectDoesNotExist
import warnings

from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.template import loader, Template, TemplateDoesNotExist
from django.contrib.sites.models import Site, RequestSite
from django.utils import feedgenerator
from django.utils.tzinfo import FixedOffset
from django.utils.encoding import smart_unicode, iri_to_uri
from django.conf import settings         
from django.template import RequestContext

def add_domain(domain, url):
    if not (url.startswith('http://') or url.startswith('https://')):
        # 'url' must already be ASCII and URL-quoted, so no need for encoding
        # conversions here.
        url = iri_to_uri(u'http://%s%s' % (domain, url))
    return url

class FeedDoesNotExist(ObjectDoesNotExist):
    pass

class Feed(object):
    item_pubdate = None
    item_enclosure_url = None
    feed_type = feedgenerator.DefaultFeed
    feed_url = None
    title_template = None
    description_template = None
# This is part of the deprecated API
from django.contrib.syndication.views import FeedDoesNotExist, add_domain

class Feed(views.Feed):
    """Provided for backwards compatibility."""
    def __init__(self, slug, request):
        warnings.warn('The syndication feeds.Feed class is deprecated. Please '
                      'use the new class based view API.',
                      category=PendingDeprecationWarning)

        self.slug = slug
        self.request = request
        self.feed_url = self.feed_url or request.path
        self.title_template_name = self.title_template or ('feeds/%s_title.html' % slug)
        self.description_template_name = self.description_template or ('feeds/%s_description.html' % slug)

    def item_link(self, item):
        try:
            return item.get_absolute_url()
        except AttributeError:
            raise ImproperlyConfigured("Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__)

    def __get_dynamic_attr(self, attname, obj, default=None):
        try:
            attr = getattr(self, attname)
        except AttributeError:
            return default
        if callable(attr):
            # Check func_code.co_argcount rather than try/excepting the
            # function and catching the TypeError, because something inside
            # the function may raise the TypeError. This technique is more
            # accurate.
            if hasattr(attr, 'func_code'):
                argcount = attr.func_code.co_argcount
            else:
                argcount = attr.__call__.func_code.co_argcount
            if argcount == 2: # one argument is 'self'
                return attr(obj)
            else:
                return attr()
        return attr

    def feed_extra_kwargs(self, obj):
        """
        Returns an extra keyword arguments dictionary that is used when
        initializing the feed generator.
        """
        return {}

    def item_extra_kwargs(self, item):
        """
        Returns an extra keyword arguments dictionary that is used with
        the `add_item` call of the feed generator.
        """
        return {}
        self.feed_url = getattr(self, 'feed_url', None) or request.path
        self.title_template = self.title_template or ('feeds/%s_title.html' % slug)
        self.description_template = self.description_template or ('feeds/%s_description.html' % slug)

    def get_object(self, bits):
        return None
@@ -86,94 +30,9 @@ class Feed(object):
            bits = url.split('/')
        else:
            bits = []

        try:
            obj = self.get_object(bits)
        except ObjectDoesNotExist:
            raise FeedDoesNotExist
        return super(Feed, self).get_feed(obj, self.request)
        if Site._meta.installed:
            current_site = Site.objects.get_current()
        else:
            current_site = RequestSite(self.request)
        
        link = self.__get_dynamic_attr('link', obj)
        link = add_domain(current_site.domain, link)

        feed = self.feed_type(
            title = self.__get_dynamic_attr('title', obj),
            subtitle = self.__get_dynamic_attr('subtitle', obj),
            link = link,
            description = self.__get_dynamic_attr('description', obj),
            language = settings.LANGUAGE_CODE.decode(),
            feed_url = add_domain(current_site.domain,
                                  self.__get_dynamic_attr('feed_url', obj)),
            author_name = self.__get_dynamic_attr('author_name', obj),
            author_link = self.__get_dynamic_attr('author_link', obj),
            author_email = self.__get_dynamic_attr('author_email', obj),
            categories = self.__get_dynamic_attr('categories', obj),
            feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
            feed_guid = self.__get_dynamic_attr('feed_guid', obj),
            ttl = self.__get_dynamic_attr('ttl', obj),
            **self.feed_extra_kwargs(obj)
        )

        try:
            title_tmp = loader.get_template(self.title_template_name)
        except TemplateDoesNotExist:
            title_tmp = Template('{{ obj }}')
        try:
            description_tmp = loader.get_template(self.description_template_name)
        except TemplateDoesNotExist:
            description_tmp = Template('{{ obj }}')

        for item in self.__get_dynamic_attr('items', obj):
            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
            enc = None
            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
            if enc_url:
                enc = feedgenerator.Enclosure(
                    url = smart_unicode(enc_url),
                    length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
                    mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
                )
            author_name = self.__get_dynamic_attr('item_author_name', item)
            if author_name is not None:
                author_email = self.__get_dynamic_attr('item_author_email', item)
                author_link = self.__get_dynamic_attr('item_author_link', item)
            else:
                author_email = author_link = None

            pubdate = self.__get_dynamic_attr('item_pubdate', item)
            if pubdate and not pubdate.tzinfo:
                now = datetime.now()
                utcnow = datetime.utcnow()

                # Must always subtract smaller time from larger time here.
                if utcnow > now:
                    sign = -1
                    tzDifference = (utcnow - now)
                else:
                    sign = 1
                    tzDifference = (now - utcnow)

                # Round the timezone offset to the nearest half hour.
                tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
                tzOffset = timedelta(minutes=tzOffsetMinutes)
                pubdate = pubdate.replace(tzinfo=FixedOffset(tzOffset))

            feed.add_item(
                title = title_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
                link = link,
                description = description_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),
                unique_id = self.__get_dynamic_attr('item_guid', item, link),
                enclosure = enc,
                pubdate = pubdate,
                author_name = author_name,
                author_email = author_email,
                author_link = author_link,
                categories = self.__get_dynamic_attr('item_categories', item),
                item_copyright = self.__get_dynamic_attr('item_copyright', item),
                **self.item_extra_kwargs(item)
            )
        return feed
+199 −2
Original line number Diff line number Diff line
from django.contrib.syndication import feeds
import datetime
from django.conf import settings
from django.contrib.sites.models import Site, RequestSite
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.http import HttpResponse, Http404
from django.template import loader, Template, TemplateDoesNotExist, RequestContext
from django.utils import feedgenerator, tzinfo
from django.utils.encoding import force_unicode, iri_to_uri, smart_unicode
from django.utils.html import escape

def add_domain(domain, url):
    if not (url.startswith('http://')
            or url.startswith('https://')
            or url.startswith('mailto:')):
        # 'url' must already be ASCII and URL-quoted, so no need for encoding
        # conversions here.
        url = iri_to_uri(u'http://%s%s' % (domain, url))
    return url

class FeedDoesNotExist(ObjectDoesNotExist):
    pass


class Feed(object):
    feed_type = feedgenerator.DefaultFeed
    title_template = None
    description_template = None

    def __call__(self, request, *args, **kwargs):
        try:
            obj = self.get_object(request, *args, **kwargs)
        except ObjectDoesNotExist:
            raise Http404('Feed object does not exist.')
        feedgen = self.get_feed(obj, request)
        response = HttpResponse(mimetype=feedgen.mime_type)
        feedgen.write(response, 'utf-8')
        return response

    def item_title(self, item):
        # Titles should be double escaped by default (see #6533)
        return escape(force_unicode(item))

    def item_description(self, item):
        return force_unicode(item)

    def item_link(self, item):
        try:
            return item.get_absolute_url()
        except AttributeError:
            raise ImproperlyConfigured('Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class.' % item.__class__.__name__)

    def __get_dynamic_attr(self, attname, obj, default=None):
        try:
            attr = getattr(self, attname)
        except AttributeError:
            return default
        if callable(attr):
            # Check func_code.co_argcount rather than try/excepting the
            # function and catching the TypeError, because something inside
            # the function may raise the TypeError. This technique is more
            # accurate.
            if hasattr(attr, 'func_code'):
                argcount = attr.func_code.co_argcount
            else:
                argcount = attr.__call__.func_code.co_argcount
            if argcount == 2: # one argument is 'self'
                return attr(obj)
            else:
                return attr()
        return attr

    def feed_extra_kwargs(self, obj):
        """
        Returns an extra keyword arguments dictionary that is used when
        initializing the feed generator.
        """
        return {}

    def item_extra_kwargs(self, item):
        """
        Returns an extra keyword arguments dictionary that is used with
        the `add_item` call of the feed generator.
        """
        return {}

    def get_object(self, request, *args, **kwargs):
        return None

    def get_feed(self, obj, request):
        """
        Returns a feedgenerator.DefaultFeed object, fully populated, for
        this feed. Raises FeedDoesNotExist for invalid parameters.
        """
        if Site._meta.installed:
            current_site = Site.objects.get_current()
        else:
            current_site = RequestSite(request)

        link = self.__get_dynamic_attr('link', obj)
        link = add_domain(current_site.domain, link)

        feed = self.feed_type(
            title = self.__get_dynamic_attr('title', obj),
            subtitle = self.__get_dynamic_attr('subtitle', obj),
            link = link,
            description = self.__get_dynamic_attr('description', obj),
            language = settings.LANGUAGE_CODE.decode(),
            feed_url = add_domain(current_site.domain,
                    self.__get_dynamic_attr('feed_url', obj) or request.path),
            author_name = self.__get_dynamic_attr('author_name', obj),
            author_link = self.__get_dynamic_attr('author_link', obj),
            author_email = self.__get_dynamic_attr('author_email', obj),
            categories = self.__get_dynamic_attr('categories', obj),
            feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
            feed_guid = self.__get_dynamic_attr('feed_guid', obj),
            ttl = self.__get_dynamic_attr('ttl', obj),
            **self.feed_extra_kwargs(obj)
        )

        title_tmp = None
        if self.title_template is not None:
            try:
                title_tmp = loader.get_template(self.title_template)
            except TemplateDoesNotExist:
                pass

        description_tmp = None
        if self.description_template is not None:
            try:
                description_tmp = loader.get_template(self.description_template)
            except TemplateDoesNotExist:
                pass

        for item in self.__get_dynamic_attr('items', obj):
            if title_tmp is not None:
                title = title_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
            else:
                title = self.__get_dynamic_attr('item_title', item)
            if description_tmp is not None:
                description = description_tmp.render(RequestContext(request, {'obj': item, 'site': current_site}))
            else:
                description = self.__get_dynamic_attr('item_description', item)
            link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))
            enc = None
            enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
            if enc_url:
                enc = feedgenerator.Enclosure(
                    url = smart_unicode(enc_url),
                    length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
                    mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
                )
            author_name = self.__get_dynamic_attr('item_author_name', item)
            if author_name is not None:
                author_email = self.__get_dynamic_attr('item_author_email', item)
                author_link = self.__get_dynamic_attr('item_author_link', item)
            else:
                author_email = author_link = None

            pubdate = self.__get_dynamic_attr('item_pubdate', item)
            if pubdate and not pubdate.tzinfo:
                now = datetime.datetime.now()
                utcnow = datetime.datetime.utcnow()

                # Must always subtract smaller time from larger time here.
                if utcnow > now:
                    sign = -1
                    tzDifference = (utcnow - now)
                else:
                    sign = 1
                    tzDifference = (now - utcnow)

                # Round the timezone offset to the nearest half hour.
                tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30
                tzOffset = datetime.timedelta(minutes=tzOffsetMinutes)
                pubdate = pubdate.replace(tzinfo=tzinfo.FixedOffset(tzOffset))

            feed.add_item(
                title = title,
                link = link,
                description = description,
                unique_id = self.__get_dynamic_attr('item_guid', item, link),
                enclosure = enc,
                pubdate = pubdate,
                author_name = author_name,
                author_email = author_email,
                author_link = author_link,
                categories = self.__get_dynamic_attr('item_categories', item),
                item_copyright = self.__get_dynamic_attr('item_copyright', item),
                **self.item_extra_kwargs(item)
            )
        return feed


def feed(request, url, feed_dict=None):
    """Provided for backwards compatibility."""
    import warnings
    warnings.warn('The syndication feed() view is deprecated. Please use the '
                  'new class based view API.',
                  category=PendingDeprecationWarning)

    if not feed_dict:
        raise Http404("No feeds are registered.")

@@ -17,9 +213,10 @@ def feed(request, url, feed_dict=None):

    try:
        feedgen = f(slug, request).get_feed(param)
    except feeds.FeedDoesNotExist:
    except FeedDoesNotExist:
        raise Http404("Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug)

    response = HttpResponse(mimetype=feedgen.mime_type)
    feedgen.write(response, 'utf-8')
    return response
+20 −8
Original line number Diff line number Diff line
@@ -19,8 +19,8 @@ For definitions of the different versions of RSS, see:
http://diveintomark.org/archives/2004/02/04/incompatible-rss
"""

import re
import datetime
import urlparse
from django.utils.xmlutils import SimplerXMLGenerator
from django.utils.encoding import force_unicode, iri_to_uri

@@ -46,12 +46,16 @@ def rfc3339_date(date):
        return date.strftime('%Y-%m-%dT%H:%M:%SZ')

def get_tag_uri(url, date):
    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
    tag = re.sub('^http://', '', url)
    """
    Creates a TagURI.

    See http://diveintomark.org/archives/2004/05/28/howto-atom-id
    """
    url_split = urlparse.urlparse(url)
    d = ''
    if date is not None:
        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
    tag = re.sub('#', '/', tag)
    return u'tag:' + tag
        d = ',%s' % date.strftime('%Y-%m-%d')
    return u'tag:%s%s:%s/%s' % (url_split.hostname, d, url_split.path, url_split.fragment)

class SyndicationFeed(object):
    "Base class for all syndication feeds. Subclasses should provide write()"
@@ -61,6 +65,9 @@ class SyndicationFeed(object):
        to_unicode = lambda s: force_unicode(s, strings_only=True)
        if categories:
            categories = [force_unicode(c) for c in categories]
        if ttl is not None:
            # Force ints to unicode
            ttl = force_unicode(ttl)
        self.feed = {
            'title': to_unicode(title),
            'link': iri_to_uri(link),
@@ -91,6 +98,9 @@ class SyndicationFeed(object):
        to_unicode = lambda s: force_unicode(s, strings_only=True)
        if categories:
            categories = [to_unicode(c) for c in categories]
        if ttl is not None:
            # Force ints to unicode
            ttl = force_unicode(ttl)
        item = {
            'title': to_unicode(title),
            'link': iri_to_uri(link),
@@ -186,7 +196,8 @@ class RssFeed(SyndicationFeed):
        handler.endElement(u"rss")

    def rss_attributes(self):
        return {u"version": self._version}
        return {u"version": self._version,
                u"xmlns:atom": u"http://www.w3.org/2005/Atom"}

    def write_items(self, handler):
        for item in self.items:
@@ -198,6 +209,7 @@ class RssFeed(SyndicationFeed):
        handler.addQuickElement(u"title", self.feed['title'])
        handler.addQuickElement(u"link", self.feed['link'])
        handler.addQuickElement(u"description", self.feed['description'])
        handler.addQuickElement(u"atom:link", None, {u"rel": u"self", u"href": self.feed['feed_url']})
        if self.feed['language'] is not None:
            handler.addQuickElement(u"language", self.feed['language'])
        for cat in self.feed['categories']:
@@ -235,7 +247,7 @@ class Rss201rev2Feed(RssFeed):
        elif item["author_email"]:
            handler.addQuickElement(u"author", item["author_email"])
        elif item["author_name"]:
            handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
            handler.addQuickElement(u"dc:creator", item["author_name"], {u"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})

        if item['pubdate'] is not None:
            handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
Loading