Ticket #12403: syndication-views-3.diff
File syndication-views-3.diff, 73.8 KB (added by , 15 years ago) |
---|
-
AUTHORS
diff --git a/AUTHORS b/AUTHORS index c8d91f7..d3fb3bc 100644
a b answer newbie questions, and generally made Django that much better: 162 162 Afonso Fernández Nogueira <fonzzo.django@gmail.com> 163 163 J. Pablo Fernandez <pupeno@pupeno.com> 164 164 Maciej Fijalkowski 165 Ben Firshman <ben@firshman.co.uk> 165 166 Matthew Flanagan <http://wadofstuff.blogspot.com> 166 167 Eric Floehr <eric@intellovations.com> 167 168 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 ade43d7..06b5f73 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 {} 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) 76 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 423d333..be374e1 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://') or url.startswith('https://')): 13 # 'url' must already be ASCII and URL-quoted, so no need for encoding 14 # conversions here. 15 url = iri_to_uri(u'http://%s%s' % (domain, url)) 16 return url 17 18 class FeedDoesNotExist(ObjectDoesNotExist): 19 pass 20 21 22 class Feed(object): 23 feed_type = feedgenerator.DefaultFeed 24 title_template = None 25 description_template = None 26 27 def __call__(self, request, *args, **kwargs): 28 try: 29 obj = self.get_object(request, *args, **kwargs) 30 except ObjectDoesNotExist: 31 raise Http404 32 feedgen = self.get_feed(obj, request) 33 response = HttpResponse(mimetype=feedgen.mime_type) 34 feedgen.write(response, 'utf-8') 35 return response 36 37 def item_title(self, item): 38 # Titles should be double escaped by default (see #6533) 39 return escape(force_unicode(item)) 40 41 def item_description(self, item): 42 return force_unicode(item) 43 44 def item_link(self, item): 45 try: 46 return item.get_absolute_url() 47 except AttributeError: 48 raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__ 49 50 def __get_dynamic_attr(self, attname, obj, default=None): 51 try: 52 attr = getattr(self, attname) 53 except AttributeError: 54 return default 55 if callable(attr): 56 # Check func_code.co_argcount rather than try/excepting the 57 # function and catching the TypeError, because something inside 58 # the function may raise the TypeError. This technique is more 59 # accurate. 60 if hasattr(attr, 'func_code'): 61 argcount = attr.func_code.co_argcount 62 else: 63 argcount = attr.__call__.func_code.co_argcount 64 if argcount == 2: # one argument is 'self' 65 return attr(obj) 66 else: 67 return attr() 68 return attr 69 70 71 def feed_extra_kwargs(self, obj): 72 """ 73 Returns an extra keyword arguments dictionary that is used when 74 initializing the feed generator. 75 """ 76 return {} 77 78 def item_extra_kwargs(self, item): 79 """ 80 Returns an extra keyword arguments dictionary that is used with 81 the `add_item` call of the feed generator. 82 """ 83 return {} 84 85 def get_object(self, request, *args, **kwargs): 86 return None 87 88 def get_feed(self, obj, request): 89 """ 90 Returns a feedgenerator.DefaultFeed object, fully populated, for 91 this feed. Raises FeedDoesNotExist for invalid parameters. 92 """ 93 if Site._meta.installed: 94 current_site = Site.objects.get_current() 95 else: 96 current_site = RequestSite(request) 97 98 link = self.__get_dynamic_attr('link', obj) 99 link = add_domain(current_site.domain, link) 100 101 feed = self.feed_type( 102 title = self.__get_dynamic_attr('title', obj), 103 subtitle = self.__get_dynamic_attr('subtitle', obj), 104 link = link, 105 description = self.__get_dynamic_attr('description', obj), 106 language = settings.LANGUAGE_CODE.decode(), 107 feed_url = add_domain(current_site.domain, 108 self.__get_dynamic_attr('feed_url', obj) or request.path), 109 author_name = self.__get_dynamic_attr('author_name', obj), 110 author_link = self.__get_dynamic_attr('author_link', obj), 111 author_email = self.__get_dynamic_attr('author_email', obj), 112 categories = self.__get_dynamic_attr('categories', obj), 113 feed_copyright = self.__get_dynamic_attr('feed_copyright', obj), 114 feed_guid = self.__get_dynamic_attr('feed_guid', obj), 115 ttl = self.__get_dynamic_attr('ttl', obj), 116 **self.feed_extra_kwargs(obj) 117 ) 118 119 title_tmp = None 120 if self.title_template is not None: 121 try: 122 title_tmp = loader.get_template(self.title_template) 123 except TemplateDoesNotExist: 124 pass 125 126 description_tmp = None 127 if self.description_template is not None: 128 try: 129 description_tmp = loader.get_template(self.description_template) 130 except TemplateDoesNotExist: 131 pass 132 133 for item in self.__get_dynamic_attr('items', obj): 134 if title_tmp is not None: 135 title = title_tmp.render(RequestContext(request, {'obj': item, 'site': current_site})) 136 else: 137 title = self.__get_dynamic_attr('item_title', item) 138 if description_tmp is not None: 139 description = description_tmp.render(RequestContext(request, {'obj': item, 'site': current_site})) 140 else: 141 description = self.__get_dynamic_attr('item_description', item) 142 link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item)) 143 enc = None 144 enc_url = self.__get_dynamic_attr('item_enclosure_url', item) 145 if enc_url: 146 enc = feedgenerator.Enclosure( 147 url = smart_unicode(enc_url), 148 length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)), 149 mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item)) 150 ) 151 author_name = self.__get_dynamic_attr('item_author_name', item) 152 if author_name is not None: 153 author_email = self.__get_dynamic_attr('item_author_email', item) 154 author_link = self.__get_dynamic_attr('item_author_link', item) 155 else: 156 author_email = author_link = None 157 158 pubdate = self.__get_dynamic_attr('item_pubdate', item) 159 if pubdate and not pubdate.tzinfo: 160 now = datetime.datetime.now() 161 utcnow = datetime.datetime.utcnow() 162 163 # Must always subtract smaller time from larger time here. 164 if utcnow > now: 165 sign = -1 166 tzDifference = (utcnow - now) 167 else: 168 sign = 1 169 tzDifference = (now - utcnow) 170 171 # Round the timezone offset to the nearest half hour. 172 tzOffsetMinutes = sign * ((tzDifference.seconds / 60 + 15) / 30) * 30 173 tzOffset = datetime.timedelta(minutes=tzOffsetMinutes) 174 pubdate = pubdate.replace(tzinfo=tzinfo.FixedOffset(tzOffset)) 175 176 feed.add_item( 177 title = title, 178 link = link, 179 description = description, 180 unique_id = self.__get_dynamic_attr('item_guid', item, link), 181 enclosure = enc, 182 pubdate = pubdate, 183 author_name = author_name, 184 author_email = author_email, 185 author_link = author_link, 186 categories = self.__get_dynamic_attr('item_categories', item), 187 item_copyright = self.__get_dynamic_attr('item_copyright', item), 188 **self.item_extra_kwargs(item) 189 ) 190 return feed 191 3 192 4 193 def feed(request, url, feed_dict=None): 194 """Provided for backwards compatibility.""" 195 import warnings 196 warnings.warn('The syndication feed() view is deprecated. Please use the ' 197 'new class based view API.', 198 category=PendingDeprecationWarning) 199 5 200 if not feed_dict: 6 201 raise Http404, "No feeds are registered." 7 202 … … def feed(request, url, feed_dict=None): 17 212 18 213 try: 19 214 feedgen = f(slug, request).get_feed(param) 20 except feeds.FeedDoesNotExist:215 except FeedDoesNotExist: 21 216 raise Http404, "Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug 22 217 23 218 response = HttpResponse(mimetype=feedgen.mime_type) 24 219 feedgen.write(response, 'utf-8') 25 220 return response 221 -
django/utils/feedgenerator.py
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index c9445f9..bb74a8e 100644
a b For definitions of the different versions of RSS, see: 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 b54ae6e..b9ccc72 100644
a b their deprecation, as per the :ref:`Django deprecation policy 50 50 backwards compatibility. These have been deprecated since the 1.2 51 51 release. 52 52 53 * The ``views.feed()`` view and ``feeds.Feed`` class in 54 ``django.contrib.syndication`` have been deprecated since the 1.2 55 release. The class-based view ``views.Feed`` should be used instead. 56 53 57 * 2.0 54 58 * ``django.views.defaults.shortcut()``. This function has been moved 55 59 to ``django.contrib.contenttypes.views.shortcut()`` as part of the -
docs/ref/contrib/syndication.txt
diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index cb9c22b..79088b0 100644
a b to generate feeds outside of a Web context, or in some other lower-level way. 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/0613/rss/` -- Returns recent crimes for beat 0613. 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>\d+)/rss/$', BeatFeed()), 212 172 213 An example makes this clear. Here's the code for these beat-specific feeds:: 173 Like a view, the arguments in the URL are passed to the :meth:`get_object()` 174 method along with the request object. Here's the code for these beat-specific 175 feeds:: 214 176 215 from django.contrib.syndication.feeds import FeedDoesNotExist 216 from django.core.exceptions import ObjectDoesNotExist 177 from django.contrib.syndication.views import FeedDoesNotExist 217 178 218 179 class BeatFeed(Feed): 219 def get_object(self, bits): 220 # In case of "/rss/beats/0613/foo/bar/baz/", or other such clutter, 221 # check that bits has only one member. 222 if len(bits) != 1: 223 raise ObjectDoesNotExist 224 return Beat.objects.get(beat__exact=bits[0]) 180 description_template = 'feeds/beat_description.html' 181 182 def get_object(self, request, beat): 183 return Beat.objects.get(beat__exact=beat) 225 184 226 185 def title(self, obj): 227 186 return "Chicagocrime.org: Crimes for beat %s" % obj.beat 228 187 229 188 def link(self, obj): 230 if not obj:231 raise FeedDoesNotExist232 189 return obj.get_absolute_url() 233 190 234 191 def description(self, obj): 235 192 return "Crimes recently reported in police beat %s" % obj.beat 236 193 237 194 def items(self, obj): 238 return Crime.objects.filter(beat__id__exact=obj.id).order_by('-crime_date')[:30] 239 240 Here's the basic algorithm the RSS framework follows, given this class and a 241 request to the URL :file:`/rss/beats/0613/`: 242 243 * The framework gets the URL :file:`/rss/beats/0613/` and notices there's an 244 extra bit of URL after the slug. It splits that remaining string by the 245 slash character (``"/"``) and calls the 246 :class:`~django.contrib.syndication.feeds.Feed` class' 247 :meth:`get_object()` method, passing it the bits. In this case, bits is 248 ``['0613']``. For a request to :file:`/rss/beats/0613/foo/bar/`, bits 249 would be ``['0613', 'foo', 'bar']``. 250 251 * :meth:`get_object()` is responsible for retrieving the given beat, from 252 the given ``bits``. In this case, it uses the Django database API to 253 retrieve the beat. Note that :meth:`get_object()` should raise 254 :exc:`django.core.exceptions.ObjectDoesNotExist` if given invalid 255 parameters. There's no ``try``/``except`` around the 256 ``Beat.objects.get()`` call, because it's not necessary; that function 257 raises :exc:`Beat.DoesNotExist` on failure, and :exc:`Beat.DoesNotExist` 258 is a subclass of :exc:`ObjectDoesNotExist`. Raising 259 :exc:`ObjectDoesNotExist` in :meth:`get_object()` tells Django to produce 260 a 404 error for that request. 261 262 .. versionadded:: 1.0 263 :meth:`get_object()` can handle the :file:`/rss/beats/` url. 264 265 The :meth:`get_object()` method also has a chance to handle the 266 :file:`/rss/beats/` url. In this case, :data:`bits` will be an 267 empty list. In our example, ``len(bits) != 1`` and an 268 :exc:`ObjectDoesNotExist` exception will be raised, so 269 :file:`/rss/beats/` will generate a 404 page. But you can handle this case 270 however you like. For example, you could generate a combined feed for all 271 beats. 272 273 * To generate the feed's ``<title>``, ``<link>`` and ``<description>``, 274 Django uses the :meth:`title()`, :meth:`link()` and :meth:`description()` 275 methods. In the previous example, they were simple string class 276 attributes, but this example illustrates that they can be either strings 277 *or* methods. For each of :attr:`title`, :attr:`link` and 278 :attr:`description`, Django follows this algorithm: 279 280 * First, it tries to call a method, passing the ``obj`` argument, where 281 ``obj`` is the object returned by :meth:`get_object()`. 282 283 * Failing that, it tries to call a method with no arguments. 284 285 * Failing that, it uses the class attribute. 286 287 Inside the :meth:`link()` method, we handle the possibility that ``obj`` 288 might be ``None``, which can occur when the URL isn't fully specified. In 289 some cases, you might want to do something else in this case, which would 290 mean you'd need to check for ``obj`` existing in other methods as well. 291 (The :meth:`link()` method is called very early in the feed generation 292 process, so it's a good place to bail out early.) 293 294 * Finally, note that :meth:`items()` in this example also takes the ``obj`` 295 argument. The algorithm for :attr:`items` is the same as described in the 296 previous step -- first, it tries :meth:`items(obj)`, then :meth:`items()`, 297 then finally an :attr:`items` class attribute (which should be a list). 195 return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30] 196 197 To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django 198 uses the :meth:`title()`, :meth:`link()` and :meth:`description()` methods. In 199 the previous example, they were simple string class attributes, but this example 200 illustrates that they can be either strings *or* methods. For each of 201 :attr:`title`, :attr:`link` and :attr:`description`, Django follows this 202 algorithm: 203 204 * First, it tries to call a method, passing the ``obj`` argument, where 205 ``obj`` is the object returned by :meth:`get_object()`. 206 207 * Failing that, it tries to call a method with no arguments. 208 209 * Failing that, it uses the class attribute. 210 211 Also note that :meth:`items()` also follows the same algorithm -- first, it 212 tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items` 213 class attribute (which should be a list). 214 215 We are using a template for the item descriptions. It can be very simple: 216 217 .. code-block:: html+django 218 219 {{ obj.description }} 220 221 However, you are free to add formatting as required. 298 222 299 223 The ``ExampleFeed`` class below gives full documentation on methods and 300 attributes of :class:`~django.contrib.syndication. feeds.Feed` classes.224 attributes of :class:`~django.contrib.syndication.views.Feed` classes. 301 225 302 226 Specifying the type of feed 303 227 --------------------------- … … Specifying the type of feed 305 229 By default, feeds produced in this framework use RSS 2.0. 306 230 307 231 To change that, add a ``feed_type`` attribute to your 308 :class:`~django.contrib.syndication. feeds.Feed` class, like so::232 :class:`~django.contrib.syndication.views.Feed` class, like so:: 309 233 310 234 from django.utils.feedgenerator import Atom1Feed 311 235 … … Publishing Atom and RSS feeds in tandem 353 277 354 278 Some developers like to make available both Atom *and* RSS versions of their 355 279 feeds. That's easy to do with Django: Just create a subclass of your 356 :class:`~django.contrib.syndication. feeds.Feed`280 :class:`~django.contrib.syndication.views.Feed` 357 281 class and set the :attr:`feed_type` to something different. Then update your 358 282 URLconf to add the extra versions. 359 283 360 284 Here's a full example:: 361 285 362 from django.contrib.syndication. feeds import Feed286 from django.contrib.syndication.views import Feed 363 287 from chicagocrime.models import NewsItem 364 288 from django.utils.feedgenerator import Atom1Feed 365 289 … … Here's a full example:: 381 305 a feed-level "description," but they *do* provide for a "subtitle." 382 306 383 307 If you provide a :attr:`description` in your 384 :class:`~django.contrib.syndication. feeds.Feed` class, Django will *not*308 :class:`~django.contrib.syndication.views.Feed` class, Django will *not* 385 309 automatically put that into the :attr:`subtitle` element, because a 386 310 subtitle and description are not necessarily the same thing. Instead, you 387 311 should define a :attr:`subtitle` attribute. … … And the accompanying URLconf:: 394 318 from django.conf.urls.defaults import * 395 319 from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed 396 320 397 feeds = {398 'rss': RssSiteNewsFeed,399 'atom': AtomSiteNewsFeed,400 }401 402 321 urlpatterns = patterns('', 403 322 # ... 404 (r'^ feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',405 {'feed_dict': feeds}),323 (r'^sitenews/rss/$', RssSiteNewsFeed()), 324 (r'^sitenews/atom/$', AtomSiteNewsFeed()), 406 325 # ... 407 326 ) 408 327 409 328 Feed class reference 410 329 -------------------- 411 330 412 .. class:: django.contrib.syndication. feeds.Feed331 .. class:: django.contrib.syndication.views.Feed 413 332 414 333 This example illustrates all possible attributes and methods for a 415 :class:`~django.contrib.syndication. feeds.Feed` class::334 :class:`~django.contrib.syndication.views.Feed` class:: 416 335 417 from django.contrib.syndication. feeds import Feed336 from django.contrib.syndication.views import Feed 418 337 from django.utils import feedgenerator 419 338 420 339 class ExampleFeed(Feed): … … This example illustrates all possible attributes and methods for a 430 349 # TEMPLATE NAMES -- Optional. These should be strings representing 431 350 # names of Django templates that the system should use in rendering the 432 351 # title and description of your feed items. Both are optional. 433 # If you don't specify one, or either, Django will use the template 434 # 'feeds/SLUG_title.html' and 'feeds/SLUG_description.html', where SLUG 435 # is the slug you specify in the URL. 352 # If one is not specified, the item_title() or item_description() 353 # methods are used instead. 436 354 437 355 title_template = None 438 356 description_template = None … … This example illustrates all possible attributes and methods for a 572 490 # COPYRIGHT NOTICE -- One of the following three is optional. The 573 491 # framework looks for them in this order. 574 492 575 def copyright(self, obj):493 def feed_copyright(self, obj): 576 494 """ 577 495 Takes the object returned by get_object() and returns the feed's 578 496 copyright notice as a normal Python string. 579 497 """ 580 498 581 def copyright(self):499 def feed_copyright(self): 582 500 """ 583 501 Returns the feed's copyright notice as a normal Python string. 584 502 """ 585 503 586 copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.504 feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice. 587 505 588 506 # TTL -- One of the following three is optional. The framework looks 589 507 # for them in this order. Ignored for Atom feeds. … … This example illustrates all possible attributes and methods for a 620 538 # GET_OBJECT -- This is required for feeds that publish different data 621 539 # for different URL parameters. (See "A complex example" above.) 622 540 623 def get_object(self, bits):541 def get_object(self, request, *args, **kwargs): 624 542 """ 625 Takes a list of strings gleaned from the URL and returns an object626 re presented by this feed. Raises543 Takes the current request and the arguments from the URL, and 544 returns an object represented by this feed. Raises 627 545 django.core.exceptions.ObjectDoesNotExist on error. 628 546 """ 547 548 # ITEM TITLE AND DESCRIPTION -- If title_template or 549 # description_template are not defined, these are used instead. Both are 550 # optional, by default they will use the unicode representation of the 551 # item. 552 553 def item_title(self, item): 554 """ 555 Takes an item, as returned by items(), and returns the item's 556 title as a normal Python string. 557 """ 558 559 def item_title(self): 560 """ 561 Returns the title for every item in the feed. 562 """ 563 564 item_title = 'Breaking News: Nothing Happening' # Hard-coded title. 565 566 def item_description(self, item): 567 """ 568 Takes an item, as returned by items(), and returns the item's 569 description as a normal Python string. 570 """ 571 572 def item_description(self): 573 """ 574 Returns the description for every item in the feed. 575 """ 629 576 577 item_description = 'A description of the item.' # Hard-coded description. 578 630 579 # ITEM LINK -- One of these three is required. The framework looks for 631 580 # them in this order. 632 581 … … This example illustrates all possible attributes and methods for a 686 635 687 636 item_author_email = 'test@example.com' # Hard-coded author e-mail. 688 637 689 # ITEM AUTHOR LINK -- One of the following three is optional. The638 # ITEM AUTHOR LINK -- One of the following three is optional. The 690 639 # framework looks for them in this order. In each case, the URL should 691 640 # include the "http://" and domain name. 692 641 # -
docs/releases/1.2.txt
diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index 9e8816e..2ff78bd 100644
a b For more information, see the full 176 176 :ref:`messages documentation <ref-contrib-messages>`. You should begin to 177 177 update your code to use the new API immediately. 178 178 179 ``django.contrib.syndication.feeds.Feed`` 180 ----------------------------------------- 181 182 The syndication ``Feed`` class has been replaced by 183 ``django.contrib.syndication.views.Feed``, and will be removed in Django 1.4. 184 185 The new class has an almost identical API, but allows instances to be used as 186 views. For example, consider the feeds using the old framework in the following 187 :ref:`URLconf <topics-http-urls>`:: 188 189 from django.conf.urls.defaults import * 190 from myproject.feeds import LatestEntries, LatestEntriesByCategory 191 192 feeds = { 193 'latest': LatestEntries, 194 'categories': LatestEntriesByCategory, 195 } 196 197 urlpatterns = patterns('', 198 # ... 199 (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', 200 {'feed_dict': feeds}), 201 # ... 202 ) 203 204 This should be replaced by:: 205 206 from django.conf.urls.defaults import * 207 from myproject.feeds import LatestEntries, LatestEntriesByCategory 208 209 urlpatterns = patterns('', 210 # ... 211 (r'^feeds/latest/$', LatestEntries()), 212 (r'^feeds/categories/(?P<category_id>\d+)/$', LatestEntriesByCategory()), 213 # ... 214 ) 215 216 The ``LatestEntries`` class would be identical, apart from subclassing from the 217 new ``Feed`` class. However, ``LatestEntriesByCategory`` used the 218 ``get_object()`` method with the ``bits`` argument to specify a specific 219 category to show. Much like a view, the ``get_object()`` method now takes a 220 ``request`` and arguments from the URL, so it would look like this:: 221 222 from django.contrib.syndication.views import Feed 223 from django.shortcuts import get_object_or_404 224 from myproject.models import Category 225 226 class LatestEntriesByCategory(Feed): 227 def get_object(self, request, category_id): 228 return get_object_or_404(Category, id=category_id) 229 230 # ... 231 232 For more information, see the full :ref:`syndication framework documentation 233 <ref-contrib-syndication>`. 234 179 235 What's new in Django 1.2 180 236 ======================== 181 237 … … alternative to the normal primary-key based object references in a 277 333 fixture, improving readability, and resolving problems referring to 278 334 objects whose primary key value may not be predictable or known. 279 335 336 Syndication feeds as views 337 -------------------------- 338 339 :ref:`Syndication feeds <ref-contrib-syndication>` can now be used directly as 340 views in your :ref:`URLconf <topics-http-urls>`. Previously, all the feeds on a 341 site had to live beneath a single URL, which produced ugly, unnatural URLs. 342 343 For example, suppose you listed blog posts for the "django" tag under 344 ``/blog/django/``. In previous versions of Django, a feed for those posts would 345 have had to be something like ``/feeds/blog-tag/django/``. In Django 1.2, it 346 could simply be ``/blog/django/feed/``. 347 348 Like any other view, feeds are now passed a ``request`` object, so you can 349 do user based access control amongst other things. 350 351 -
tests/regressiontests/syndication/feeds.py
diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py index 79837f9..8648cec 100644
a b 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..fdc373d 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 42 ] 43 No newline at end of file -
tests/regressiontests/syndication/models.py
diff --git a/tests/regressiontests/syndication/models.py b/tests/regressiontests/syndication/models.py index 99e14ad..b6cf9e5 100644
a b 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 10 def __unicode__(self): 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 7 21 def __unicode__(self): 8 return self.title 9 No newline at end of file 22 return self.title -
new file tests/regressiontests/syndication/templates/syndication/description.html
diff --git a/tests/regressiontests/syndication/templates/syndication/description.html b/tests/regressiontests/syndication/templates/syndication/description.html new file mode 100644 index 0000000..85ec82c
- + 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..1861e88 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 'http://example.commailto:uhoh%40djangoproject.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..0e50118
- + 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 )