Ticket #12403: syndication-view-6.diff
File syndication-view-6.diff, 75.6 KB (added by , 15 years ago) |
---|
-
AUTHORS
diff --git a/AUTHORS b/AUTHORS index 56ab83d..ae6a090 100644
a b answer newbie questions, and generally made Django that much better: 166 166 Afonso Fernández Nogueira <fonzzo.django@gmail.com> 167 167 J. Pablo Fernandez <pupeno@pupeno.com> 168 168 Maciej Fijalkowski 169 Ben Firshman <ben@firshman.co.uk> 169 170 Matthew Flanagan <http://wadofstuff.blogspot.com> 170 171 Eric Floehr <eric@intellovations.com> 171 172 Eric Florenzano <floguy@gmail.com> -
django/contrib/comments/feeds.py
diff --git a/django/contrib/comments/feeds.py b/django/contrib/comments/feeds.py index 24b10d4..d4269e9 100644
a b 1 1 from django.conf import settings 2 from django.contrib.syndication. feeds import Feed2 from django.contrib.syndication.views import Feed 3 3 from django.contrib.sites.models import Site 4 4 from django.contrib import comments 5 5 from django.utils.translation import ugettext as _ -
django/contrib/syndication/feeds.py
diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py index e5e0877..7a99660 100644
a b 1 from datetime import datetime, timedelta 1 from django.contrib.syndication import views 2 from django.core.exceptions import ObjectDoesNotExist 3 import warnings 2 4 3 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist 4 from django.template import loader, Template, TemplateDoesNotExist 5 from django.contrib.sites.models import Site, RequestSite 6 from django.utils import feedgenerator 7 from django.utils.tzinfo import FixedOffset 8 from django.utils.encoding import smart_unicode, iri_to_uri 9 from django.conf import settings 10 from django.template import RequestContext 11 12 def add_domain(domain, url): 13 if not (url.startswith('http://') or url.startswith('https://')): 14 # 'url' must already be ASCII and URL-quoted, so no need for encoding 15 # conversions here. 16 url = iri_to_uri(u'http://%s%s' % (domain, url)) 17 return url 18 19 class FeedDoesNotExist(ObjectDoesNotExist): 20 pass 21 22 class Feed(object): 23 item_pubdate = None 24 item_enclosure_url = None 25 feed_type = feedgenerator.DefaultFeed 26 feed_url = None 27 title_template = None 28 description_template = None 5 # This is part of the deprecated API 6 from django.contrib.syndication.views import FeedDoesNotExist 29 7 8 class Feed(views.Feed): 9 """Provided for backwards compatibility.""" 30 10 def __init__(self, slug, request): 11 warnings.warn('The syndication feeds.Feed class is deprecated. Please ' 12 'use the new class based view API.', 13 category=PendingDeprecationWarning) 14 31 15 self.slug = slug 32 16 self.request = request 33 self.feed_url = self.feed_url or request.path 34 self.title_template_name = self.title_template or ('feeds/%s_title.html' % slug) 35 self.description_template_name = self.description_template or ('feeds/%s_description.html' % slug) 36 37 def item_link(self, item): 38 try: 39 return item.get_absolute_url() 40 except AttributeError: 41 raise ImproperlyConfigured("Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__) 42 43 def __get_dynamic_attr(self, attname, obj, default=None): 44 try: 45 attr = getattr(self, attname) 46 except AttributeError: 47 return default 48 if callable(attr): 49 # Check func_code.co_argcount rather than try/excepting the 50 # function and catching the TypeError, because something inside 51 # the function may raise the TypeError. This technique is more 52 # accurate. 53 if hasattr(attr, 'func_code'): 54 argcount = attr.func_code.co_argcount 55 else: 56 argcount = attr.__call__.func_code.co_argcount 57 if argcount == 2: # one argument is 'self' 58 return attr(obj) 59 else: 60 return attr() 61 return attr 62 63 def feed_extra_kwargs(self, obj): 64 """ 65 Returns an extra keyword arguments dictionary that is used when 66 initializing the feed generator. 67 """ 68 return {} 69 70 def item_extra_kwargs(self, item): 71 """ 72 Returns an extra keyword arguments dictionary that is used with 73 the `add_item` call of the feed generator. 74 """ 75 return {} 76 17 self.feed_url = getattr(self, 'feed_url', None) or request.path 18 self.title_template = self.title_template or ('feeds/%s_title.html' % slug) 19 self.description_template = self.description_template or ('feeds/%s_description.html' % slug) 20 77 21 def get_object(self, bits): 78 22 return None 79 23 80 24 def get_feed(self, url=None): 81 25 """ 82 26 Returns a feedgenerator.DefaultFeed object, fully populated, for … … class Feed(object): 86 30 bits = url.split('/') 87 31 else: 88 32 bits = [] 89 90 33 try: 91 34 obj = self.get_object(bits) 92 35 except ObjectDoesNotExist: 93 36 raise FeedDoesNotExist 37 return super(Feed, self).get_feed(obj, self.request) 94 38 95 if Site._meta.installed:96 current_site = Site.objects.get_current()97 else:98 current_site = RequestSite(self.request)99 100 link = self.__get_dynamic_attr('link', obj)101 link = add_domain(current_site.domain, link)102 103 feed = self.feed_type(104 title = self.__get_dynamic_attr('title', obj),105 subtitle = self.__get_dynamic_attr('subtitle', obj),106 link = link,107 description = self.__get_dynamic_attr('description', obj),108 language = settings.LANGUAGE_CODE.decode(),109 feed_url = add_domain(current_site.domain,110 self.__get_dynamic_attr('feed_url', obj)),111 author_name = self.__get_dynamic_attr('author_name', obj),112 author_link = self.__get_dynamic_attr('author_link', obj),113 author_email = self.__get_dynamic_attr('author_email', obj),114 categories = self.__get_dynamic_attr('categories', obj),115 feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),116 feed_guid = self.__get_dynamic_attr('feed_guid', obj),117 ttl = self.__get_dynamic_attr('ttl', obj),118 **self.feed_extra_kwargs(obj)119 )120 121 try:122 title_tmp = loader.get_template(self.title_template_name)123 except TemplateDoesNotExist:124 title_tmp = Template('{{ obj }}')125 try:126 description_tmp = loader.get_template(self.description_template_name)127 except TemplateDoesNotExist:128 description_tmp = Template('{{ obj }}')129 130 for item in self.__get_dynamic_attr('items', obj):131 link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item))132 enc = None133 enc_url = self.__get_dynamic_attr('item_enclosure_url', item)134 if enc_url:135 enc = feedgenerator.Enclosure(136 url = smart_unicode(enc_url),137 length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),138 mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))139 )140 author_name = self.__get_dynamic_attr('item_author_name', item)141 if author_name is not None:142 author_email = self.__get_dynamic_attr('item_author_email', item)143 author_link = self.__get_dynamic_attr('item_author_link', item)144 else:145 author_email = author_link = None146 147 pubdate = self.__get_dynamic_attr('item_pubdate', item)148 if pubdate and not pubdate.tzinfo:149 now = datetime.now()150 utcnow = datetime.utcnow()151 152 # Must always subtract smaller time from larger time here.153 if utcnow > now:154 sign = -1155 tzDifference = (utcnow - now)156 else:157 sign = 1158 tzDifference = (now - utcnow)159 160 # Round the timezone offset to the nearest half hour.161 tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30162 tzOffset = timedelta(minutes=tzOffsetMinutes)163 pubdate = pubdate.replace(tzinfo=FixedOffset(tzOffset))164 165 feed.add_item(166 title = title_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),167 link = link,168 description = description_tmp.render(RequestContext(self.request, {'obj': item, 'site': current_site})),169 unique_id = self.__get_dynamic_attr('item_guid', item, link),170 enclosure = enc,171 pubdate = pubdate,172 author_name = author_name,173 author_email = author_email,174 author_link = author_link,175 categories = self.__get_dynamic_attr('item_categories', item),176 item_copyright = self.__get_dynamic_attr('item_copyright', item),177 **self.item_extra_kwargs(item)178 )179 return feed -
django/contrib/syndication/views.py
diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index d422036..55c435d 100644
a b 1 from django.contrib.syndication import feeds 1 import datetime 2 from django.conf import settings 3 from django.contrib.sites.models import Site, RequestSite 4 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist 2 5 from django.http import HttpResponse, Http404 6 from django.template import loader, Template, TemplateDoesNotExist, RequestContext 7 from django.utils import feedgenerator, tzinfo 8 from django.utils.encoding import force_unicode, iri_to_uri, smart_unicode 9 from django.utils.html import escape 10 11 def add_domain(domain, url): 12 if not (url.startswith('http://') 13 or url.startswith('https://') 14 or url.startswith('mailto:')): 15 # 'url' must already be ASCII and URL-quoted, so no need for encoding 16 # conversions here. 17 url = iri_to_uri(u'http://%s%s' % (domain, url)) 18 return url 19 20 class FeedDoesNotExist(ObjectDoesNotExist): 21 pass 22 23 24 class Feed(object): 25 feed_type = feedgenerator.DefaultFeed 26 title_template = None 27 description_template = None 28 29 def __call__(self, request, *args, **kwargs): 30 try: 31 obj = self.get_object(request, *args, **kwargs) 32 except ObjectDoesNotExist: 33 raise Http404('Feed object does not exist.') 34 feedgen = self.get_feed(obj, request) 35 response = HttpResponse(mimetype=feedgen.mime_type) 36 feedgen.write(response, 'utf-8') 37 return response 38 39 def item_title(self, item): 40 # Titles should be double escaped by default (see #6533) 41 return escape(force_unicode(item)) 42 43 def item_description(self, item): 44 return force_unicode(item) 45 46 def item_link(self, item): 47 try: 48 return item.get_absolute_url() 49 except AttributeError: 50 raise ImproperlyConfigured('Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class.' % item.__class__.__name__) 51 52 def __get_dynamic_attr(self, attname, obj, default=None): 53 try: 54 attr = getattr(self, attname) 55 except AttributeError: 56 return default 57 if callable(attr): 58 # Check func_code.co_argcount rather than try/excepting the 59 # function and catching the TypeError, because something inside 60 # the function may raise the TypeError. This technique is more 61 # accurate. 62 if hasattr(attr, 'func_code'): 63 argcount = attr.func_code.co_argcount 64 else: 65 argcount = attr.__call__.func_code.co_argcount 66 if argcount == 2: # one argument is 'self' 67 return attr(obj) 68 else: 69 return attr() 70 return attr 71 72 def feed_extra_kwargs(self, obj): 73 """ 74 Returns an extra keyword arguments dictionary that is used when 75 initializing the feed generator. 76 """ 77 return {} 78 79 def item_extra_kwargs(self, item): 80 """ 81 Returns an extra keyword arguments dictionary that is used with 82 the `add_item` call of the feed generator. 83 """ 84 return {} 85 86 def get_object(self, request, *args, **kwargs): 87 return None 88 89 def get_feed(self, obj, request): 90 """ 91 Returns a feedgenerator.DefaultFeed object, fully populated, for 92 this feed. Raises FeedDoesNotExist for invalid parameters. 93 """ 94 if Site._meta.installed: 95 current_site = Site.objects.get_current() 96 else: 97 current_site = RequestSite(request) 98 99 link = self.__get_dynamic_attr('link', obj) 100 link = add_domain(current_site.domain, link) 101 102 feed = self.feed_type( 103 title = self.__get_dynamic_attr('title', obj), 104 subtitle = self.__get_dynamic_attr('subtitle', obj), 105 link = link, 106 description = self.__get_dynamic_attr('description', obj), 107 language = settings.LANGUAGE_CODE.decode(), 108 feed_url = add_domain(current_site.domain, 109 self.__get_dynamic_attr('feed_url', obj) or request.path), 110 author_name = self.__get_dynamic_attr('author_name', obj), 111 author_link = self.__get_dynamic_attr('author_link', obj), 112 author_email = self.__get_dynamic_attr('author_email', obj), 113 categories = self.__get_dynamic_attr('categories', obj), 114 feed_copyright = self.__get_dynamic_attr('feed_copyright', obj), 115 feed_guid = self.__get_dynamic_attr('feed_guid', obj), 116 ttl = self.__get_dynamic_attr('ttl', obj), 117 **self.feed_extra_kwargs(obj) 118 ) 119 120 title_tmp = None 121 if self.title_template is not None: 122 try: 123 title_tmp = loader.get_template(self.title_template) 124 except TemplateDoesNotExist: 125 pass 126 127 description_tmp = None 128 if self.description_template is not None: 129 try: 130 description_tmp = loader.get_template(self.description_template) 131 except TemplateDoesNotExist: 132 pass 133 134 for item in self.__get_dynamic_attr('items', obj): 135 if title_tmp is not None: 136 title = title_tmp.render(RequestContext(request, {'obj': item, 'site': current_site})) 137 else: 138 title = self.__get_dynamic_attr('item_title', item) 139 if description_tmp is not None: 140 description = description_tmp.render(RequestContext(request, {'obj': item, 'site': current_site})) 141 else: 142 description = self.__get_dynamic_attr('item_description', item) 143 link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item)) 144 enc = None 145 enc_url = self.__get_dynamic_attr('item_enclosure_url', item) 146 if enc_url: 147 enc = feedgenerator.Enclosure( 148 url = smart_unicode(enc_url), 149 length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)), 150 mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item)) 151 ) 152 author_name = self.__get_dynamic_attr('item_author_name', item) 153 if author_name is not None: 154 author_email = self.__get_dynamic_attr('item_author_email', item) 155 author_link = self.__get_dynamic_attr('item_author_link', item) 156 else: 157 author_email = author_link = None 158 159 pubdate = self.__get_dynamic_attr('item_pubdate', item) 160 if pubdate and not pubdate.tzinfo: 161 now = datetime.datetime.now() 162 utcnow = datetime.datetime.utcnow() 163 164 # Must always subtract smaller time from larger time here. 165 if utcnow > now: 166 sign = -1 167 tzDifference = (utcnow - now) 168 else: 169 sign = 1 170 tzDifference = (now - utcnow) 171 172 # Round the timezone offset to the nearest half hour. 173 tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30 174 tzOffset = datetime.timedelta(minutes=tzOffsetMinutes) 175 pubdate = pubdate.replace(tzinfo=tzinfo.FixedOffset(tzOffset)) 176 177 feed.add_item( 178 title = title, 179 link = link, 180 description = description, 181 unique_id = self.__get_dynamic_attr('item_guid', item, link), 182 enclosure = enc, 183 pubdate = pubdate, 184 author_name = author_name, 185 author_email = author_email, 186 author_link = author_link, 187 categories = self.__get_dynamic_attr('item_categories', item), 188 item_copyright = self.__get_dynamic_attr('item_copyright', item), 189 **self.item_extra_kwargs(item) 190 ) 191 return feed 192 3 193 4 194 def feed(request, url, feed_dict=None): 195 """Provided for backwards compatibility.""" 196 import warnings 197 warnings.warn('The syndication feed() view is deprecated. Please use the ' 198 'new class based view API.', 199 category=PendingDeprecationWarning) 200 5 201 if not feed_dict: 6 202 raise Http404("No feeds are registered.") 7 203 8 204 try: 9 205 slug, param = url.split('/', 1) 10 206 except ValueError: 11 207 slug, param = url, '' 12 208 13 209 try: 14 210 f = feed_dict[slug] 15 211 except KeyError: 16 212 raise Http404("Slug %r isn't registered." % slug) 17 213 18 214 try: 19 215 feedgen = f(slug, request).get_feed(param) 20 except feeds.FeedDoesNotExist:216 except FeedDoesNotExist: 21 217 raise Http404("Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug) 22 218 23 219 response = HttpResponse(mimetype=feedgen.mime_type) 24 220 feedgen.write(response, 'utf-8') 25 221 return response 222 -
django/utils/feedgenerator.py
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index c9445f9..bb74a8e 100644
a b For definitions of the different versions of RSS, see: 19 19 http://diveintomark.org/archives/2004/02/04/incompatible-rss 20 20 """ 21 21 22 import re23 22 import datetime 23 import urlparse 24 24 from django.utils.xmlutils import SimplerXMLGenerator 25 25 from django.utils.encoding import force_unicode, iri_to_uri 26 26 … … def rfc3339_date(date): 46 46 return date.strftime('%Y-%m-%dT%H:%M:%SZ') 47 47 48 48 def get_tag_uri(url, date): 49 "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id" 50 tag = re.sub('^http://', '', url) 49 """ 50 Creates a TagURI. 51 52 See http://diveintomark.org/archives/2004/05/28/howto-atom-id 53 """ 54 url_split = urlparse.urlparse(url) 55 d = '' 51 56 if date is not None: 52 tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1) 53 tag = re.sub('#', '/', tag) 54 return u'tag:' + tag 57 d = ',%s' % date.strftime('%Y-%m-%d') 58 return u'tag:%s%s:%s/%s' % (url_split.hostname, d, url_split.path, url_split.fragment) 55 59 56 60 class SyndicationFeed(object): 57 61 "Base class for all syndication feeds. Subclasses should provide write()" … … class SyndicationFeed(object): 61 65 to_unicode = lambda s: force_unicode(s, strings_only=True) 62 66 if categories: 63 67 categories = [force_unicode(c) for c in categories] 68 if ttl is not None: 69 # Force ints to unicode 70 ttl = force_unicode(ttl) 64 71 self.feed = { 65 72 'title': to_unicode(title), 66 73 'link': iri_to_uri(link), … … class SyndicationFeed(object): 91 98 to_unicode = lambda s: force_unicode(s, strings_only=True) 92 99 if categories: 93 100 categories = [to_unicode(c) for c in categories] 101 if ttl is not None: 102 # Force ints to unicode 103 ttl = force_unicode(ttl) 94 104 item = { 95 105 'title': to_unicode(title), 96 106 'link': iri_to_uri(link), … … class RssFeed(SyndicationFeed): 186 196 handler.endElement(u"rss") 187 197 188 198 def rss_attributes(self): 189 return {u"version": self._version} 199 return {u"version": self._version, 200 u"xmlns:atom": u"http://www.w3.org/2005/Atom"} 190 201 191 202 def write_items(self, handler): 192 203 for item in self.items: … … class RssFeed(SyndicationFeed): 198 209 handler.addQuickElement(u"title", self.feed['title']) 199 210 handler.addQuickElement(u"link", self.feed['link']) 200 211 handler.addQuickElement(u"description", self.feed['description']) 212 handler.addQuickElement(u"atom:link", None, {u"rel": u"self", u"href": self.feed['feed_url']}) 201 213 if self.feed['language'] is not None: 202 214 handler.addQuickElement(u"language", self.feed['language']) 203 215 for cat in self.feed['categories']: … … class Rss201rev2Feed(RssFeed): 235 247 elif item["author_email"]: 236 248 handler.addQuickElement(u"author", item["author_email"]) 237 249 elif item["author_name"]: 238 handler.addQuickElement(u"dc:creator", item["author_name"], { "xmlns:dc": u"http://purl.org/dc/elements/1.1/"})250 handler.addQuickElement(u"dc:creator", item["author_name"], {u"xmlns:dc": u"http://purl.org/dc/elements/1.1/"}) 239 251 240 252 if item['pubdate'] is not None: 241 253 handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8')) -
docs/internals/deprecation.txt
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index e2d4b6c..3b03ecc 100644
a b their deprecation, as per the :ref:`Django deprecation policy 77 77 * The ability to use a function-based test runners will be removed, 78 78 along with the ``django.test.simple.run_tests()`` test runner. 79 79 80 * The ``views.feed()`` view and ``feeds.Feed`` class in 81 ``django.contrib.syndication`` have been deprecated since the 1.2 82 release. The class-based view ``views.Feed`` should be used instead. 83 80 84 * 2.0 81 85 * ``django.views.defaults.shortcut()``. This function has been moved 82 86 to ``django.contrib.contenttypes.views.shortcut()`` as part of the -
docs/ref/contrib/syndication.txt
diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index c276663..9b97ed1 100644
a b to generate feeds outside of a Web context, or in some other lower-level way. 23 23 The high-level framework 24 24 ======================== 25 25 26 .. versionchanged:: 1.2 27 26 28 Overview 27 29 -------- 28 30 29 The high-level feed-generating framework is a view that's hooked to ``/feeds/`` 30 by default. Django uses the remainder of the URL (everything after ``/feeds/``) 31 to determine which feed to output. 32 33 To create a feed, just write a :class:`~django.contrib.syndication.feeds.Feed` 34 class and point to it in your :ref:`URLconf <topics-http-urls>`. 35 36 Initialization 37 -------------- 38 39 To activate syndication feeds on your Django site, add this line to your 40 :ref:`URLconf <topics-http-urls>`:: 41 42 (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}), 43 44 This tells Django to use the RSS framework to handle all URLs starting with 45 :file:`"feeds/"`. (You can change that :file:`"feeds/"` prefix to fit your own 46 needs.) 47 48 This URLconf line has an extra argument: ``{'feed_dict': feeds}``. Use this 49 extra argument to pass the syndication framework the feeds that should be 50 published under that URL. 51 52 Specifically, :data:`feed_dict` should be a dictionary that maps a feed's slug 53 (short URL label) to its :class:`~django.contrib.syndication.feeds.Feed` class. 54 55 You can define the ``feed_dict`` in the URLconf itself. Here's a full example 56 URLconf:: 57 58 from django.conf.urls.defaults import * 59 from myproject.feeds import LatestEntries, LatestEntriesByCategory 60 61 feeds = { 62 'latest': LatestEntries, 63 'categories': LatestEntriesByCategory, 64 } 65 66 urlpatterns = patterns('', 67 # ... 68 (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', 69 {'feed_dict': feeds}), 70 # ... 71 ) 72 73 The above example registers two feeds: 74 75 * The feed represented by ``LatestEntries`` will live at ``feeds/latest/``. 76 * The feed represented by ``LatestEntriesByCategory`` will live at 77 ``feeds/categories/``. 78 79 Once that's set up, you just need to define the 80 :class:`~django.contrib.syndication.feeds.Feed` classes themselves. 31 The high-level feed-generating framework is supplied by the 32 :class:`~django.contrib.syndication.views.Feed` class. To create a feed, just write a :class:`~django.contrib.syndication.views.Feed` 33 class and point to an instance of it in your :ref:`URLconf <topics-http-urls>`. 81 34 82 35 Feed classes 83 36 ------------ 84 37 85 A :class:`~django.contrib.syndication. feeds.Feed` class is a simplePython class38 A :class:`~django.contrib.syndication.views.Feed` class is a Python class 86 39 that represents a syndication feed. A feed can be simple (e.g., a "site news" 87 40 feed, or a basic feed displaying the latest entries of a blog) or more complex 88 41 (e.g., a feed displaying all the blog entries in a particular category, where 89 42 the category is variable). 90 43 91 :class:`~django.contrib.syndication. feeds.Feed` classes mustsubclass92 ``django.contrib.syndication. feeds.Feed``. They can live anywhere in your44 :class:`~django.contrib.syndication.views.Feed` classes subclass 45 ``django.contrib.syndication.views.Feed``. They can live anywhere in your 93 46 codebase. 94 47 48 Instances of :class:`~django.contrib.syndication.views.Feed` classes are views 49 which can be used in your :ref:`URLconf <topics-http-urls>`. 50 95 51 A simple example 96 52 ---------------- 97 53 98 54 This simple example, taken from `chicagocrime.org`_, describes a feed of the 99 55 latest five news items:: 100 56 101 from django.contrib.syndication. feeds import Feed57 from django.contrib.syndication.views import Feed 102 58 from chicagocrime.models import NewsItem 103 59 104 class LatestEntries (Feed):60 class LatestEntriesFeed(Feed): 105 61 title = "Chicagocrime.org site news" 106 62 link = "/sitenews/" 107 63 description = "Updates on changes and additions to chicagocrime.org." 108 64 109 65 def items(self): 110 66 return NewsItem.objects.order_by('-pub_date')[:5] 67 68 def item_title(self, item): 69 return item.title 70 71 def item_description(self, item): 72 return item.description 73 74 To connect a URL to this feed, it needs to be put in your :ref:`URLconf <topics-http-urls>`. Here is a full example:: 75 76 from django.conf.urls.defaults import * 77 from myproject.feeds import LatestEntriesFeed 78 79 urlpatterns = patterns('', 80 # ... 81 (r'^latest/feed/$', LatestEntriesFeed()), 82 # ... 83 ) 111 84 112 85 Note: 113 86 114 * The class subclasses ``django.contrib.syndication. feeds.Feed``.87 * The class subclasses ``django.contrib.syndication.views.Feed``. 115 88 116 89 * :attr:`title`, :attr:`link` and :attr:`description` correspond to the 117 90 standard RSS ``<title>``, ``<link>`` and ``<description>`` elements, … … One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``, 133 106 ``<link>`` and ``<description>``. We need to tell the framework what data to put 134 107 into those elements. 135 108 136 * To specify the contents of ``<title>`` and ``<description>``, create 137 :ref:`Django templates <topics-templates>` called 138 :file:`feeds/latest_title.html` and 139 :file:`feeds/latest_description.html`, where :attr:`latest` is the 140 :attr:`slug` specified in the URLconf for the given feed. Note the 141 ``.html`` extension is required. The RSS system renders that template for 142 each item, passing it two template context variables: 109 * For the contents of ``<title>`` and ``<description>``, Django tries 110 calling the methods :meth:`item_title()` and :meth:`item_description()` on 111 the :class:`~django.contrib.syndication.views.Feed` class. They are passed 112 a single parameter, :attr:`item`, which is the object itself. These are 113 optional; by default, the unicode representation of the object is used for 114 both. 115 116 If you want to do any special formatting for either the title or 117 description, :ref:`Django templates <topics-templates>` can be used 118 instead. Their paths can be specified with the ``title_template`` and 119 ``description_template`` attributes on the 120 :class:`~django.contrib.syndication.views.Feed` class. The templates are 121 rendered for each item and are passed two template context variables: 143 122 144 123 * ``{{ obj }}`` -- The current object (one of whichever objects you 145 124 returned in :meth:`items()`). … … into those elements. 152 131 :ref:`RequestSite section of the sites framework documentation 153 132 <requestsite-objects>` for more. 154 133 155 If you don't create a template for either the title or description, the 156 framework will use the template ``"{{ obj }}"`` by default -- that is, the 157 normal string representation of the object. You can also change the names 158 of these two templates by specifying ``title_template`` and 159 ``description_template`` as attributes of your 160 :class:`~django.contrib.syndication.feeds.Feed` class. 134 See `a complex example`_ below that uses a description template. 161 135 162 136 * To specify the contents of ``<link>``, you have two options. For each item 163 in :meth:`items()`, Django first tries calling a method 164 :meth:`item_link()` in the :class:`~django.contrib.syndication.feeds.Feed` 165 class, passing it a single parameter, :attr:`item`, which is the object 166 itself. If that method doesn't exist, Django tries executing a 167 ``get_absolute_url()`` method on that object. . Both 168 ``get_absolute_url()`` and :meth:`item_link()` should return the item's 169 URL as a normal Python string. As with ``get_absolute_url()``, the result 170 of :meth:`item_link()` will be included directly in the URL, so you are 171 responsible for doing all necessary URL quoting and conversion to ASCII 172 inside the method itself. 173 174 * For the LatestEntries example above, we could have very simple feed 175 templates: 176 177 * latest_title.html: 178 179 .. code-block:: html+django 180 181 {{ obj.title }} 182 183 * latest_description.html: 184 185 .. code-block:: html+django 186 187 {{ obj.description }} 137 in :meth:`items()`, Django first tries calling the 138 :meth:`item_link()` method on the 139 :class:`~django.contrib.syndication.views.Feed` class. In a similar way to 140 the title and description, it is passed it a single parameter, 141 :attr:`item`. If that method doesn't exist, Django tries executing a 142 ``get_absolute_url()`` method on that object. Both 143 :meth:`get_absolute_url()` and :meth:`item_link()` should return the 144 item's URL as a normal Python string. As with ``get_absolute_url()``, the 145 result of :meth:`item_link()` will be included directly in the URL, so you 146 are responsible for doing all necessary URL quoting and conversion to 147 ASCII inside the method itself. 188 148 189 149 .. _chicagocrime.org: http://www.chicagocrime.org/ 190 150 191 151 A complex example 192 152 ----------------- 193 153 194 The framework also supports more complex feeds, via parameters.154 The framework also supports more complex feeds, via arguments. 195 155 196 156 For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every 197 157 police beat in Chicago. It'd be silly to create a separate 198 :class:`~django.contrib.syndication. feeds.Feed` class for each police beat; that158 :class:`~django.contrib.syndication.views.Feed` class for each police beat; that 199 159 would violate the :ref:`DRY principle <dry>` and would couple data to 200 programming logic. Instead, the syndication framework lets you make generic 201 feeds that output items based on information in the feed's URL. 160 programming logic. Instead, the syndication framework lets you access the 161 arguments passed from your :ref:`URLconf <topics-http-urls>` so feeds can output 162 items based on information in the feed's URL. 202 163 203 164 On chicagocrime.org, the police-beat feeds are accessible via URLs like this: 204 165 205 * :file:`/rss/beats/0613/` -- Returns recent crimes for beat 0613. 206 * :file:`/rss/beats/1424/` -- Returns recent crimes for beat 1424. 166 * :file:`/beats/613/rss/` -- Returns recent crimes for beat 613. 167 * :file:`/beats/1424/rss/` -- Returns recent crimes for beat 1424. 168 169 These can be matched with a :ref:`URLconf <topics-http-urls>` line such as:: 207 170 208 The slug here is ``"beats"``. The syndication framework sees the extra URL bits 209 after the slug -- ``0613`` and ``1424`` -- and gives you a hook to tell it what 210 those URL bits mean, and how they should influence which items get published in 211 the feed. 171 (r'^beats/(?P<beat_id>\d+)/rss/$', BeatFeed()), 212 172 213 An example makes this clear. Here's the code for these beat-specific feeds:: 173 .. versionchanged:: 1.2 174 Instead of just taking the ``bits`` argument, ``get_object()`` now takes a 175 ``request`` object and the URL arguments. 214 176 215 from django.contrib.syndication.feeds import FeedDoesNotExist 216 from django.core.exceptions import ObjectDoesNotExist 177 Like a view, the arguments in the URL are passed to the :meth:`get_object()` 178 method along with the request object. Here's the code for these beat-specific 179 feeds:: 180 181 from django.contrib.syndication.views import FeedDoesNotExist 182 from django.shortcuts import get_object_or_404 217 183 218 184 class BeatFeed(Feed): 219 def get_object(self, bits): 220 # In case of "/rss/beats/0613/foo/bar/baz/", or other such clutter, 221 # check that bits has only one member. 222 if len(bits) != 1: 223 raise ObjectDoesNotExist 224 return Beat.objects.get(beat__exact=bits[0]) 185 description_template = 'feeds/beat_description.html' 186 187 def get_object(self, request, beat_id): 188 return get_object_or_404(Beat, pk=beat_id) 225 189 226 190 def title(self, obj): 227 191 return "Chicagocrime.org: Crimes for beat %s" % obj.beat 228 192 229 193 def link(self, obj): 230 if not obj:231 raise FeedDoesNotExist232 194 return obj.get_absolute_url() 233 195 234 196 def description(self, obj): 235 197 return "Crimes recently reported in police beat %s" % obj.beat 236 198 237 199 def items(self, obj): 238 return Crime.objects.filter(beat__id__exact=obj.id).order_by('-crime_date')[:30] 239 240 Here's the basic algorithm the RSS framework follows, given this class and a 241 request to the URL :file:`/rss/beats/0613/`: 242 243 * The framework gets the URL :file:`/rss/beats/0613/` and notices there's an 244 extra bit of URL after the slug. It splits that remaining string by the 245 slash character (``"/"``) and calls the 246 :class:`~django.contrib.syndication.feeds.Feed` class' 247 :meth:`get_object()` method, passing it the bits. In this case, bits is 248 ``['0613']``. For a request to :file:`/rss/beats/0613/foo/bar/`, bits 249 would be ``['0613', 'foo', 'bar']``. 250 251 * :meth:`get_object()` is responsible for retrieving the given beat, from 252 the given ``bits``. In this case, it uses the Django database API to 253 retrieve the beat. Note that :meth:`get_object()` should raise 254 :exc:`django.core.exceptions.ObjectDoesNotExist` if given invalid 255 parameters. There's no ``try``/``except`` around the 256 ``Beat.objects.get()`` call, because it's not necessary; that function 257 raises :exc:`Beat.DoesNotExist` on failure, and :exc:`Beat.DoesNotExist` 258 is a subclass of :exc:`ObjectDoesNotExist`. Raising 259 :exc:`ObjectDoesNotExist` in :meth:`get_object()` tells Django to produce 260 a 404 error for that request. 261 262 .. versionadded:: 1.0 263 :meth:`get_object()` can handle the :file:`/rss/beats/` url. 264 265 The :meth:`get_object()` method also has a chance to handle the 266 :file:`/rss/beats/` url. In this case, :data:`bits` will be an 267 empty list. In our example, ``len(bits) != 1`` and an 268 :exc:`ObjectDoesNotExist` exception will be raised, so 269 :file:`/rss/beats/` will generate a 404 page. But you can handle this case 270 however you like. For example, you could generate a combined feed for all 271 beats. 272 273 * To generate the feed's ``<title>``, ``<link>`` and ``<description>``, 274 Django uses the :meth:`title()`, :meth:`link()` and :meth:`description()` 275 methods. In the previous example, they were simple string class 276 attributes, but this example illustrates that they can be either strings 277 *or* methods. For each of :attr:`title`, :attr:`link` and 278 :attr:`description`, Django follows this algorithm: 279 280 * First, it tries to call a method, passing the ``obj`` argument, where 281 ``obj`` is the object returned by :meth:`get_object()`. 282 283 * Failing that, it tries to call a method with no arguments. 284 285 * Failing that, it uses the class attribute. 286 287 Inside the :meth:`link()` method, we handle the possibility that ``obj`` 288 might be ``None``, which can occur when the URL isn't fully specified. In 289 some cases, you might want to do something else in this case, which would 290 mean you'd need to check for ``obj`` existing in other methods as well. 291 (The :meth:`link()` method is called very early in the feed generation 292 process, so it's a good place to bail out early.) 293 294 * Finally, note that :meth:`items()` in this example also takes the ``obj`` 295 argument. The algorithm for :attr:`items` is the same as described in the 296 previous step -- first, it tries :meth:`items(obj)`, then :meth:`items()`, 297 then finally an :attr:`items` class attribute (which should be a list). 200 return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30] 201 202 To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django 203 uses the :meth:`title()`, :meth:`link()` and :meth:`description()` methods. In 204 the previous example, they were simple string class attributes, but this example 205 illustrates that they can be either strings *or* methods. For each of 206 :attr:`title`, :attr:`link` and :attr:`description`, Django follows this 207 algorithm: 208 209 * First, it tries to call a method, passing the ``obj`` argument, where 210 ``obj`` is the object returned by :meth:`get_object()`. 211 212 * Failing that, it tries to call a method with no arguments. 213 214 * Failing that, it uses the class attribute. 215 216 Also note that :meth:`items()` also follows the same algorithm -- first, it 217 tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items` 218 class attribute (which should be a list). 219 220 We are using a template for the item descriptions. It can be very simple: 221 222 .. code-block:: html+django 223 224 {{ obj.description }} 225 226 However, you are free to add formatting as required. 298 227 299 228 The ``ExampleFeed`` class below gives full documentation on methods and 300 attributes of :class:`~django.contrib.syndication. feeds.Feed` classes.229 attributes of :class:`~django.contrib.syndication.views.Feed` classes. 301 230 302 231 Specifying the type of feed 303 232 --------------------------- … … Specifying the type of feed 305 234 By default, feeds produced in this framework use RSS 2.0. 306 235 307 236 To change that, add a ``feed_type`` attribute to your 308 :class:`~django.contrib.syndication. feeds.Feed` class, like so::237 :class:`~django.contrib.syndication.views.Feed` class, like so:: 309 238 310 239 from django.utils.feedgenerator import Atom1Feed 311 240 … … Publishing Atom and RSS feeds in tandem 353 282 354 283 Some developers like to make available both Atom *and* RSS versions of their 355 284 feeds. That's easy to do with Django: Just create a subclass of your 356 :class:`~django.contrib.syndication. feeds.Feed`285 :class:`~django.contrib.syndication.views.Feed` 357 286 class and set the :attr:`feed_type` to something different. Then update your 358 287 URLconf to add the extra versions. 359 288 360 289 Here's a full example:: 361 290 362 from django.contrib.syndication. feeds import Feed291 from django.contrib.syndication.views import Feed 363 292 from chicagocrime.models import NewsItem 364 293 from django.utils.feedgenerator import Atom1Feed 365 294 … … Here's a full example:: 381 310 a feed-level "description," but they *do* provide for a "subtitle." 382 311 383 312 If you provide a :attr:`description` in your 384 :class:`~django.contrib.syndication. feeds.Feed` class, Django will *not*313 :class:`~django.contrib.syndication.views.Feed` class, Django will *not* 385 314 automatically put that into the :attr:`subtitle` element, because a 386 315 subtitle and description are not necessarily the same thing. Instead, you 387 316 should define a :attr:`subtitle` attribute. … … And the accompanying URLconf:: 394 323 from django.conf.urls.defaults import * 395 324 from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed 396 325 397 feeds = {398 'rss': RssSiteNewsFeed,399 'atom': AtomSiteNewsFeed,400 }401 402 326 urlpatterns = patterns('', 403 327 # ... 404 (r'^ feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',405 {'feed_dict': feeds}),328 (r'^sitenews/rss/$', RssSiteNewsFeed()), 329 (r'^sitenews/atom/$', AtomSiteNewsFeed()), 406 330 # ... 407 331 ) 408 332 409 333 Feed class reference 410 334 -------------------- 411 335 412 .. class:: django.contrib.syndication. feeds.Feed336 .. class:: django.contrib.syndication.views.Feed 413 337 414 338 This example illustrates all possible attributes and methods for a 415 :class:`~django.contrib.syndication. feeds.Feed` class::339 :class:`~django.contrib.syndication.views.Feed` class:: 416 340 417 from django.contrib.syndication. feeds import Feed341 from django.contrib.syndication.views import Feed 418 342 from django.utils import feedgenerator 419 343 420 344 class ExampleFeed(Feed): … … This example illustrates all possible attributes and methods for a 430 354 # TEMPLATE NAMES -- Optional. These should be strings representing 431 355 # names of Django templates that the system should use in rendering the 432 356 # title and description of your feed items. Both are optional. 433 # If you don't specify one, or either, Django will use the template 434 # 'feeds/SLUG_title.html' and 'feeds/SLUG_description.html', where SLUG 435 # is the slug you specify in the URL. 357 # If one is not specified, the item_title() or item_description() 358 # methods are used instead. 436 359 437 360 title_template = None 438 361 description_template = None … … This example illustrates all possible attributes and methods for a 572 495 # COPYRIGHT NOTICE -- One of the following three is optional. The 573 496 # framework looks for them in this order. 574 497 575 def copyright(self, obj):498 def feed_copyright(self, obj): 576 499 """ 577 500 Takes the object returned by get_object() and returns the feed's 578 501 copyright notice as a normal Python string. 579 502 """ 580 503 581 def copyright(self):504 def feed_copyright(self): 582 505 """ 583 506 Returns the feed's copyright notice as a normal Python string. 584 507 """ 585 508 586 copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.509 feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice. 587 510 588 511 # TTL -- One of the following three is optional. The framework looks 589 512 # for them in this order. Ignored for Atom feeds. … … This example illustrates all possible attributes and methods for a 620 543 # GET_OBJECT -- This is required for feeds that publish different data 621 544 # for different URL parameters. (See "A complex example" above.) 622 545 623 def get_object(self, bits):546 def get_object(self, request, *args, **kwargs): 624 547 """ 625 Takes a list of strings gleaned from the URL and returns an object626 re presented by this feed. Raises548 Takes the current request and the arguments from the URL, and 549 returns an object represented by this feed. Raises 627 550 django.core.exceptions.ObjectDoesNotExist on error. 628 551 """ 552 553 # ITEM TITLE AND DESCRIPTION -- If title_template or 554 # description_template are not defined, these are used instead. Both are 555 # optional, by default they will use the unicode representation of the 556 # item. 557 558 def item_title(self, item): 559 """ 560 Takes an item, as returned by items(), and returns the item's 561 title as a normal Python string. 562 """ 563 564 def item_title(self): 565 """ 566 Returns the title for every item in the feed. 567 """ 568 569 item_title = 'Breaking News: Nothing Happening' # Hard-coded title. 570 571 def item_description(self, item): 572 """ 573 Takes an item, as returned by items(), and returns the item's 574 description as a normal Python string. 575 """ 576 577 def item_description(self): 578 """ 579 Returns the description for every item in the feed. 580 """ 629 581 582 item_description = 'A description of the item.' # Hard-coded description. 583 630 584 # ITEM LINK -- One of these three is required. The framework looks for 631 585 # them in this order. 632 586 … … This example illustrates all possible attributes and methods for a 686 640 687 641 item_author_email = 'test@example.com' # Hard-coded author e-mail. 688 642 689 # ITEM AUTHOR LINK -- One of the following three is optional. The643 # ITEM AUTHOR LINK -- One of the following three is optional. The 690 644 # framework looks for them in this order. In each case, the URL should 691 645 # include the "http://" and domain name. 692 646 # -
docs/releases/1.2.txt
diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index f83e4dc..1b08486 100644
a b approach. Old style function-based test runners will still work, but 386 386 should be updated to use the new :ref:`class-based runners 387 387 <topics-testing-test_runner>`. 388 388 389 ``Feed`` in ``django.contrib.syndication.feeds`` 390 ------------------------------------------------ 391 392 The ``Feed`` class in ``syndication.feeds`` has been replaced by 393 the ``Feed`` class in ``syndication.views``, and will be removed in Django 1.4. 394 395 The new class has an almost identical API, but allows instances to be used as 396 views. For example, consider the use of the old framework in the following 397 :ref:`URLconf <topics-http-urls>`:: 398 399 from django.conf.urls.defaults import * 400 from myproject.feeds import LatestEntries, LatestEntriesByCategory 401 402 feeds = { 403 'latest': LatestEntries, 404 'categories': LatestEntriesByCategory, 405 } 406 407 urlpatterns = patterns('', 408 # ... 409 (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', 410 {'feed_dict': feeds}), 411 # ... 412 ) 413 414 This should be replaced by:: 415 416 from django.conf.urls.defaults import * 417 from myproject.feeds import LatestEntries, LatestEntriesByCategory 418 419 urlpatterns = patterns('', 420 # ... 421 (r'^feeds/latest/$', LatestEntries()), 422 (r'^feeds/categories/(?P<category_id>\d+)/$', LatestEntriesByCategory()), 423 # ... 424 ) 425 426 If you used the ``feed()`` view, the ``LatestEntries`` class would not need to 427 be modified apart from subclassing the new ``Feed`` class. However, 428 ``LatestEntriesByCategory`` used the ``get_object()`` method with the ``bits`` 429 argument to specify a specific category to show. Much like a view, the 430 ``get_object()`` method now takes a ``request`` and arguments from the URL, so 431 it would look like this:: 432 433 from django.contrib.syndication.views import Feed 434 from django.shortcuts import get_object_or_404 435 from myproject.models import Category 436 437 class LatestEntriesByCategory(Feed): 438 def get_object(self, request, category_id): 439 return get_object_or_404(Category, id=category_id) 440 441 # ... 442 443 Additionally, the ``get_feed()`` method on ``Feed`` classes now take different 444 arguments, which may impact you if you use the ``Feed`` classes directly. 445 Instead of just taking an optional ``url`` argument, it now takes two arguments: 446 the object returned by its own ``get_object()`` method, and the current 447 ``request`` object. 448 449 To take into account ``Feed`` classes not being initialized for each 450 request, the ``__init__()`` method now takes no arguments by default. 451 Previously it would have taken the ``slug`` from the URL and the ``request`` 452 object. 453 454 In previous versions of Django, the ``feed_copyright()`` method was incorrectly 455 documented as ``copyright()``. Change any ``copyright()`` methods you have on 456 your ``Feed`` classes to ``feed_copyright()``. 457 458 In accordance with `RSS best practices`_, RSS feeds will now include an 459 ``atom:link`` element. You may need to update your tests to take this into 460 account. 461 462 For more information, see the full :ref:`syndication framework documentation 463 <ref-contrib-syndication>`. 464 465 .. _RSS best practices: http://www.rssboard.org/rss-profile 466 389 467 What's new in Django 1.2 390 468 ======================== 391 469 … … reusable, encapsulated validation behavior. Note, however, that 549 627 validation must still be performed explicitly. Simply invoking a model 550 628 instance's ``save()`` method will not perform any validation of the 551 629 instance's data. 630 631 Syndication feeds as views 632 -------------------------- 633 634 :ref:`Syndication feeds <ref-contrib-syndication>` can now be used directly as 635 views in your :ref:`URLconf <topics-http-urls>`. Previously, all the feeds on a 636 site had to live beneath a single URL, which produced ugly, unnatural URLs. 637 638 For example, suppose you listed blog posts for the "django" tag under 639 ``/blog/django/``. In previous versions of Django, a feed for those posts would 640 have had to be something like ``/feeds/blog-tag/django/``. In Django 1.2, it 641 can simply be ``/blog/django/feed/``. 642 643 Like any other view, feeds are now passed a ``request`` object, so you can 644 do user based access control amongst other things. 645 646 Also, you no longer need to create templates for your feed item titles and 647 descriptions. You can set these with the ``item_title()`` and 648 ``item_description()`` methods. 649 -
tests/regressiontests/syndication/feeds.py
diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py index 79837f9..8648cec 100644
a b 1 from django.contrib.syndication import feeds, views 1 2 from django.core.exceptions import ObjectDoesNotExist 2 from django.contrib.syndication import feeds 3 from django.utils.feedgenerator import Atom1Feed 4 from django.utils import tzinfo 3 from django.utils import feedgenerator, tzinfo 4 from models import Article, Entry 5 5 6 class ComplexFeed(feeds.Feed): 7 def get_object(self, bits): 8 if len(bits) != 1: 6 7 class ComplexFeed(views.Feed): 8 def get_object(self, request, foo=None): 9 if foo is not None: 9 10 raise ObjectDoesNotExist 10 11 return None 11 12 12 class TestRssFeed(feeds.Feed): 13 link = "/blog/" 13 14 class TestRss2Feed(views.Feed): 14 15 title = 'My blog' 16 description = 'A more thorough description of my blog.' 17 link = '/blog/' 18 feed_guid = '/foo/bar/1234' 19 author_name = 'Sally Smith' 20 author_email = 'test@example.com' 21 author_link = 'http://www.example.com/' 22 categories = ('python', 'django') 23 feed_copyright = 'Copyright (c) 2007, Sally Smith' 24 ttl = 600 15 25 16 26 def items(self): 17 from models import Entry18 27 return Entry.objects.all() 19 20 def item_link(self, item): 21 return "/blog/%s/" % item.pk 28 29 def item_description(self, item): 30 return "Overridden description: %s" % item 31 32 def item_pubdate(self, item): 33 return item.date 34 35 item_author_name = 'Sally Smith' 36 item_author_email = 'test@example.com' 37 item_author_link = 'http://www.example.com/' 38 item_categories = ('python', 'testing') 39 item_copyright = 'Copyright (c) 2007, Sally Smith' 40 41 42 class TestRss091Feed(TestRss2Feed): 43 feed_type = feedgenerator.RssUserland091Feed 44 22 45 23 class TestAtomFeed(TestRssFeed): 24 feed_type = Atom1Feed 46 class TestAtomFeed(TestRss2Feed): 47 feed_type = feedgenerator.Atom1Feed 48 subtitle = TestRss2Feed.description 25 49 26 class MyCustomAtom1Feed(Atom1Feed): 50 51 class ArticlesFeed(TestRss2Feed): 52 """ 53 A feed to test no link being defined. Articles have no get_absolute_url() 54 method, and item_link() is not defined. 55 """ 56 def items(self): 57 return Article.objects.all() 58 59 60 class TestEnclosureFeed(TestRss2Feed): 61 pass 62 63 64 class TemplateFeed(TestRss2Feed): 65 """ 66 A feed to test defining item titles and descriptions with templates. 67 """ 68 title_template = 'syndication/title.html' 69 description_template = 'syndication/description.html' 70 71 # Defining a template overrides any item_title definition 72 def item_title(self): 73 return "Not in a template" 74 75 76 class NaiveDatesFeed(TestAtomFeed): 77 """ 78 A feed with naive (non-timezone-aware) dates. 79 """ 80 def item_pubdate(self, item): 81 return item.date 82 83 84 class TZAwareDatesFeed(TestAtomFeed): 85 """ 86 A feed with timezone-aware dates. 87 """ 88 def item_pubdate(self, item): 89 # Provide a weird offset so that the test can know it's getting this 90 # specific offset and not accidentally getting on from 91 # settings.TIME_ZONE. 92 return item.date.replace(tzinfo=tzinfo.FixedOffset(42)) 93 94 95 class TestFeedUrlFeed(TestAtomFeed): 96 feed_url = 'http://example.com/customfeedurl/' 97 98 99 class MyCustomAtom1Feed(feedgenerator.Atom1Feed): 27 100 """ 28 101 Test of a custom feed generator class. 29 102 """ … … class MyCustomAtom1Feed(Atom1Feed): 44 117 def add_item_elements(self, handler, item): 45 118 super(MyCustomAtom1Feed, self).add_item_elements(handler, item) 46 119 handler.addQuickElement(u'ministry', u'silly walks') 47 120 121 48 122 class TestCustomFeed(TestAtomFeed): 49 123 feed_type = MyCustomAtom1Feed 124 125 126 class DeprecatedComplexFeed(feeds.Feed): 127 def get_object(self, bits): 128 if len(bits) != 1: 129 raise ObjectDoesNotExist 130 return None 131 132 133 class DeprecatedRssFeed(feeds.Feed): 134 link = "/blog/" 135 title = 'My blog' 50 136 51 class NaiveDatesFeed(TestAtomFeed): 52 """ 53 A feed with naive (non-timezone-aware) dates. 54 """ 55 def item_pubdate(self, item): 56 return item.date 137 def items(self): 138 return Entry.objects.all() 57 139 58 class TZAwareDatesFeed(TestAtomFeed): 59 """ 60 A feed with timezone-aware dates. 61 """ 62 def item_pubdate(self, item): 63 # Provide a weird offset so that the test can know it's getting this 64 # specific offset and not accidentally getting on from 65 # settings.TIME_ZONE. 66 return item.date.replace(tzinfo=tzinfo.FixedOffset(42)) 67 No newline at end of file 140 def item_link(self, item): 141 return "/blog/%s/" % item.pk 142 -
tests/regressiontests/syndication/fixtures/feeddata.json
diff --git a/tests/regressiontests/syndication/fixtures/feeddata.json b/tests/regressiontests/syndication/fixtures/feeddata.json index 375ee16..4a5c022 100644
a b 30 30 "title": "A & B < C > D", 31 31 "date": "2008-01-03 13:30:00" 32 32 } 33 }, 34 { 35 "model": "syndication.article", 36 "pk": 1, 37 "fields": { 38 "title": "My first article", 39 "entry": "1" 40 } 33 41 } 34 ] 35 No newline at end of file 42 ] -
tests/regressiontests/syndication/models.py
diff --git a/tests/regressiontests/syndication/models.py b/tests/regressiontests/syndication/models.py index 99e14ad..19e645a 100644
a b 1 1 from django.db import models 2 2 3 3 class Entry(models.Model): 4 4 title = models.CharField(max_length=200) 5 5 date = models.DateTimeField() 6 6 7 class Meta: 8 ordering = ('date',) 9 7 10 def __unicode__(self): 8 return self.title 9 No newline at end of file 11 return self.title 12 13 def get_absolute_url(self): 14 return "/blog/%s/" % self.pk 15 16 17 class Article(models.Model): 18 title = models.CharField(max_length=200) 19 entry = models.ForeignKey(Entry) 20 21 def __unicode__(self): 22 return self.title 23 -
new file tests/regressiontests/syndication/templates/syndication/description.html
diff --git a/tests/regressiontests/syndication/templates/syndication/description.html b/tests/regressiontests/syndication/templates/syndication/description.html new file mode 100644 index 0000000..85ec82c
- + 1 Description in your templates: {{ obj }} 2 No newline at end of file -
new file tests/regressiontests/syndication/templates/syndication/title.html
diff --git a/tests/regressiontests/syndication/templates/syndication/title.html b/tests/regressiontests/syndication/templates/syndication/title.html new file mode 100644 index 0000000..eb17969
- + 1 Title in your templates: {{ obj }} 2 No newline at end of file -
tests/regressiontests/syndication/tests.py
diff --git a/tests/regressiontests/syndication/tests.py b/tests/regressiontests/syndication/tests.py index 816cb44..2c304c2 100644
a b 1 # -*- coding: utf-8 -*-2 3 1 import datetime 4 from xml.dom import minidom 2 from django.contrib.syndication import feeds, views 3 from django.core.exceptions import ImproperlyConfigured 5 4 from django.test import TestCase 6 from django.test.client import Client7 5 from django.utils import tzinfo 8 6 from models import Entry 7 from xml.dom import minidom 8 9 9 try: 10 10 set 11 11 except NameError: 12 12 from sets import Set as set 13 13 14 class SyndicationFeedTest(TestCase):14 class FeedTestCase(TestCase): 15 15 fixtures = ['feeddata.json'] 16 16 17 17 def assertChildNodes(self, elem, expected): 18 18 actual = set([n.nodeName for n in elem.childNodes]) 19 19 expected = set(expected) 20 20 self.assertEqual(actual, expected) 21 22 def assertChildNodeContent(self, elem, expected): 23 for k, v in expected.items(): 24 self.assertEqual( 25 elem.getElementsByTagName(k)[0].firstChild.wholeText, v) 26 27 def assertCategories(self, elem, expected): 28 self.assertEqual(set(i.firstChild.wholeText for i in elem.childNodes if i.nodeName == 'category'), set(expected)); 21 29 22 def test_rss_feed(self): 23 response = self.client.get('/syndication/feeds/rss/') 30 ###################################### 31 # Feed view 32 ###################################### 33 34 class SyndicationFeedTest(FeedTestCase): 35 """ 36 Tests for the high-level syndication feed framework. 37 """ 38 39 def test_rss2_feed(self): 40 """ 41 Test the structure and content of feeds generated by Rss201rev2Feed. 42 """ 43 response = self.client.get('/syndication/rss2/') 24 44 doc = minidom.parseString(response.content) 25 45 26 46 # Making sure there's only 1 `rss` element and that the correct … … class SyndicationFeedTest(TestCase): 35 55 chan_elem = feed.getElementsByTagName('channel') 36 56 self.assertEqual(len(chan_elem), 1) 37 57 chan = chan_elem[0] 38 self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item']) 58 self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category']) 59 self.assertChildNodeContent(chan, { 60 'title': 'My blog', 61 'description': 'A more thorough description of my blog.', 62 'link': 'http://example.com/blog/', 63 'language': 'en', 64 'lastBuildDate': 'Thu, 03 Jan 2008 13:30:00 -0600', 65 #'atom:link': '', 66 'ttl': '600', 67 'copyright': 'Copyright (c) 2007, Sally Smith', 68 }) 69 self.assertCategories(chan, ['python', 'django']); 70 71 # Ensure the content of the channel is correct 72 self.assertChildNodeContent(chan, { 73 'title': 'My blog', 74 'link': 'http://example.com/blog/', 75 }) 76 77 # Check feed_url is passed 78 self.assertEqual( 79 chan.getElementsByTagName('atom:link')[0].getAttribute('href'), 80 'http://example.com/syndication/rss2/' 81 ) 82 83 items = chan.getElementsByTagName('item') 84 self.assertEqual(len(items), Entry.objects.count()) 85 self.assertChildNodeContent(items[0], { 86 'title': 'My first entry', 87 'description': 'Overridden description: My first entry', 88 'link': 'http://example.com/blog/1/', 89 'guid': 'http://example.com/blog/1/', 90 'pubDate': 'Tue, 01 Jan 2008 12:30:00 -0600', 91 'author': 'test@example.com (Sally Smith)', 92 }) 93 self.assertCategories(items[0], ['python', 'testing']); 94 95 for item in items: 96 self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'category', 'pubDate', 'author']) 39 97 98 def test_rss091_feed(self): 99 """ 100 Test the structure and content of feeds generated by RssUserland091Feed. 101 """ 102 response = self.client.get('/syndication/rss091/') 103 doc = minidom.parseString(response.content) 104 105 # Making sure there's only 1 `rss` element and that the correct 106 # RSS version was specified. 107 feed_elem = doc.getElementsByTagName('rss') 108 self.assertEqual(len(feed_elem), 1) 109 feed = feed_elem[0] 110 self.assertEqual(feed.getAttribute('version'), '0.91') 111 112 # Making sure there's only one `channel` element w/in the 113 # `rss` element. 114 chan_elem = feed.getElementsByTagName('channel') 115 self.assertEqual(len(chan_elem), 1) 116 chan = chan_elem[0] 117 self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category']) 118 119 # Ensure the content of the channel is correct 120 self.assertChildNodeContent(chan, { 121 'title': 'My blog', 122 'link': 'http://example.com/blog/', 123 }) 124 self.assertCategories(chan, ['python', 'django']) 125 126 # Check feed_url is passed 127 self.assertEqual( 128 chan.getElementsByTagName('atom:link')[0].getAttribute('href'), 129 'http://example.com/syndication/rss091/' 130 ) 131 40 132 items = chan.getElementsByTagName('item') 41 133 self.assertEqual(len(items), Entry.objects.count()) 134 self.assertChildNodeContent(items[0], { 135 'title': 'My first entry', 136 'description': 'Overridden description: My first entry', 137 'link': 'http://example.com/blog/1/', 138 }) 42 139 for item in items: 43 self.assertChildNodes(item, ['title', 'link', 'description', 'guid']) 140 self.assertChildNodes(item, ['title', 'link', 'description']) 141 self.assertCategories(item, []) 44 142 45 143 def test_atom_feed(self): 46 response = self.client.get('/syndication/feeds/atom/') 47 doc = minidom.parseString(response.content) 144 """ 145 Test the structure and content of feeds generated by Atom1Feed. 146 """ 147 response = self.client.get('/syndication/atom/') 148 feed = minidom.parseString(response.content).firstChild 48 149 49 feed = doc.firstChild50 150 self.assertEqual(feed.nodeName, 'feed') 51 151 self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom') 52 self.assertChildNodes(feed, ['title', 'link', 'id', 'updated', 'entry']) 152 self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'rights', 'category', 'author']) 153 for link in feed.getElementsByTagName('link'): 154 if link.getAttribute('rel') == 'self': 155 self.assertEqual(link.getAttribute('href'), 'http://example.com/syndication/atom/') 53 156 54 157 entries = feed.getElementsByTagName('entry') 55 158 self.assertEqual(len(entries), Entry.objects.count()) 56 159 for entry in entries: 57 self.assertChildNodes(entry, ['title', 'link', 'id', 'summary' ])160 self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'category', 'updated', 'rights', 'author']) 58 161 summary = entry.getElementsByTagName('summary')[0] 59 162 self.assertEqual(summary.getAttribute('type'), 'html') 60 163 61 164 def test_custom_feed_generator(self): 62 response = self.client.get('/syndication/ feeds/custom/')63 doc = minidom.parseString(response.content)165 response = self.client.get('/syndication/custom/') 166 feed = minidom.parseString(response.content).firstChild 64 167 65 feed = doc.firstChild66 168 self.assertEqual(feed.nodeName, 'feed') 67 169 self.assertEqual(feed.getAttribute('django'), 'rocks') 68 self.assertChildNodes(feed, ['title', ' link', 'id', 'updated', 'entry', 'spam'])170 self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'spam', 'rights', 'category', 'author']) 69 171 70 172 entries = feed.getElementsByTagName('entry') 71 173 self.assertEqual(len(entries), Entry.objects.count()) 72 174 for entry in entries: 73 175 self.assertEqual(entry.getAttribute('bacon'), 'yum') 74 self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry' ])176 self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry', 'rights', 'author', 'updated', 'category']) 75 177 summary = entry.getElementsByTagName('summary')[0] 76 178 self.assertEqual(summary.getAttribute('type'), 'html') 77 78 def test_complex_base_url(self): 79 """ 80 Tests that that the base url for a complex feed doesn't raise a 500 81 exception. 82 """ 83 response = self.client.get('/syndication/feeds/complex/') 84 self.assertEquals(response.status_code, 404) 85 179 86 180 def test_title_escaping(self): 87 181 """ 88 182 Tests that titles are escaped correctly in RSS feeds. 89 183 """ 90 response = self.client.get('/syndication/ feeds/rss/')184 response = self.client.get('/syndication/rss2/') 91 185 doc = minidom.parseString(response.content) 92 186 for item in doc.getElementsByTagName('item'): 93 187 link = item.getElementsByTagName('link')[0] … … class SyndicationFeedTest(TestCase): 101 195 """ 102 196 # Naive date times passed in get converted to the local time zone, so 103 197 # check the recived zone offset against the local offset. 104 response = self.client.get('/syndication/ feeds/naive-dates/')198 response = self.client.get('/syndication/naive-dates/') 105 199 doc = minidom.parseString(response.content) 106 200 updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText 107 201 tz = tzinfo.LocalTimezone(datetime.datetime.now()) … … class SyndicationFeedTest(TestCase): 112 206 """ 113 207 Test that datetimes with timezones don't get trodden on. 114 208 """ 115 response = self.client.get('/syndication/ feeds/aware-dates/')209 response = self.client.get('/syndication/aware-dates/') 116 210 doc = minidom.parseString(response.content) 117 211 updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText 118 212 self.assertEqual(updated[-6:], '+00:42') 119 120 No newline at end of file 213 214 def test_feed_url(self): 215 """ 216 Test that the feed_url can be overridden. 217 """ 218 response = self.client.get('/syndication/feedurl/') 219 doc = minidom.parseString(response.content) 220 for link in doc.getElementsByTagName('link'): 221 if link.getAttribute('rel') == 'self': 222 self.assertEqual(link.getAttribute('href'), 'http://example.com/customfeedurl/') 223 224 def test_item_link_error(self): 225 """ 226 Test that a ImproperlyConfigured is raised if no link could be found 227 for the item(s). 228 """ 229 self.assertRaises(ImproperlyConfigured, 230 self.client.get, 231 '/syndication/articles/') 232 233 def test_template_feed(self): 234 """ 235 Test that the item title and description can be overridden with 236 templates. 237 """ 238 response = self.client.get('/syndication/template/') 239 doc = minidom.parseString(response.content) 240 feed = doc.getElementsByTagName('rss')[0] 241 chan = feed.getElementsByTagName('channel')[0] 242 items = chan.getElementsByTagName('item') 243 244 self.assertChildNodeContent(items[0], { 245 'title': 'Title in your templates: My first entry', 246 'description': 'Description in your templates: My first entry', 247 'link': 'http://example.com/blog/1/', 248 }) 249 250 def test_add_domain(self): 251 """ 252 Test add_domain() prefixes domains onto the correct URLs. 253 """ 254 self.assertEqual( 255 views.add_domain('example.com', '/foo/?arg=value'), 256 'http://example.com/foo/?arg=value' 257 ) 258 self.assertEqual( 259 views.add_domain('example.com', 'http://djangoproject.com/doc/'), 260 'http://djangoproject.com/doc/' 261 ) 262 self.assertEqual( 263 views.add_domain('example.com', 'https://djangoproject.com/doc/'), 264 'https://djangoproject.com/doc/' 265 ) 266 self.assertEqual( 267 views.add_domain('example.com', 'mailto:uhoh@djangoproject.com'), 268 'mailto:uhoh@djangoproject.com' 269 ) 270 271 272 ###################################### 273 # Deprecated feeds 274 ###################################### 275 276 class DeprecatedSyndicationFeedTest(FeedTestCase): 277 """ 278 Tests for the deprecated API (feed() view and the feed_dict etc). 279 """ 280 281 def test_empty_feed_dict(self): 282 """ 283 Test that an empty feed_dict raises a 404. 284 """ 285 response = self.client.get('/syndication/depr-feeds-empty/aware-dates/') 286 self.assertEquals(response.status_code, 404) 287 288 def test_nonexistent_slug(self): 289 """ 290 Test that a non-existent slug raises a 404. 291 """ 292 response = self.client.get('/syndication/depr-feeds/foobar/') 293 self.assertEquals(response.status_code, 404) 294 295 def test_rss_feed(self): 296 """ 297 A simple test for Rss201rev2Feed feeds generated by the deprecated 298 system. 299 """ 300 response = self.client.get('/syndication/depr-feeds/rss/') 301 doc = minidom.parseString(response.content) 302 feed = doc.getElementsByTagName('rss')[0] 303 self.assertEqual(feed.getAttribute('version'), '2.0') 304 305 chan = feed.getElementsByTagName('channel')[0] 306 self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link']) 307 308 items = chan.getElementsByTagName('item') 309 self.assertEqual(len(items), Entry.objects.count()) 310 311 def test_complex_base_url(self): 312 """ 313 Tests that the base url for a complex feed doesn't raise a 500 314 exception. 315 """ 316 response = self.client.get('/syndication/depr-feeds/complex/') 317 self.assertEquals(response.status_code, 404) 318 -
tests/regressiontests/syndication/urls.py
diff --git a/tests/regressiontests/syndication/urls.py b/tests/regressiontests/syndication/urls.py index ec45026..b4e0bcb 100644
a b 1 import feeds 2 from django.conf.urls.defaults import patterns 1 from django.conf.urls.defaults import * 3 2 3 import feeds 4 4 5 feed_dict = { 5 'complex': feeds.ComplexFeed, 6 'rss': feeds.TestRssFeed, 7 'atom': feeds.TestAtomFeed, 8 'custom': feeds.TestCustomFeed, 9 'naive-dates': feeds.NaiveDatesFeed, 10 'aware-dates': feeds.TZAwareDatesFeed, 6 'complex': feeds.DeprecatedComplexFeed, 7 'rss': feeds.DeprecatedRssFeed, 11 8 } 12 urlpatterns = patterns('', 13 (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feed_dict}) 9 10 urlpatterns = patterns('django.contrib.syndication.views', 11 (r'^complex/(?P<foo>.*)/$', feeds.ComplexFeed()), 12 (r'^rss2/$', feeds.TestRss2Feed()), 13 (r'^rss091/$', feeds.TestRss091Feed()), 14 (r'^atom/$', feeds.TestAtomFeed()), 15 (r'^custom/$', feeds.TestCustomFeed()), 16 (r'^naive-dates/$', feeds.NaiveDatesFeed()), 17 (r'^aware-dates/$', feeds.TZAwareDatesFeed()), 18 (r'^feedurl/$', feeds.TestFeedUrlFeed()), 19 (r'^articles/$', feeds.ArticlesFeed()), 20 (r'^template/$', feeds.TemplateFeed()), 21 22 (r'^depr-feeds/(?P<url>.*)/$', 'feed', {'feed_dict': feed_dict}), 23 (r'^depr-feeds-empty/(?P<url>.*)/$', 'feed', {'feed_dict': None}), 14 24 ) -
new file tests/regressiontests/utils/feedgenerator.py
diff --git a/tests/regressiontests/utils/feedgenerator.py b/tests/regressiontests/utils/feedgenerator.py new file mode 100644 index 0000000..3274d69
- + 1 import datetime 2 from django.utils import feedgenerator, tzinfo 3 4 class FeedgeneratorTest(TestCase): 5 """ 6 Tests for the low-level syndication feed framework. 7 """ 8 9 def test_get_tag_uri(self): 10 """ 11 Test get_tag_uri() correctly generates TagURIs. 12 """ 13 self.assertEqual( 14 feedgenerator.get_tag_uri('http://example.org/foo/bar#headline', datetime.date(2004, 10, 25)), 15 u'tag:example.org,2004-10-25:/foo/bar/headline') 16 17 def test_get_tag_uri_with_port(self): 18 """ 19 Test that get_tag_uri() correctly generates TagURIs from URLs with port 20 numbers. 21 """ 22 self.assertEqual( 23 feedgenerator.get_tag_uri('http://www.example.org:8000/2008/11/14/django#headline', datetime.datetime(2008, 11, 14, 13, 37, 0)), 24 u'tag:www.example.org,2008-11-14:/2008/11/14/django/headline') 25 26 def test_rfc2822_date(self): 27 """ 28 Test rfc2822_date() correctly formats datetime objects. 29 """ 30 self.assertEqual( 31 feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0)), 32 "Fri, 14 Nov 2008 13:37:00 -0000" 33 ) 34 35 def test_rfc2822_date_with_timezone(self): 36 """ 37 Test rfc2822_date() correctly formats datetime objects with tzinfo. 38 """ 39 self.assertEqual( 40 feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=60)))), 41 "Fri, 14 Nov 2008 13:37:00 +0100" 42 ) 43 44 def test_rfc3339_date(self): 45 """ 46 Test rfc3339_date() correctly formats datetime objects. 47 """ 48 self.assertEqual( 49 feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0)), 50 "2008-11-14T13:37:00Z" 51 ) 52 53 def test_rfc3339_date_with_timezone(self): 54 """ 55 Test rfc3339_date() correctly formats datetime objects with tzinfo. 56 """ 57 self.assertEqual( 58 feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=120)))), 59 "2008-11-14T13:37:00+02:00" 60 ) 61