mirror of https://github.com/django/django.git
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
This commit is contained in:
parent
3f68d255e2
commit
c4c27d8a04
1
AUTHORS
1
AUTHORS
|
@ -166,6 +166,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Afonso Fernández Nogueira <fonzzo.django@gmail.com>
|
Afonso Fernández Nogueira <fonzzo.django@gmail.com>
|
||||||
J. Pablo Fernandez <pupeno@pupeno.com>
|
J. Pablo Fernandez <pupeno@pupeno.com>
|
||||||
Maciej Fijalkowski
|
Maciej Fijalkowski
|
||||||
|
Ben Firshman <ben@firshman.co.uk>
|
||||||
Matthew Flanagan <http://wadofstuff.blogspot.com>
|
Matthew Flanagan <http://wadofstuff.blogspot.com>
|
||||||
Eric Floehr <eric@intellovations.com>
|
Eric Floehr <eric@intellovations.com>
|
||||||
Eric Florenzano <floguy@gmail.com>
|
Eric Florenzano <floguy@gmail.com>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.conf import settings
|
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.sites.models import Site
|
||||||
from django.contrib import comments
|
from django.contrib import comments
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
|
@ -1,78 +1,22 @@
|
||||||
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
|
# This is part of the deprecated API
|
||||||
from django.template import loader, Template, TemplateDoesNotExist
|
from django.contrib.syndication.views import FeedDoesNotExist, add_domain
|
||||||
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
|
|
||||||
|
|
||||||
|
class Feed(views.Feed):
|
||||||
|
"""Provided for backwards compatibility."""
|
||||||
def __init__(self, slug, request):
|
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.slug = slug
|
||||||
self.request = request
|
self.request = request
|
||||||
self.feed_url = self.feed_url or request.path
|
self.feed_url = getattr(self, 'feed_url', None) or request.path
|
||||||
self.title_template_name = self.title_template or ('feeds/%s_title.html' % slug)
|
self.title_template = self.title_template or ('feeds/%s_title.html' % slug)
|
||||||
self.description_template_name = self.description_template or ('feeds/%s_description.html' % slug)
|
self.description_template = 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 {}
|
|
||||||
|
|
||||||
def get_object(self, bits):
|
def get_object(self, bits):
|
||||||
return None
|
return None
|
||||||
|
@ -86,94 +30,9 @@ class Feed(object):
|
||||||
bits = url.split('/')
|
bits = url.split('/')
|
||||||
else:
|
else:
|
||||||
bits = []
|
bits = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = self.get_object(bits)
|
obj = self.get_object(bits)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise FeedDoesNotExist
|
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
|
|
||||||
|
|
|
@ -1,7 +1,203 @@
|
||||||
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.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):
|
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:
|
if not feed_dict:
|
||||||
raise Http404("No feeds are registered.")
|
raise Http404("No feeds are registered.")
|
||||||
|
|
||||||
|
@ -17,9 +213,10 @@ def feed(request, url, feed_dict=None):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
feedgen = f(slug, request).get_feed(param)
|
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)
|
raise Http404("Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug)
|
||||||
|
|
||||||
response = HttpResponse(mimetype=feedgen.mime_type)
|
response = HttpResponse(mimetype=feedgen.mime_type)
|
||||||
feedgen.write(response, 'utf-8')
|
feedgen.write(response, 'utf-8')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,8 @@ For definitions of the different versions of RSS, see:
|
||||||
http://diveintomark.org/archives/2004/02/04/incompatible-rss
|
http://diveintomark.org/archives/2004/02/04/incompatible-rss
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import urlparse
|
||||||
from django.utils.xmlutils import SimplerXMLGenerator
|
from django.utils.xmlutils import SimplerXMLGenerator
|
||||||
from django.utils.encoding import force_unicode, iri_to_uri
|
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')
|
return date.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
|
||||||
def get_tag_uri(url, date):
|
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:
|
if date is not None:
|
||||||
tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
|
d = ',%s' % date.strftime('%Y-%m-%d')
|
||||||
tag = re.sub('#', '/', tag)
|
return u'tag:%s%s:%s/%s' % (url_split.hostname, d, url_split.path, url_split.fragment)
|
||||||
return u'tag:' + tag
|
|
||||||
|
|
||||||
class SyndicationFeed(object):
|
class SyndicationFeed(object):
|
||||||
"Base class for all syndication feeds. Subclasses should provide write()"
|
"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)
|
to_unicode = lambda s: force_unicode(s, strings_only=True)
|
||||||
if categories:
|
if categories:
|
||||||
categories = [force_unicode(c) for c in categories]
|
categories = [force_unicode(c) for c in categories]
|
||||||
|
if ttl is not None:
|
||||||
|
# Force ints to unicode
|
||||||
|
ttl = force_unicode(ttl)
|
||||||
self.feed = {
|
self.feed = {
|
||||||
'title': to_unicode(title),
|
'title': to_unicode(title),
|
||||||
'link': iri_to_uri(link),
|
'link': iri_to_uri(link),
|
||||||
|
@ -91,6 +98,9 @@ class SyndicationFeed(object):
|
||||||
to_unicode = lambda s: force_unicode(s, strings_only=True)
|
to_unicode = lambda s: force_unicode(s, strings_only=True)
|
||||||
if categories:
|
if categories:
|
||||||
categories = [to_unicode(c) for c in categories]
|
categories = [to_unicode(c) for c in categories]
|
||||||
|
if ttl is not None:
|
||||||
|
# Force ints to unicode
|
||||||
|
ttl = force_unicode(ttl)
|
||||||
item = {
|
item = {
|
||||||
'title': to_unicode(title),
|
'title': to_unicode(title),
|
||||||
'link': iri_to_uri(link),
|
'link': iri_to_uri(link),
|
||||||
|
@ -186,7 +196,8 @@ class RssFeed(SyndicationFeed):
|
||||||
handler.endElement(u"rss")
|
handler.endElement(u"rss")
|
||||||
|
|
||||||
def rss_attributes(self):
|
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):
|
def write_items(self, handler):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
|
@ -198,6 +209,7 @@ class RssFeed(SyndicationFeed):
|
||||||
handler.addQuickElement(u"title", self.feed['title'])
|
handler.addQuickElement(u"title", self.feed['title'])
|
||||||
handler.addQuickElement(u"link", self.feed['link'])
|
handler.addQuickElement(u"link", self.feed['link'])
|
||||||
handler.addQuickElement(u"description", self.feed['description'])
|
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:
|
if self.feed['language'] is not None:
|
||||||
handler.addQuickElement(u"language", self.feed['language'])
|
handler.addQuickElement(u"language", self.feed['language'])
|
||||||
for cat in self.feed['categories']:
|
for cat in self.feed['categories']:
|
||||||
|
@ -235,7 +247,7 @@ class Rss201rev2Feed(RssFeed):
|
||||||
elif item["author_email"]:
|
elif item["author_email"]:
|
||||||
handler.addQuickElement(u"author", item["author_email"])
|
handler.addQuickElement(u"author", item["author_email"])
|
||||||
elif item["author_name"]:
|
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:
|
if item['pubdate'] is not None:
|
||||||
handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
|
handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
|
||||||
|
|
|
@ -82,6 +82,10 @@ their deprecation, as per the :ref:`Django deprecation policy
|
||||||
* The ability to use a function-based test runners will be removed,
|
* The ability to use a function-based test runners will be removed,
|
||||||
along with the ``django.test.simple.run_tests()`` test runner.
|
along with the ``django.test.simple.run_tests()`` test runner.
|
||||||
|
|
||||||
|
* The ``views.feed()`` view and ``feeds.Feed`` class in
|
||||||
|
``django.contrib.syndication`` have been deprecated since the 1.2
|
||||||
|
release. The class-based view ``views.Feed`` should be used instead.
|
||||||
|
|
||||||
* 2.0
|
* 2.0
|
||||||
* ``django.views.defaults.shortcut()``. This function has been moved
|
* ``django.views.defaults.shortcut()``. This function has been moved
|
||||||
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
|
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
|
||||||
|
|
|
@ -8,14 +8,15 @@ The syndication feed framework
|
||||||
:synopsis: A framework for generating syndication feeds, in RSS and Atom,
|
:synopsis: A framework for generating syndication feeds, in RSS and Atom,
|
||||||
quite easily.
|
quite easily.
|
||||||
|
|
||||||
Django comes with a high-level syndication-feed-generating framework that makes
|
Django comes with a high-level syndication-feed-generating framework
|
||||||
creating RSS_ and Atom_ feeds easy.
|
that makes creating RSS_ and Atom_ feeds easy.
|
||||||
|
|
||||||
To create any syndication feed, all you have to do is write a short Python
|
To create any syndication feed, all you have to do is write a short
|
||||||
class. You can create as many feeds as you want.
|
Python class. You can create as many feeds as you want.
|
||||||
|
|
||||||
Django also comes with a lower-level feed-generating API. Use this if you want
|
Django also comes with a lower-level feed-generating API. Use this if
|
||||||
to generate feeds outside of a Web context, or in some other lower-level way.
|
you want to generate feeds outside of a Web context, or in some other
|
||||||
|
lower-level way.
|
||||||
|
|
||||||
.. _RSS: http://www.whatisrss.com/
|
.. _RSS: http://www.whatisrss.com/
|
||||||
.. _Atom: http://www.atomenabled.org/
|
.. _Atom: http://www.atomenabled.org/
|
||||||
|
@ -23,74 +24,37 @@ to generate feeds outside of a Web context, or in some other lower-level way.
|
||||||
The high-level framework
|
The high-level framework
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
.. versionchanged:: 1.2
|
||||||
|
The high-level feeds framework was refactored in Django 1.2. The
|
||||||
|
pre-1.2 interface still exists, but it has been deprecated, and
|
||||||
|
will be removed in Django 1.4. If you need to maintain an old-style
|
||||||
|
Django feed, please consult the Django 1.1 documentation. For
|
||||||
|
details on updating to use the new high-level feed framework, see
|
||||||
|
the :ref:`Django 1.2 release notes <1.2-updating-feeds>`.
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
--------
|
--------
|
||||||
|
|
||||||
The high-level feed-generating framework is a view that's hooked to ``/feeds/``
|
The high-level feed-generating framework is supplied by the
|
||||||
by default. Django uses the remainder of the URL (everything after ``/feeds/``)
|
:class:`~django.contrib.syndication.views.Feed` class. To create a
|
||||||
to determine which feed to output.
|
feed, write a :class:`~django.contrib.syndication.views.Feed` class
|
||||||
|
and point to an instance of it in your :ref:`URLconf
|
||||||
To create a feed, just write a :class:`~django.contrib.syndication.feeds.Feed`
|
<topics-http-urls>`.
|
||||||
class and point to it in your :ref:`URLconf <topics-http-urls>`.
|
|
||||||
|
|
||||||
Initialization
|
|
||||||
--------------
|
|
||||||
|
|
||||||
To activate syndication feeds on your Django site, add this line to your
|
|
||||||
:ref:`URLconf <topics-http-urls>`::
|
|
||||||
|
|
||||||
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}),
|
|
||||||
|
|
||||||
This tells Django to use the RSS framework to handle all URLs starting with
|
|
||||||
:file:`"feeds/"`. (You can change that :file:`"feeds/"` prefix to fit your own
|
|
||||||
needs.)
|
|
||||||
|
|
||||||
This URLconf line has an extra argument: ``{'feed_dict': feeds}``. Use this
|
|
||||||
extra argument to pass the syndication framework the feeds that should be
|
|
||||||
published under that URL.
|
|
||||||
|
|
||||||
Specifically, :data:`feed_dict` should be a dictionary that maps a feed's slug
|
|
||||||
(short URL label) to its :class:`~django.contrib.syndication.feeds.Feed` class.
|
|
||||||
|
|
||||||
You can define the ``feed_dict`` in the URLconf itself. Here's a full example
|
|
||||||
URLconf::
|
|
||||||
|
|
||||||
from django.conf.urls.defaults import *
|
|
||||||
from myproject.feeds import LatestEntries, LatestEntriesByCategory
|
|
||||||
|
|
||||||
feeds = {
|
|
||||||
'latest': LatestEntries,
|
|
||||||
'categories': LatestEntriesByCategory,
|
|
||||||
}
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
|
||||||
# ...
|
|
||||||
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
|
|
||||||
{'feed_dict': feeds}),
|
|
||||||
# ...
|
|
||||||
)
|
|
||||||
|
|
||||||
The above example registers two feeds:
|
|
||||||
|
|
||||||
* The feed represented by ``LatestEntries`` will live at ``feeds/latest/``.
|
|
||||||
* The feed represented by ``LatestEntriesByCategory`` will live at
|
|
||||||
``feeds/categories/``.
|
|
||||||
|
|
||||||
Once that's set up, you just need to define the
|
|
||||||
:class:`~django.contrib.syndication.feeds.Feed` classes themselves.
|
|
||||||
|
|
||||||
Feed classes
|
Feed classes
|
||||||
------------
|
------------
|
||||||
|
|
||||||
A :class:`~django.contrib.syndication.feeds.Feed` class is a simple Python class
|
A :class:`~django.contrib.syndication.views.Feed` class is a Python
|
||||||
that represents a syndication feed. A feed can be simple (e.g., a "site news"
|
class that represents a syndication feed. A feed can be simple (e.g.,
|
||||||
feed, or a basic feed displaying the latest entries of a blog) or more complex
|
a "site news" feed, or a basic feed displaying the latest entries of a
|
||||||
(e.g., a feed displaying all the blog entries in a particular category, where
|
blog) or more complex (e.g., a feed displaying all the blog entries in
|
||||||
the category is variable).
|
a particular category, where the category is variable).
|
||||||
|
|
||||||
:class:`~django.contrib.syndication.feeds.Feed` classes must subclass
|
Feed classes subclass :class:`django.contrib.syndication.views.Feed`.
|
||||||
``django.contrib.syndication.feeds.Feed``. They can live anywhere in your
|
They can live anywhere in your codebase.
|
||||||
codebase.
|
|
||||||
|
Instances of :class:`~django.contrib.syndication.views.Feed` classes
|
||||||
|
are views which can be used in your :ref:`URLconf <topics-http-urls>`.
|
||||||
|
|
||||||
A simple example
|
A simple example
|
||||||
----------------
|
----------------
|
||||||
|
@ -98,10 +62,10 @@ A simple example
|
||||||
This simple example, taken from `chicagocrime.org`_, describes a feed of the
|
This simple example, taken from `chicagocrime.org`_, describes a feed of the
|
||||||
latest five news items::
|
latest five news items::
|
||||||
|
|
||||||
from django.contrib.syndication.feeds import Feed
|
from django.contrib.syndication.views import Feed
|
||||||
from chicagocrime.models import NewsItem
|
from chicagocrime.models import NewsItem
|
||||||
|
|
||||||
class LatestEntries(Feed):
|
class LatestEntriesFeed(Feed):
|
||||||
title = "Chicagocrime.org site news"
|
title = "Chicagocrime.org site news"
|
||||||
link = "/sitenews/"
|
link = "/sitenews/"
|
||||||
description = "Updates on changes and additions to chicagocrime.org."
|
description = "Updates on changes and additions to chicagocrime.org."
|
||||||
|
@ -109,9 +73,27 @@ latest five news items::
|
||||||
def items(self):
|
def items(self):
|
||||||
return NewsItem.objects.order_by('-pub_date')[:5]
|
return NewsItem.objects.order_by('-pub_date')[:5]
|
||||||
|
|
||||||
|
def item_title(self, item):
|
||||||
|
return item.title
|
||||||
|
|
||||||
|
def item_description(self, item):
|
||||||
|
return item.description
|
||||||
|
|
||||||
|
To connect a URL to this feed, put an instance of the Feed object in
|
||||||
|
your :ref:`URLconf <topics-http-urls>`. For example::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from myproject.feeds import LatestEntriesFeed
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
# ...
|
||||||
|
(r'^latest/feed/$', LatestEntriesFeed()),
|
||||||
|
# ...
|
||||||
|
)
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
|
|
||||||
* The class subclasses ``django.contrib.syndication.feeds.Feed``.
|
* The Feed class subclasses :class:`django.contrib.syndication.views.Feed`.
|
||||||
|
|
||||||
* :attr:`title`, :attr:`link` and :attr:`description` correspond to the
|
* :attr:`title`, :attr:`link` and :attr:`description` correspond to the
|
||||||
standard RSS ``<title>``, ``<link>`` and ``<description>`` elements,
|
standard RSS ``<title>``, ``<link>`` and ``<description>`` elements,
|
||||||
|
@ -129,17 +111,23 @@ Note:
|
||||||
:attr:`subtitle` attribute instead of the :attr:`description` attribute.
|
:attr:`subtitle` attribute instead of the :attr:`description` attribute.
|
||||||
See `Publishing Atom and RSS feeds in tandem`_, later, for an example.
|
See `Publishing Atom and RSS feeds in tandem`_, later, for an example.
|
||||||
|
|
||||||
One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``,
|
One thing is left to do. In an RSS feed, each ``<item>`` has a ``<title>``,
|
||||||
``<link>`` and ``<description>``. We need to tell the framework what data to put
|
``<link>`` and ``<description>``. We need to tell the framework what data to put
|
||||||
into those elements.
|
into those elements.
|
||||||
|
|
||||||
* To specify the contents of ``<title>`` and ``<description>``, create
|
* For the contents of ``<title>`` and ``<description>``, Django tries
|
||||||
:ref:`Django templates <topics-templates>` called
|
calling the methods :meth:`item_title()` and :meth:`item_description()` on
|
||||||
:file:`feeds/latest_title.html` and
|
the :class:`~django.contrib.syndication.views.Feed` class. They are passed
|
||||||
:file:`feeds/latest_description.html`, where :attr:`latest` is the
|
a single parameter, :attr:`item`, which is the object itself. These are
|
||||||
:attr:`slug` specified in the URLconf for the given feed. Note the
|
optional; by default, the unicode representation of the object is used for
|
||||||
``.html`` extension is required. The RSS system renders that template for
|
both.
|
||||||
each item, passing it two template context variables:
|
|
||||||
|
If you want to do any special formatting for either the title or
|
||||||
|
description, :ref:`Django templates <topics-templates>` can be used
|
||||||
|
instead. Their paths can be specified with the ``title_template`` and
|
||||||
|
``description_template`` attributes on the
|
||||||
|
:class:`~django.contrib.syndication.views.Feed` class. The templates are
|
||||||
|
rendered for each item and are passed two template context variables:
|
||||||
|
|
||||||
* ``{{ obj }}`` -- The current object (one of whichever objects you
|
* ``{{ obj }}`` -- The current object (one of whichever objects you
|
||||||
returned in :meth:`items()`).
|
returned in :meth:`items()`).
|
||||||
|
@ -152,130 +140,80 @@ into those elements.
|
||||||
:ref:`RequestSite section of the sites framework documentation
|
:ref:`RequestSite section of the sites framework documentation
|
||||||
<requestsite-objects>` for more.
|
<requestsite-objects>` for more.
|
||||||
|
|
||||||
If you don't create a template for either the title or description, the
|
See `a complex example`_ below that uses a description template.
|
||||||
framework will use the template ``"{{ obj }}"`` by default -- that is, the
|
|
||||||
normal string representation of the object. You can also change the names
|
|
||||||
of these two templates by specifying ``title_template`` and
|
|
||||||
``description_template`` as attributes of your
|
|
||||||
:class:`~django.contrib.syndication.feeds.Feed` class.
|
|
||||||
|
|
||||||
* To specify the contents of ``<link>``, you have two options. For each item
|
* To specify the contents of ``<link>``, you have two options. For each item
|
||||||
in :meth:`items()`, Django first tries calling a method
|
in :meth:`items()`, Django first tries calling the
|
||||||
:meth:`item_link()` in the :class:`~django.contrib.syndication.feeds.Feed`
|
:meth:`item_link()` method on the
|
||||||
class, passing it a single parameter, :attr:`item`, which is the object
|
:class:`~django.contrib.syndication.views.Feed` class. In a similar way to
|
||||||
itself. If that method doesn't exist, Django tries executing a
|
the title and description, it is passed it a single parameter,
|
||||||
``get_absolute_url()`` method on that object. . Both
|
:attr:`item`. If that method doesn't exist, Django tries executing a
|
||||||
``get_absolute_url()`` and :meth:`item_link()` should return the item's
|
``get_absolute_url()`` method on that object. Both
|
||||||
URL as a normal Python string. As with ``get_absolute_url()``, the result
|
:meth:`get_absolute_url()` and :meth:`item_link()` should return the
|
||||||
of :meth:`item_link()` will be included directly in the URL, so you are
|
item's URL as a normal Python string. As with ``get_absolute_url()``, the
|
||||||
responsible for doing all necessary URL quoting and conversion to ASCII
|
result of :meth:`item_link()` will be included directly in the URL, so you
|
||||||
inside the method itself.
|
are responsible for doing all necessary URL quoting and conversion to
|
||||||
|
ASCII inside the method itself.
|
||||||
* For the LatestEntries example above, we could have very simple feed
|
|
||||||
templates:
|
|
||||||
|
|
||||||
* latest_title.html:
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
|
||||||
|
|
||||||
{{ obj.title }}
|
|
||||||
|
|
||||||
* latest_description.html:
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
|
||||||
|
|
||||||
{{ obj.description }}
|
|
||||||
|
|
||||||
.. _chicagocrime.org: http://www.chicagocrime.org/
|
.. _chicagocrime.org: http://www.chicagocrime.org/
|
||||||
|
|
||||||
A complex example
|
A complex example
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
The framework also supports more complex feeds, via parameters.
|
The framework also supports more complex feeds, via arguments.
|
||||||
|
|
||||||
For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every
|
For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every
|
||||||
police beat in Chicago. It'd be silly to create a separate
|
police beat in Chicago. It'd be silly to create a separate
|
||||||
:class:`~django.contrib.syndication.feeds.Feed` class for each police beat; that
|
:class:`~django.contrib.syndication.views.Feed` class for each police beat; that
|
||||||
would violate the :ref:`DRY principle <dry>` and would couple data to
|
would violate the :ref:`DRY principle <dry>` and would couple data to
|
||||||
programming logic. Instead, the syndication framework lets you make generic
|
programming logic. Instead, the syndication framework lets you access the
|
||||||
feeds that output items based on information in the feed's URL.
|
arguments passed from your :ref:`URLconf <topics-http-urls>` so feeds can output
|
||||||
|
items based on information in the feed's URL.
|
||||||
|
|
||||||
On chicagocrime.org, the police-beat feeds are accessible via URLs like this:
|
On chicagocrime.org, the police-beat feeds are accessible via URLs like this:
|
||||||
|
|
||||||
* :file:`/rss/beats/0613/` -- Returns recent crimes for beat 0613.
|
* :file:`/beats/613/rss/` -- Returns recent crimes for beat 613.
|
||||||
* :file:`/rss/beats/1424/` -- Returns recent crimes for beat 1424.
|
* :file:`/beats/1424/rss/` -- Returns recent crimes for beat 1424.
|
||||||
|
|
||||||
The slug here is ``"beats"``. The syndication framework sees the extra URL bits
|
These can be matched with a :ref:`URLconf <topics-http-urls>` line such as::
|
||||||
after the slug -- ``0613`` and ``1424`` -- and gives you a hook to tell it what
|
|
||||||
those URL bits mean, and how they should influence which items get published in
|
|
||||||
the feed.
|
|
||||||
|
|
||||||
An example makes this clear. Here's the code for these beat-specific feeds::
|
(r'^beats/(?P<beat_id>\d+)/rss/$', BeatFeed()),
|
||||||
|
|
||||||
from django.contrib.syndication.feeds import FeedDoesNotExist
|
Like a view, the arguments in the URL are passed to the :meth:`get_object()`
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
method along with the request object.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.2
|
||||||
|
Prior to version 1.2, ``get_object()`` only accepted a ``bits`` argument.
|
||||||
|
|
||||||
|
Here's the code for these beat-specific feeds::
|
||||||
|
|
||||||
|
from django.contrib.syndication.views import FeedDoesNotExist
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
class BeatFeed(Feed):
|
class BeatFeed(Feed):
|
||||||
def get_object(self, bits):
|
description_template = 'feeds/beat_description.html'
|
||||||
# In case of "/rss/beats/0613/foo/bar/baz/", or other such clutter,
|
|
||||||
# check that bits has only one member.
|
def get_object(self, request, beat_id):
|
||||||
if len(bits) != 1:
|
return get_object_or_404(Beat, pk=beat_id)
|
||||||
raise ObjectDoesNotExist
|
|
||||||
return Beat.objects.get(beat__exact=bits[0])
|
|
||||||
|
|
||||||
def title(self, obj):
|
def title(self, obj):
|
||||||
return "Chicagocrime.org: Crimes for beat %s" % obj.beat
|
return "Chicagocrime.org: Crimes for beat %s" % obj.beat
|
||||||
|
|
||||||
def link(self, obj):
|
def link(self, obj):
|
||||||
if not obj:
|
|
||||||
raise FeedDoesNotExist
|
|
||||||
return obj.get_absolute_url()
|
return obj.get_absolute_url()
|
||||||
|
|
||||||
def description(self, obj):
|
def description(self, obj):
|
||||||
return "Crimes recently reported in police beat %s" % obj.beat
|
return "Crimes recently reported in police beat %s" % obj.beat
|
||||||
|
|
||||||
def items(self, obj):
|
def items(self, obj):
|
||||||
return Crime.objects.filter(beat__id__exact=obj.id).order_by('-crime_date')[:30]
|
return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30]
|
||||||
|
|
||||||
Here's the basic algorithm the RSS framework follows, given this class and a
|
To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django
|
||||||
request to the URL :file:`/rss/beats/0613/`:
|
uses the :meth:`title()`, :meth:`link()` and :meth:`description()` methods. In
|
||||||
|
the previous example, they were simple string class attributes, but this example
|
||||||
* The framework gets the URL :file:`/rss/beats/0613/` and notices there's an
|
illustrates that they can be either strings *or* methods. For each of
|
||||||
extra bit of URL after the slug. It splits that remaining string by the
|
:attr:`title`, :attr:`link` and :attr:`description`, Django follows this
|
||||||
slash character (``"/"``) and calls the
|
algorithm:
|
||||||
:class:`~django.contrib.syndication.feeds.Feed` class'
|
|
||||||
:meth:`get_object()` method, passing it the bits. In this case, bits is
|
|
||||||
``['0613']``. For a request to :file:`/rss/beats/0613/foo/bar/`, bits
|
|
||||||
would be ``['0613', 'foo', 'bar']``.
|
|
||||||
|
|
||||||
* :meth:`get_object()` is responsible for retrieving the given beat, from
|
|
||||||
the given ``bits``. In this case, it uses the Django database API to
|
|
||||||
retrieve the beat. Note that :meth:`get_object()` should raise
|
|
||||||
:exc:`django.core.exceptions.ObjectDoesNotExist` if given invalid
|
|
||||||
parameters. There's no ``try``/``except`` around the
|
|
||||||
``Beat.objects.get()`` call, because it's not necessary; that function
|
|
||||||
raises :exc:`Beat.DoesNotExist` on failure, and :exc:`Beat.DoesNotExist`
|
|
||||||
is a subclass of :exc:`ObjectDoesNotExist`. Raising
|
|
||||||
:exc:`ObjectDoesNotExist` in :meth:`get_object()` tells Django to produce
|
|
||||||
a 404 error for that request.
|
|
||||||
|
|
||||||
.. versionadded:: 1.0
|
|
||||||
:meth:`get_object()` can handle the :file:`/rss/beats/` url.
|
|
||||||
|
|
||||||
The :meth:`get_object()` method also has a chance to handle the
|
|
||||||
:file:`/rss/beats/` url. In this case, :data:`bits` will be an
|
|
||||||
empty list. In our example, ``len(bits) != 1`` and an
|
|
||||||
:exc:`ObjectDoesNotExist` exception will be raised, so
|
|
||||||
:file:`/rss/beats/` will generate a 404 page. But you can handle this case
|
|
||||||
however you like. For example, you could generate a combined feed for all
|
|
||||||
beats.
|
|
||||||
|
|
||||||
* To generate the feed's ``<title>``, ``<link>`` and ``<description>``,
|
|
||||||
Django uses the :meth:`title()`, :meth:`link()` and :meth:`description()`
|
|
||||||
methods. In the previous example, they were simple string class
|
|
||||||
attributes, but this example illustrates that they can be either strings
|
|
||||||
*or* methods. For each of :attr:`title`, :attr:`link` and
|
|
||||||
:attr:`description`, Django follows this algorithm:
|
|
||||||
|
|
||||||
* First, it tries to call a method, passing the ``obj`` argument, where
|
* First, it tries to call a method, passing the ``obj`` argument, where
|
||||||
``obj`` is the object returned by :meth:`get_object()`.
|
``obj`` is the object returned by :meth:`get_object()`.
|
||||||
|
@ -284,20 +222,20 @@ request to the URL :file:`/rss/beats/0613/`:
|
||||||
|
|
||||||
* Failing that, it uses the class attribute.
|
* Failing that, it uses the class attribute.
|
||||||
|
|
||||||
Inside the :meth:`link()` method, we handle the possibility that ``obj``
|
Also note that :meth:`items()` also follows the same algorithm -- first, it
|
||||||
might be ``None``, which can occur when the URL isn't fully specified. In
|
tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items`
|
||||||
some cases, you might want to do something else in this case, which would
|
class attribute (which should be a list).
|
||||||
mean you'd need to check for ``obj`` existing in other methods as well.
|
|
||||||
(The :meth:`link()` method is called very early in the feed generation
|
|
||||||
process, so it's a good place to bail out early.)
|
|
||||||
|
|
||||||
* Finally, note that :meth:`items()` in this example also takes the ``obj``
|
We are using a template for the item descriptions. It can be very simple:
|
||||||
argument. The algorithm for :attr:`items` is the same as described in the
|
|
||||||
previous step -- first, it tries :meth:`items(obj)`, then :meth:`items()`,
|
.. code-block:: html+django
|
||||||
then finally an :attr:`items` class attribute (which should be a list).
|
|
||||||
|
{{ obj.description }}
|
||||||
|
|
||||||
|
However, you are free to add formatting as desired.
|
||||||
|
|
||||||
The ``ExampleFeed`` class below gives full documentation on methods and
|
The ``ExampleFeed`` class below gives full documentation on methods and
|
||||||
attributes of :class:`~django.contrib.syndication.feeds.Feed` classes.
|
attributes of :class:`~django.contrib.syndication.views.Feed` classes.
|
||||||
|
|
||||||
Specifying the type of feed
|
Specifying the type of feed
|
||||||
---------------------------
|
---------------------------
|
||||||
|
@ -305,7 +243,7 @@ Specifying the type of feed
|
||||||
By default, feeds produced in this framework use RSS 2.0.
|
By default, feeds produced in this framework use RSS 2.0.
|
||||||
|
|
||||||
To change that, add a ``feed_type`` attribute to your
|
To change that, add a ``feed_type`` attribute to your
|
||||||
:class:`~django.contrib.syndication.feeds.Feed` class, like so::
|
:class:`~django.contrib.syndication.views.Feed` class, like so::
|
||||||
|
|
||||||
from django.utils.feedgenerator import Atom1Feed
|
from django.utils.feedgenerator import Atom1Feed
|
||||||
|
|
||||||
|
@ -353,13 +291,13 @@ Publishing Atom and RSS feeds in tandem
|
||||||
|
|
||||||
Some developers like to make available both Atom *and* RSS versions of their
|
Some developers like to make available both Atom *and* RSS versions of their
|
||||||
feeds. That's easy to do with Django: Just create a subclass of your
|
feeds. That's easy to do with Django: Just create a subclass of your
|
||||||
:class:`~django.contrib.syndication.feeds.Feed`
|
:class:`~django.contrib.syndication.views.Feed`
|
||||||
class and set the :attr:`feed_type` to something different. Then update your
|
class and set the :attr:`feed_type` to something different. Then update your
|
||||||
URLconf to add the extra versions.
|
URLconf to add the extra versions.
|
||||||
|
|
||||||
Here's a full example::
|
Here's a full example::
|
||||||
|
|
||||||
from django.contrib.syndication.feeds import Feed
|
from django.contrib.syndication.views import Feed
|
||||||
from chicagocrime.models import NewsItem
|
from chicagocrime.models import NewsItem
|
||||||
from django.utils.feedgenerator import Atom1Feed
|
from django.utils.feedgenerator import Atom1Feed
|
||||||
|
|
||||||
|
@ -381,7 +319,7 @@ Here's a full example::
|
||||||
a feed-level "description," but they *do* provide for a "subtitle."
|
a feed-level "description," but they *do* provide for a "subtitle."
|
||||||
|
|
||||||
If you provide a :attr:`description` in your
|
If you provide a :attr:`description` in your
|
||||||
:class:`~django.contrib.syndication.feeds.Feed` class, Django will *not*
|
:class:`~django.contrib.syndication.views.Feed` class, Django will *not*
|
||||||
automatically put that into the :attr:`subtitle` element, because a
|
automatically put that into the :attr:`subtitle` element, because a
|
||||||
subtitle and description are not necessarily the same thing. Instead, you
|
subtitle and description are not necessarily the same thing. Instead, you
|
||||||
should define a :attr:`subtitle` attribute.
|
should define a :attr:`subtitle` attribute.
|
||||||
|
@ -394,56 +332,50 @@ And the accompanying URLconf::
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
|
from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
|
||||||
|
|
||||||
feeds = {
|
|
||||||
'rss': RssSiteNewsFeed,
|
|
||||||
'atom': AtomSiteNewsFeed,
|
|
||||||
}
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
# ...
|
# ...
|
||||||
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
|
(r'^sitenews/rss/$', RssSiteNewsFeed()),
|
||||||
{'feed_dict': feeds}),
|
(r'^sitenews/atom/$', AtomSiteNewsFeed()),
|
||||||
# ...
|
# ...
|
||||||
)
|
)
|
||||||
|
|
||||||
Feed class reference
|
Feed class reference
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
.. class:: django.contrib.syndication.feeds.Feed
|
.. class:: django.contrib.syndication.views.Feed
|
||||||
|
|
||||||
This example illustrates all possible attributes and methods for a
|
This example illustrates all possible attributes and methods for a
|
||||||
:class:`~django.contrib.syndication.feeds.Feed` class::
|
:class:`~django.contrib.syndication.views.Feed` class::
|
||||||
|
|
||||||
from django.contrib.syndication.feeds import Feed
|
from django.contrib.syndication.views import Feed
|
||||||
from django.utils import feedgenerator
|
from django.utils import feedgenerator
|
||||||
|
|
||||||
class ExampleFeed(Feed):
|
class ExampleFeed(Feed):
|
||||||
|
|
||||||
# FEED TYPE -- Optional. This should be a class that subclasses
|
# FEED TYPE -- Optional. This should be a class that subclasses
|
||||||
# django.utils.feedgenerator.SyndicationFeed. This designates which
|
# django.utils.feedgenerator.SyndicationFeed. This designates
|
||||||
# type of feed this should be: RSS 2.0, Atom 1.0, etc.
|
# which type of feed this should be: RSS 2.0, Atom 1.0, etc. If
|
||||||
# If you don't specify feed_type, your feed will be RSS 2.0.
|
# you don't specify feed_type, your feed will be RSS 2.0. This
|
||||||
# This should be a class, not an instance of the class.
|
# should be a class, not an instance of the class.
|
||||||
|
|
||||||
feed_type = feedgenerator.Rss201rev2Feed
|
feed_type = feedgenerator.Rss201rev2Feed
|
||||||
|
|
||||||
# TEMPLATE NAMES -- Optional. These should be strings representing
|
# TEMPLATE NAMES -- Optional. These should be strings
|
||||||
# names of Django templates that the system should use in rendering the
|
# representing names of Django templates that the system should
|
||||||
# title and description of your feed items. Both are optional.
|
# use in rendering the title and description of your feed items.
|
||||||
# If you don't specify one, or either, Django will use the template
|
# Both are optional. If a template is not specified, the
|
||||||
# 'feeds/SLUG_title.html' and 'feeds/SLUG_description.html', where SLUG
|
# item_title() or item_description() methods are used instead.
|
||||||
# is the slug you specify in the URL.
|
|
||||||
|
|
||||||
title_template = None
|
title_template = None
|
||||||
description_template = None
|
description_template = None
|
||||||
|
|
||||||
# TITLE -- One of the following three is required. The framework looks
|
# TITLE -- One of the following three is required. The framework
|
||||||
# for them in this order.
|
# looks for them in this order.
|
||||||
|
|
||||||
def title(self, obj):
|
def title(self, obj):
|
||||||
"""
|
"""
|
||||||
Takes the object returned by get_object() and returns the feed's
|
Takes the object returned by get_object() and returns the
|
||||||
title as a normal Python string.
|
feed's title as a normal Python string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def title(self):
|
def title(self):
|
||||||
|
@ -453,13 +385,13 @@ This example illustrates all possible attributes and methods for a
|
||||||
|
|
||||||
title = 'foo' # Hard-coded title.
|
title = 'foo' # Hard-coded title.
|
||||||
|
|
||||||
# LINK -- One of the following three is required. The framework looks
|
# LINK -- One of the following three is required. The framework
|
||||||
# for them in this order.
|
# looks for them in this order.
|
||||||
|
|
||||||
def link(self, obj):
|
def link(self, obj):
|
||||||
"""
|
"""
|
||||||
Takes the object returned by get_object() and returns the feed's
|
# Takes the object returned by get_object() and returns the feed's
|
||||||
link as a normal Python string.
|
# link as a normal Python string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def link(self):
|
def link(self):
|
||||||
|
@ -572,18 +504,18 @@ This example illustrates all possible attributes and methods for a
|
||||||
# COPYRIGHT NOTICE -- One of the following three is optional. The
|
# COPYRIGHT NOTICE -- One of the following three is optional. The
|
||||||
# framework looks for them in this order.
|
# framework looks for them in this order.
|
||||||
|
|
||||||
def copyright(self, obj):
|
def feed_copyright(self, obj):
|
||||||
"""
|
"""
|
||||||
Takes the object returned by get_object() and returns the feed's
|
Takes the object returned by get_object() and returns the feed's
|
||||||
copyright notice as a normal Python string.
|
copyright notice as a normal Python string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def copyright(self):
|
def feed_copyright(self):
|
||||||
"""
|
"""
|
||||||
Returns the feed's copyright notice as a normal Python string.
|
Returns the feed's copyright notice as a normal Python string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
|
feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.
|
||||||
|
|
||||||
# TTL -- One of the following three is optional. The framework looks
|
# TTL -- One of the following three is optional. The framework looks
|
||||||
# for them in this order. Ignored for Atom feeds.
|
# for them in this order. Ignored for Atom feeds.
|
||||||
|
@ -620,13 +552,44 @@ This example illustrates all possible attributes and methods for a
|
||||||
# GET_OBJECT -- This is required for feeds that publish different data
|
# GET_OBJECT -- This is required for feeds that publish different data
|
||||||
# for different URL parameters. (See "A complex example" above.)
|
# for different URL parameters. (See "A complex example" above.)
|
||||||
|
|
||||||
def get_object(self, bits):
|
def get_object(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Takes a list of strings gleaned from the URL and returns an object
|
Takes the current request and the arguments from the URL, and
|
||||||
represented by this feed. Raises
|
returns an object represented by this feed. Raises
|
||||||
django.core.exceptions.ObjectDoesNotExist on error.
|
django.core.exceptions.ObjectDoesNotExist on error.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# ITEM TITLE AND DESCRIPTION -- If title_template or
|
||||||
|
# description_template are not defined, these are used instead. Both are
|
||||||
|
# optional, by default they will use the unicode representation of the
|
||||||
|
# item.
|
||||||
|
|
||||||
|
def item_title(self, item):
|
||||||
|
"""
|
||||||
|
Takes an item, as returned by items(), and returns the item's
|
||||||
|
title as a normal Python string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def item_title(self):
|
||||||
|
"""
|
||||||
|
Returns the title for every item in the feed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
item_title = 'Breaking News: Nothing Happening' # Hard-coded title.
|
||||||
|
|
||||||
|
def item_description(self, item):
|
||||||
|
"""
|
||||||
|
Takes an item, as returned by items(), and returns the item's
|
||||||
|
description as a normal Python string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def item_description(self):
|
||||||
|
"""
|
||||||
|
Returns the description for every item in the feed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
item_description = 'A description of the item.' # Hard-coded description.
|
||||||
|
|
||||||
# ITEM LINK -- One of these three is required. The framework looks for
|
# ITEM LINK -- One of these three is required. The framework looks for
|
||||||
# them in this order.
|
# them in this order.
|
||||||
|
|
||||||
|
@ -686,7 +649,7 @@ This example illustrates all possible attributes and methods for a
|
||||||
|
|
||||||
item_author_email = 'test@example.com' # Hard-coded author e-mail.
|
item_author_email = 'test@example.com' # Hard-coded author e-mail.
|
||||||
|
|
||||||
# ITEM AUTHOR LINK --One of the following three is optional. The
|
# ITEM AUTHOR LINK -- One of the following three is optional. The
|
||||||
# framework looks for them in this order. In each case, the URL should
|
# framework looks for them in this order. In each case, the URL should
|
||||||
# include the "http://" and domain name.
|
# include the "http://" and domain name.
|
||||||
#
|
#
|
||||||
|
|
|
@ -386,6 +386,87 @@ approach. Old style function-based test runners will still work, but
|
||||||
should be updated to use the new :ref:`class-based runners
|
should be updated to use the new :ref:`class-based runners
|
||||||
<topics-testing-test_runner>`.
|
<topics-testing-test_runner>`.
|
||||||
|
|
||||||
|
.. _1.2-updating-feeds:
|
||||||
|
|
||||||
|
``Feed`` in ``django.contrib.syndication.feeds``
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
The :class:`django.contrib.syndication.feeds.Feed` class has been
|
||||||
|
replaced by the :class:`django.contrib.syndication.views.Feed` class.
|
||||||
|
The old ``feeds.Feed`` class is deprecated, and will be removed in
|
||||||
|
Django 1.4.
|
||||||
|
|
||||||
|
The new class has an almost identical API, but allows instances to be
|
||||||
|
used as views. For example, consider the use of the old framework in
|
||||||
|
the following :ref:`URLconf <topics-http-urls>`::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from myproject.feeds import LatestEntries, LatestEntriesByCategory
|
||||||
|
|
||||||
|
feeds = {
|
||||||
|
'latest': LatestEntries,
|
||||||
|
'categories': LatestEntriesByCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
# ...
|
||||||
|
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
|
||||||
|
{'feed_dict': feeds}),
|
||||||
|
# ...
|
||||||
|
)
|
||||||
|
|
||||||
|
Using the new Feed class, these feeds can be deployed directly as views::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from myproject.feeds import LatestEntries, LatestEntriesByCategory
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
# ...
|
||||||
|
(r'^feeds/latest/$', LatestEntries()),
|
||||||
|
(r'^feeds/categories/(?P<category_id>\d+)/$', LatestEntriesByCategory()),
|
||||||
|
# ...
|
||||||
|
)
|
||||||
|
|
||||||
|
If you currently use the ``feed()`` view, the ``LatestEntries`` class
|
||||||
|
would not need to be modified apart from subclassing the new
|
||||||
|
:class:`~django.contrib.syndication.views.Feed` class.
|
||||||
|
|
||||||
|
However, ``LatestEntriesByCategory`` uses the ``get_object()`` method
|
||||||
|
with the ``bits`` argument to specify a specific category to show. In
|
||||||
|
the new :class:`~django.contrib.syndication.views.Feed` class,
|
||||||
|
``get_object()`` method takes a ``request`` and arguments from the
|
||||||
|
URL, so it would look like this::
|
||||||
|
|
||||||
|
from django.contrib.syndication.views import Feed
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from myproject.models import Category
|
||||||
|
|
||||||
|
class LatestEntriesByCategory(Feed):
|
||||||
|
def get_object(self, request, category_id):
|
||||||
|
return get_object_or_404(Category, id=category_id)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
Additionally, the ``get_feed()`` method on ``Feed`` classes now take
|
||||||
|
different arguments, which may impact you if you use the ``Feed``
|
||||||
|
classes directly. Instead of just taking an optional ``url`` argument,
|
||||||
|
it now takes two arguments: the object returned by its own
|
||||||
|
``get_object()`` method, and the current ``request`` object.
|
||||||
|
|
||||||
|
To take into account ``Feed`` classes not being initialized for each
|
||||||
|
request, the ``__init__()`` method now takes no arguments by default.
|
||||||
|
Previously it would have taken the ``slug`` from the URL and the
|
||||||
|
``request`` object.
|
||||||
|
|
||||||
|
In accordance with `RSS best practices`_, RSS feeds will now include
|
||||||
|
an ``atom:link`` element. You may need to update your tests to take
|
||||||
|
this into account.
|
||||||
|
|
||||||
|
For more information, see the full :ref:`syndication framework
|
||||||
|
documentation <ref-contrib-syndication>`.
|
||||||
|
|
||||||
|
.. _RSS best practices: http://www.rssboard.org/rss-profile
|
||||||
|
|
||||||
What's new in Django 1.2
|
What's new in Django 1.2
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
@ -568,3 +649,12 @@ User already did. This is useful for centralizing permission handling - apps
|
||||||
can always delegate the question of whether something is allowed or not to
|
can always delegate the question of whether something is allowed or not to
|
||||||
the authorization/authentication backend. See the :ref:`authentication
|
the authorization/authentication backend. See the :ref:`authentication
|
||||||
docs <topics-auth>` for more details.
|
docs <topics-auth>` for more details.
|
||||||
|
|
||||||
|
Syndication feeds as views
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
:ref:`Syndication feeds <ref-contrib-syndication>` can now be used directly as
|
||||||
|
views in your :ref:`URLconf <topics-http-urls>`. This means that you can
|
||||||
|
maintain complete control over the URL structure of your feeds. Like any other view, feeds views are passed a ``request`` object, so you can
|
||||||
|
do anything you would normally do with a view, like user based access control,
|
||||||
|
or making a feed a named URL.
|
||||||
|
|
|
@ -1,29 +1,102 @@
|
||||||
|
from django.contrib.syndication import feeds, views
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.contrib.syndication import feeds
|
from django.utils import feedgenerator, tzinfo
|
||||||
from django.utils.feedgenerator import Atom1Feed
|
from models import Article, Entry
|
||||||
from django.utils import tzinfo
|
|
||||||
|
|
||||||
class ComplexFeed(feeds.Feed):
|
|
||||||
def get_object(self, bits):
|
class ComplexFeed(views.Feed):
|
||||||
if len(bits) != 1:
|
def get_object(self, request, foo=None):
|
||||||
|
if foo is not None:
|
||||||
raise ObjectDoesNotExist
|
raise ObjectDoesNotExist
|
||||||
return None
|
return None
|
||||||
|
|
||||||
class TestRssFeed(feeds.Feed):
|
|
||||||
link = "/blog/"
|
class TestRss2Feed(views.Feed):
|
||||||
title = 'My blog'
|
title = 'My blog'
|
||||||
|
description = 'A more thorough description of my blog.'
|
||||||
|
link = '/blog/'
|
||||||
|
feed_guid = '/foo/bar/1234'
|
||||||
|
author_name = 'Sally Smith'
|
||||||
|
author_email = 'test@example.com'
|
||||||
|
author_link = 'http://www.example.com/'
|
||||||
|
categories = ('python', 'django')
|
||||||
|
feed_copyright = 'Copyright (c) 2007, Sally Smith'
|
||||||
|
ttl = 600
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
from models import Entry
|
|
||||||
return Entry.objects.all()
|
return Entry.objects.all()
|
||||||
|
|
||||||
def item_link(self, item):
|
def item_description(self, item):
|
||||||
return "/blog/%s/" % item.pk
|
return "Overridden description: %s" % item
|
||||||
|
|
||||||
class TestAtomFeed(TestRssFeed):
|
def item_pubdate(self, item):
|
||||||
feed_type = Atom1Feed
|
return item.date
|
||||||
|
|
||||||
class MyCustomAtom1Feed(Atom1Feed):
|
item_author_name = 'Sally Smith'
|
||||||
|
item_author_email = 'test@example.com'
|
||||||
|
item_author_link = 'http://www.example.com/'
|
||||||
|
item_categories = ('python', 'testing')
|
||||||
|
item_copyright = 'Copyright (c) 2007, Sally Smith'
|
||||||
|
|
||||||
|
|
||||||
|
class TestRss091Feed(TestRss2Feed):
|
||||||
|
feed_type = feedgenerator.RssUserland091Feed
|
||||||
|
|
||||||
|
|
||||||
|
class TestAtomFeed(TestRss2Feed):
|
||||||
|
feed_type = feedgenerator.Atom1Feed
|
||||||
|
subtitle = TestRss2Feed.description
|
||||||
|
|
||||||
|
|
||||||
|
class ArticlesFeed(TestRss2Feed):
|
||||||
|
"""
|
||||||
|
A feed to test no link being defined. Articles have no get_absolute_url()
|
||||||
|
method, and item_link() is not defined.
|
||||||
|
"""
|
||||||
|
def items(self):
|
||||||
|
return Article.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnclosureFeed(TestRss2Feed):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateFeed(TestRss2Feed):
|
||||||
|
"""
|
||||||
|
A feed to test defining item titles and descriptions with templates.
|
||||||
|
"""
|
||||||
|
title_template = 'syndication/title.html'
|
||||||
|
description_template = 'syndication/description.html'
|
||||||
|
|
||||||
|
# Defining a template overrides any item_title definition
|
||||||
|
def item_title(self):
|
||||||
|
return "Not in a template"
|
||||||
|
|
||||||
|
|
||||||
|
class NaiveDatesFeed(TestAtomFeed):
|
||||||
|
"""
|
||||||
|
A feed with naive (non-timezone-aware) dates.
|
||||||
|
"""
|
||||||
|
def item_pubdate(self, item):
|
||||||
|
return item.date
|
||||||
|
|
||||||
|
|
||||||
|
class TZAwareDatesFeed(TestAtomFeed):
|
||||||
|
"""
|
||||||
|
A feed with timezone-aware dates.
|
||||||
|
"""
|
||||||
|
def item_pubdate(self, item):
|
||||||
|
# Provide a weird offset so that the test can know it's getting this
|
||||||
|
# specific offset and not accidentally getting on from
|
||||||
|
# settings.TIME_ZONE.
|
||||||
|
return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
|
||||||
|
|
||||||
|
|
||||||
|
class TestFeedUrlFeed(TestAtomFeed):
|
||||||
|
feed_url = 'http://example.com/customfeedurl/'
|
||||||
|
|
||||||
|
|
||||||
|
class MyCustomAtom1Feed(feedgenerator.Atom1Feed):
|
||||||
"""
|
"""
|
||||||
Test of a custom feed generator class.
|
Test of a custom feed generator class.
|
||||||
"""
|
"""
|
||||||
|
@ -45,22 +118,25 @@ class MyCustomAtom1Feed(Atom1Feed):
|
||||||
super(MyCustomAtom1Feed, self).add_item_elements(handler, item)
|
super(MyCustomAtom1Feed, self).add_item_elements(handler, item)
|
||||||
handler.addQuickElement(u'ministry', u'silly walks')
|
handler.addQuickElement(u'ministry', u'silly walks')
|
||||||
|
|
||||||
|
|
||||||
class TestCustomFeed(TestAtomFeed):
|
class TestCustomFeed(TestAtomFeed):
|
||||||
feed_type = MyCustomAtom1Feed
|
feed_type = MyCustomAtom1Feed
|
||||||
|
|
||||||
class NaiveDatesFeed(TestAtomFeed):
|
|
||||||
"""
|
|
||||||
A feed with naive (non-timezone-aware) dates.
|
|
||||||
"""
|
|
||||||
def item_pubdate(self, item):
|
|
||||||
return item.date
|
|
||||||
|
|
||||||
class TZAwareDatesFeed(TestAtomFeed):
|
class DeprecatedComplexFeed(feeds.Feed):
|
||||||
"""
|
def get_object(self, bits):
|
||||||
A feed with timezone-aware dates.
|
if len(bits) != 1:
|
||||||
"""
|
raise ObjectDoesNotExist
|
||||||
def item_pubdate(self, item):
|
return None
|
||||||
# Provide a weird offset so that the test can know it's getting this
|
|
||||||
# specific offset and not accidentally getting on from
|
|
||||||
# settings.TIME_ZONE.
|
class DeprecatedRssFeed(feeds.Feed):
|
||||||
return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
|
link = "/blog/"
|
||||||
|
title = 'My blog'
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Entry.objects.all()
|
||||||
|
|
||||||
|
def item_link(self, item):
|
||||||
|
return "/blog/%s/" % item.pk
|
||||||
|
|
||||||
|
|
|
@ -30,5 +30,13 @@
|
||||||
"title": "A & B < C > D",
|
"title": "A & B < C > D",
|
||||||
"date": "2008-01-03 13:30:00"
|
"date": "2008-01-03 13:30:00"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "syndication.article",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"title": "My first article",
|
||||||
|
"entry": "1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -4,5 +4,20 @@ class Entry(models.Model):
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
date = models.DateTimeField()
|
date = models.DateTimeField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('date',)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return "/blog/%s/" % self.pk
|
||||||
|
|
||||||
|
|
||||||
|
class Article(models.Model):
|
||||||
|
title = models.CharField(max_length=200)
|
||||||
|
entry = models.ForeignKey(Entry)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Description in your templates: {{ obj }}
|
|
@ -0,0 +1 @@
|
||||||
|
Title in your templates: {{ obj }}
|
|
@ -1,17 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from xml.dom import minidom
|
from django.contrib.syndication import feeds, views
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import Client
|
|
||||||
from django.utils import tzinfo
|
from django.utils import tzinfo
|
||||||
from models import Entry
|
from models import Entry
|
||||||
|
from xml.dom import minidom
|
||||||
|
|
||||||
try:
|
try:
|
||||||
set
|
set
|
||||||
except NameError:
|
except NameError:
|
||||||
from sets import Set as set
|
from sets import Set as set
|
||||||
|
|
||||||
class SyndicationFeedTest(TestCase):
|
class FeedTestCase(TestCase):
|
||||||
fixtures = ['feeddata.json']
|
fixtures = ['feeddata.json']
|
||||||
|
|
||||||
def assertChildNodes(self, elem, expected):
|
def assertChildNodes(self, elem, expected):
|
||||||
|
@ -19,8 +19,28 @@ class SyndicationFeedTest(TestCase):
|
||||||
expected = set(expected)
|
expected = set(expected)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
def test_rss_feed(self):
|
def assertChildNodeContent(self, elem, expected):
|
||||||
response = self.client.get('/syndication/feeds/rss/')
|
for k, v in expected.items():
|
||||||
|
self.assertEqual(
|
||||||
|
elem.getElementsByTagName(k)[0].firstChild.wholeText, v)
|
||||||
|
|
||||||
|
def assertCategories(self, elem, expected):
|
||||||
|
self.assertEqual(set(i.firstChild.wholeText for i in elem.childNodes if i.nodeName == 'category'), set(expected));
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Feed view
|
||||||
|
######################################
|
||||||
|
|
||||||
|
class SyndicationFeedTest(FeedTestCase):
|
||||||
|
"""
|
||||||
|
Tests for the high-level syndication feed framework.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_rss2_feed(self):
|
||||||
|
"""
|
||||||
|
Test the structure and content of feeds generated by Rss201rev2Feed.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/rss2/')
|
||||||
doc = minidom.parseString(response.content)
|
doc = minidom.parseString(response.content)
|
||||||
|
|
||||||
# Making sure there's only 1 `rss` element and that the correct
|
# Making sure there's only 1 `rss` element and that the correct
|
||||||
|
@ -35,59 +55,133 @@ class SyndicationFeedTest(TestCase):
|
||||||
chan_elem = feed.getElementsByTagName('channel')
|
chan_elem = feed.getElementsByTagName('channel')
|
||||||
self.assertEqual(len(chan_elem), 1)
|
self.assertEqual(len(chan_elem), 1)
|
||||||
chan = chan_elem[0]
|
chan = chan_elem[0]
|
||||||
self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item'])
|
self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category'])
|
||||||
|
self.assertChildNodeContent(chan, {
|
||||||
|
'title': 'My blog',
|
||||||
|
'description': 'A more thorough description of my blog.',
|
||||||
|
'link': 'http://example.com/blog/',
|
||||||
|
'language': 'en',
|
||||||
|
'lastBuildDate': 'Thu, 03 Jan 2008 13:30:00 -0600',
|
||||||
|
#'atom:link': '',
|
||||||
|
'ttl': '600',
|
||||||
|
'copyright': 'Copyright (c) 2007, Sally Smith',
|
||||||
|
})
|
||||||
|
self.assertCategories(chan, ['python', 'django']);
|
||||||
|
|
||||||
|
# Ensure the content of the channel is correct
|
||||||
|
self.assertChildNodeContent(chan, {
|
||||||
|
'title': 'My blog',
|
||||||
|
'link': 'http://example.com/blog/',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check feed_url is passed
|
||||||
|
self.assertEqual(
|
||||||
|
chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
|
||||||
|
'http://example.com/syndication/rss2/'
|
||||||
|
)
|
||||||
|
|
||||||
items = chan.getElementsByTagName('item')
|
items = chan.getElementsByTagName('item')
|
||||||
self.assertEqual(len(items), Entry.objects.count())
|
self.assertEqual(len(items), Entry.objects.count())
|
||||||
for item in items:
|
self.assertChildNodeContent(items[0], {
|
||||||
self.assertChildNodes(item, ['title', 'link', 'description', 'guid'])
|
'title': 'My first entry',
|
||||||
|
'description': 'Overridden description: My first entry',
|
||||||
|
'link': 'http://example.com/blog/1/',
|
||||||
|
'guid': 'http://example.com/blog/1/',
|
||||||
|
'pubDate': 'Tue, 01 Jan 2008 12:30:00 -0600',
|
||||||
|
'author': 'test@example.com (Sally Smith)',
|
||||||
|
})
|
||||||
|
self.assertCategories(items[0], ['python', 'testing']);
|
||||||
|
|
||||||
def test_atom_feed(self):
|
for item in items:
|
||||||
response = self.client.get('/syndication/feeds/atom/')
|
self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'category', 'pubDate', 'author'])
|
||||||
|
|
||||||
|
def test_rss091_feed(self):
|
||||||
|
"""
|
||||||
|
Test the structure and content of feeds generated by RssUserland091Feed.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/rss091/')
|
||||||
doc = minidom.parseString(response.content)
|
doc = minidom.parseString(response.content)
|
||||||
|
|
||||||
feed = doc.firstChild
|
# Making sure there's only 1 `rss` element and that the correct
|
||||||
|
# RSS version was specified.
|
||||||
|
feed_elem = doc.getElementsByTagName('rss')
|
||||||
|
self.assertEqual(len(feed_elem), 1)
|
||||||
|
feed = feed_elem[0]
|
||||||
|
self.assertEqual(feed.getAttribute('version'), '0.91')
|
||||||
|
|
||||||
|
# Making sure there's only one `channel` element w/in the
|
||||||
|
# `rss` element.
|
||||||
|
chan_elem = feed.getElementsByTagName('channel')
|
||||||
|
self.assertEqual(len(chan_elem), 1)
|
||||||
|
chan = chan_elem[0]
|
||||||
|
self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category'])
|
||||||
|
|
||||||
|
# Ensure the content of the channel is correct
|
||||||
|
self.assertChildNodeContent(chan, {
|
||||||
|
'title': 'My blog',
|
||||||
|
'link': 'http://example.com/blog/',
|
||||||
|
})
|
||||||
|
self.assertCategories(chan, ['python', 'django'])
|
||||||
|
|
||||||
|
# Check feed_url is passed
|
||||||
|
self.assertEqual(
|
||||||
|
chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
|
||||||
|
'http://example.com/syndication/rss091/'
|
||||||
|
)
|
||||||
|
|
||||||
|
items = chan.getElementsByTagName('item')
|
||||||
|
self.assertEqual(len(items), Entry.objects.count())
|
||||||
|
self.assertChildNodeContent(items[0], {
|
||||||
|
'title': 'My first entry',
|
||||||
|
'description': 'Overridden description: My first entry',
|
||||||
|
'link': 'http://example.com/blog/1/',
|
||||||
|
})
|
||||||
|
for item in items:
|
||||||
|
self.assertChildNodes(item, ['title', 'link', 'description'])
|
||||||
|
self.assertCategories(item, [])
|
||||||
|
|
||||||
|
def test_atom_feed(self):
|
||||||
|
"""
|
||||||
|
Test the structure and content of feeds generated by Atom1Feed.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/atom/')
|
||||||
|
feed = minidom.parseString(response.content).firstChild
|
||||||
|
|
||||||
self.assertEqual(feed.nodeName, 'feed')
|
self.assertEqual(feed.nodeName, 'feed')
|
||||||
self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom')
|
self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom')
|
||||||
self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry'])
|
self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'rights', 'category', 'author'])
|
||||||
|
for link in feed.getElementsByTagName('link'):
|
||||||
|
if link.getAttribute('rel') == 'self':
|
||||||
|
self.assertEqual(link.getAttribute('href'), 'http://example.com/syndication/atom/')
|
||||||
|
|
||||||
entries = feed.getElementsByTagName('entry')
|
entries = feed.getElementsByTagName('entry')
|
||||||
self.assertEqual(len(entries), Entry.objects.count())
|
self.assertEqual(len(entries), Entry.objects.count())
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary'])
|
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'category', 'updated', 'rights', 'author'])
|
||||||
summary = entry.getElementsByTagName('summary')[0]
|
summary = entry.getElementsByTagName('summary')[0]
|
||||||
self.assertEqual(summary.getAttribute('type'), 'html')
|
self.assertEqual(summary.getAttribute('type'), 'html')
|
||||||
|
|
||||||
def test_custom_feed_generator(self):
|
def test_custom_feed_generator(self):
|
||||||
response = self.client.get('/syndication/feeds/custom/')
|
response = self.client.get('/syndication/custom/')
|
||||||
doc = minidom.parseString(response.content)
|
feed = minidom.parseString(response.content).firstChild
|
||||||
|
|
||||||
feed = doc.firstChild
|
|
||||||
self.assertEqual(feed.nodeName, 'feed')
|
self.assertEqual(feed.nodeName, 'feed')
|
||||||
self.assertEqual(feed.getAttribute('django'), 'rocks')
|
self.assertEqual(feed.getAttribute('django'), 'rocks')
|
||||||
self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry', 'spam'])
|
self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'spam', 'rights', 'category', 'author'])
|
||||||
|
|
||||||
entries = feed.getElementsByTagName('entry')
|
entries = feed.getElementsByTagName('entry')
|
||||||
self.assertEqual(len(entries), Entry.objects.count())
|
self.assertEqual(len(entries), Entry.objects.count())
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
self.assertEqual(entry.getAttribute('bacon'), 'yum')
|
self.assertEqual(entry.getAttribute('bacon'), 'yum')
|
||||||
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry'])
|
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry', 'rights', 'author', 'updated', 'category'])
|
||||||
summary = entry.getElementsByTagName('summary')[0]
|
summary = entry.getElementsByTagName('summary')[0]
|
||||||
self.assertEqual(summary.getAttribute('type'), 'html')
|
self.assertEqual(summary.getAttribute('type'), 'html')
|
||||||
|
|
||||||
def test_complex_base_url(self):
|
|
||||||
"""
|
|
||||||
Tests that that the base url for a complex feed doesn't raise a 500
|
|
||||||
exception.
|
|
||||||
"""
|
|
||||||
response = self.client.get('/syndication/feeds/complex/')
|
|
||||||
self.assertEquals(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_title_escaping(self):
|
def test_title_escaping(self):
|
||||||
"""
|
"""
|
||||||
Tests that titles are escaped correctly in RSS feeds.
|
Tests that titles are escaped correctly in RSS feeds.
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/syndication/feeds/rss/')
|
response = self.client.get('/syndication/rss2/')
|
||||||
doc = minidom.parseString(response.content)
|
doc = minidom.parseString(response.content)
|
||||||
for item in doc.getElementsByTagName('item'):
|
for item in doc.getElementsByTagName('item'):
|
||||||
link = item.getElementsByTagName('link')[0]
|
link = item.getElementsByTagName('link')[0]
|
||||||
|
@ -101,7 +195,7 @@ class SyndicationFeedTest(TestCase):
|
||||||
"""
|
"""
|
||||||
# Naive date times passed in get converted to the local time zone, so
|
# Naive date times passed in get converted to the local time zone, so
|
||||||
# check the recived zone offset against the local offset.
|
# check the recived zone offset against the local offset.
|
||||||
response = self.client.get('/syndication/feeds/naive-dates/')
|
response = self.client.get('/syndication/naive-dates/')
|
||||||
doc = minidom.parseString(response.content)
|
doc = minidom.parseString(response.content)
|
||||||
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
|
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
|
||||||
tz = tzinfo.LocalTimezone(datetime.datetime.now())
|
tz = tzinfo.LocalTimezone(datetime.datetime.now())
|
||||||
|
@ -112,8 +206,113 @@ class SyndicationFeedTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Test that datetimes with timezones don't get trodden on.
|
Test that datetimes with timezones don't get trodden on.
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/syndication/feeds/aware-dates/')
|
response = self.client.get('/syndication/aware-dates/')
|
||||||
doc = minidom.parseString(response.content)
|
doc = minidom.parseString(response.content)
|
||||||
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
|
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
|
||||||
self.assertEqual(updated[-6:], '+00:42')
|
self.assertEqual(updated[-6:], '+00:42')
|
||||||
|
|
||||||
|
def test_feed_url(self):
|
||||||
|
"""
|
||||||
|
Test that the feed_url can be overridden.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/feedurl/')
|
||||||
|
doc = minidom.parseString(response.content)
|
||||||
|
for link in doc.getElementsByTagName('link'):
|
||||||
|
if link.getAttribute('rel') == 'self':
|
||||||
|
self.assertEqual(link.getAttribute('href'), 'http://example.com/customfeedurl/')
|
||||||
|
|
||||||
|
def test_item_link_error(self):
|
||||||
|
"""
|
||||||
|
Test that a ImproperlyConfigured is raised if no link could be found
|
||||||
|
for the item(s).
|
||||||
|
"""
|
||||||
|
self.assertRaises(ImproperlyConfigured,
|
||||||
|
self.client.get,
|
||||||
|
'/syndication/articles/')
|
||||||
|
|
||||||
|
def test_template_feed(self):
|
||||||
|
"""
|
||||||
|
Test that the item title and description can be overridden with
|
||||||
|
templates.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/template/')
|
||||||
|
doc = minidom.parseString(response.content)
|
||||||
|
feed = doc.getElementsByTagName('rss')[0]
|
||||||
|
chan = feed.getElementsByTagName('channel')[0]
|
||||||
|
items = chan.getElementsByTagName('item')
|
||||||
|
|
||||||
|
self.assertChildNodeContent(items[0], {
|
||||||
|
'title': 'Title in your templates: My first entry',
|
||||||
|
'description': 'Description in your templates: My first entry',
|
||||||
|
'link': 'http://example.com/blog/1/',
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_add_domain(self):
|
||||||
|
"""
|
||||||
|
Test add_domain() prefixes domains onto the correct URLs.
|
||||||
|
"""
|
||||||
|
self.assertEqual(
|
||||||
|
views.add_domain('example.com', '/foo/?arg=value'),
|
||||||
|
'http://example.com/foo/?arg=value'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
views.add_domain('example.com', 'http://djangoproject.com/doc/'),
|
||||||
|
'http://djangoproject.com/doc/'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
views.add_domain('example.com', 'https://djangoproject.com/doc/'),
|
||||||
|
'https://djangoproject.com/doc/'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
views.add_domain('example.com', 'mailto:uhoh@djangoproject.com'),
|
||||||
|
'mailto:uhoh@djangoproject.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Deprecated feeds
|
||||||
|
######################################
|
||||||
|
|
||||||
|
class DeprecatedSyndicationFeedTest(FeedTestCase):
|
||||||
|
"""
|
||||||
|
Tests for the deprecated API (feed() view and the feed_dict etc).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_empty_feed_dict(self):
|
||||||
|
"""
|
||||||
|
Test that an empty feed_dict raises a 404.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/depr-feeds-empty/aware-dates/')
|
||||||
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_nonexistent_slug(self):
|
||||||
|
"""
|
||||||
|
Test that a non-existent slug raises a 404.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/depr-feeds/foobar/')
|
||||||
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_rss_feed(self):
|
||||||
|
"""
|
||||||
|
A simple test for Rss201rev2Feed feeds generated by the deprecated
|
||||||
|
system.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/depr-feeds/rss/')
|
||||||
|
doc = minidom.parseString(response.content)
|
||||||
|
feed = doc.getElementsByTagName('rss')[0]
|
||||||
|
self.assertEqual(feed.getAttribute('version'), '2.0')
|
||||||
|
|
||||||
|
chan = feed.getElementsByTagName('channel')[0]
|
||||||
|
self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link'])
|
||||||
|
|
||||||
|
items = chan.getElementsByTagName('item')
|
||||||
|
self.assertEqual(len(items), Entry.objects.count())
|
||||||
|
|
||||||
|
def test_complex_base_url(self):
|
||||||
|
"""
|
||||||
|
Tests that the base url for a complex feed doesn't raise a 500
|
||||||
|
exception.
|
||||||
|
"""
|
||||||
|
response = self.client.get('/syndication/depr-feeds/complex/')
|
||||||
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
import feeds
|
import feeds
|
||||||
from django.conf.urls.defaults import patterns
|
|
||||||
|
|
||||||
feed_dict = {
|
feed_dict = {
|
||||||
'complex': feeds.ComplexFeed,
|
'complex': feeds.DeprecatedComplexFeed,
|
||||||
'rss': feeds.TestRssFeed,
|
'rss': feeds.DeprecatedRssFeed,
|
||||||
'atom': feeds.TestAtomFeed,
|
|
||||||
'custom': feeds.TestCustomFeed,
|
|
||||||
'naive-dates': feeds.NaiveDatesFeed,
|
|
||||||
'aware-dates': feeds.TZAwareDatesFeed,
|
|
||||||
}
|
}
|
||||||
urlpatterns = patterns('',
|
|
||||||
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feed_dict})
|
urlpatterns = patterns('django.contrib.syndication.views',
|
||||||
|
(r'^complex/(?P<foo>.*)/$', feeds.ComplexFeed()),
|
||||||
|
(r'^rss2/$', feeds.TestRss2Feed()),
|
||||||
|
(r'^rss091/$', feeds.TestRss091Feed()),
|
||||||
|
(r'^atom/$', feeds.TestAtomFeed()),
|
||||||
|
(r'^custom/$', feeds.TestCustomFeed()),
|
||||||
|
(r'^naive-dates/$', feeds.NaiveDatesFeed()),
|
||||||
|
(r'^aware-dates/$', feeds.TZAwareDatesFeed()),
|
||||||
|
(r'^feedurl/$', feeds.TestFeedUrlFeed()),
|
||||||
|
(r'^articles/$', feeds.ArticlesFeed()),
|
||||||
|
(r'^template/$', feeds.TemplateFeed()),
|
||||||
|
|
||||||
|
(r'^depr-feeds/(?P<url>.*)/$', 'feed', {'feed_dict': feed_dict}),
|
||||||
|
(r'^depr-feeds-empty/(?P<url>.*)/$', 'feed', {'feed_dict': None}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import datetime
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from django.utils import feedgenerator, tzinfo
|
||||||
|
|
||||||
|
class FeedgeneratorTest(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for the low-level syndication feed framework.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_get_tag_uri(self):
|
||||||
|
"""
|
||||||
|
Test get_tag_uri() correctly generates TagURIs.
|
||||||
|
"""
|
||||||
|
self.assertEqual(
|
||||||
|
feedgenerator.get_tag_uri('http://example.org/foo/bar#headline', datetime.date(2004, 10, 25)),
|
||||||
|
u'tag:example.org,2004-10-25:/foo/bar/headline')
|
||||||
|
|
||||||
|
def test_get_tag_uri_with_port(self):
|
||||||
|
"""
|
||||||
|
Test that get_tag_uri() correctly generates TagURIs from URLs with port
|
||||||
|
numbers.
|
||||||
|
"""
|
||||||
|
self.assertEqual(
|
||||||
|
feedgenerator.get_tag_uri('http://www.example.org:8000/2008/11/14/django#headline', datetime.datetime(2008, 11, 14, 13, 37, 0)),
|
||||||
|
u'tag:www.example.org,2008-11-14:/2008/11/14/django/headline')
|
||||||
|
|
||||||
|
def test_rfc2822_date(self):
|
||||||
|
"""
|
||||||
|
Test rfc2822_date() correctly formats datetime objects.
|
||||||
|
"""
|
||||||
|
self.assertEqual(
|
||||||
|
feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0)),
|
||||||
|
"Fri, 14 Nov 2008 13:37:00 -0000"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rfc2822_date_with_timezone(self):
|
||||||
|
"""
|
||||||
|
Test rfc2822_date() correctly formats datetime objects with tzinfo.
|
||||||
|
"""
|
||||||
|
self.assertEqual(
|
||||||
|
feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=60)))),
|
||||||
|
"Fri, 14 Nov 2008 13:37:00 +0100"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rfc3339_date(self):
|
||||||
|
"""
|
||||||
|
Test rfc3339_date() correctly formats datetime objects.
|
||||||
|
"""
|
||||||
|
self.assertEqual(
|
||||||
|
feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0)),
|
||||||
|
"2008-11-14T13:37:00Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rfc3339_date_with_timezone(self):
|
||||||
|
"""
|
||||||
|
Test rfc3339_date() correctly formats datetime objects with tzinfo.
|
||||||
|
"""
|
||||||
|
self.assertEqual(
|
||||||
|
feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=120)))),
|
||||||
|
"2008-11-14T13:37:00+02:00"
|
||||||
|
)
|
||||||
|
|
|
@ -31,6 +31,7 @@ __test__ = {
|
||||||
}
|
}
|
||||||
|
|
||||||
from dateformat import *
|
from dateformat import *
|
||||||
|
from feedgenerator import *
|
||||||
from termcolors import *
|
from termcolors import *
|
||||||
|
|
||||||
class TestUtilsHtml(TestCase):
|
class TestUtilsHtml(TestCase):
|
||||||
|
|
Loading…
Reference in New Issue