Ticket #12403: syndication-views-1.diff
File syndication-views-1.diff, 66.1 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..996f981 100644
a b 1 from datetime import datetime, timedelta 1 from django.contrib.syndication import views 2 from django.core.exceptions import ObjectDoesNotExist 2 3 3 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist 4 from django.template import loader, Template, TemplateDoesNotExist 5 from django.contrib.sites.models import Site, RequestSite 6 from django.utils import feedgenerator 7 from django.utils.tzinfo import FixedOffset 8 from django.utils.encoding import smart_unicode, iri_to_uri 9 from django.conf import settings 10 from django.template import RequestContext 11 12 def add_domain(domain, url): 13 if not (url.startswith('http://') or url.startswith('https://')): 14 # 'url' must already be ASCII and URL-quoted, so no need for encoding 15 # conversions here. 16 url = iri_to_uri(u'http://%s%s' % (domain, url)) 17 return url 18 19 class FeedDoesNotExist(ObjectDoesNotExist): 20 pass 21 22 class Feed(object): 23 item_pubdate = None 24 item_enclosure_url = None 25 feed_type = feedgenerator.DefaultFeed 26 feed_url = None 27 title_template = None 28 description_template = None 4 # This is part of the depreciated API 5 from django.contrib.syndication.views import FeedDoesNotExist 29 6 7 class Feed(views.Feed): 8 """Provided for backwards compatibility.""" 30 9 def __init__(self, slug, request): 31 10 self.slug = slug 32 11 self.request = request 33 self.feed_url = self.feed_url or request.path 34 self.title_template_name = self.title_template or ('feeds/%s_title.html' % slug) 35 self.description_template_name = self.description_template or ('feeds/%s_description.html' % slug) 36 37 def item_link(self, item): 38 try: 39 return item.get_absolute_url() 40 except AttributeError: 41 raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__ 42 43 def __get_dynamic_attr(self, attname, obj, default=None): 44 try: 45 attr = getattr(self, attname) 46 except AttributeError: 47 return default 48 if callable(attr): 49 # Check func_code.co_argcount rather than try/excepting the 50 # function and catching the TypeError, because something inside 51 # the function may raise the TypeError. This technique is more 52 # accurate. 53 if hasattr(attr, 'func_code'): 54 argcount = attr.func_code.co_argcount 55 else: 56 argcount = attr.__call__.func_code.co_argcount 57 if argcount == 2: # one argument is 'self' 58 return attr(obj) 59 else: 60 return attr() 61 return attr 62 63 def feed_extra_kwargs(self, obj): 64 """ 65 Returns an extra keyword arguments dictionary that is used when 66 initializing the feed generator. 67 """ 68 return {} 69 70 def item_extra_kwargs(self, item): 71 """ 72 Returns an extra keyword arguments dictionary that is used with 73 the `add_item` call of the feed generator. 74 """ 75 return {} 12 self.feed_url = getattr(self, 'feed_url', None) or request.path 13 self.title_template = self.title_template or ('feeds/%s_title.html' % slug) 14 self.description_template = self.description_template or ('feeds/%s_description.html' % slug) 76 15 77 16 def get_object(self, bits): 78 17 return None 79 18 80 19 def get_feed(self, url=None): 81 20 """ 82 21 Returns a feedgenerator.DefaultFeed object, fully populated, for … … class Feed(object): 86 25 bits = url.split('/') 87 26 else: 88 27 bits = [] 89 90 28 try: 91 29 obj = self.get_object(bits) 92 30 except ObjectDoesNotExist: 93 31 raise FeedDoesNotExist 32 return super(Feed, self).get_feed(obj, self.request) 94 33 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..7231557 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.""" 5 195 if not feed_dict: 6 196 raise Http404, "No feeds are registered." 7 197 … … def feed(request, url, feed_dict=None): 17 207 18 208 try: 19 209 feedgen = f(slug, request).get_feed(param) 20 except feeds.FeedDoesNotExist:210 except FeedDoesNotExist: 21 211 raise Http404, "Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug 22 212 23 213 response = HttpResponse(mimetype=feedgen.mime_type) 24 214 feedgen.write(response, 'utf-8') 25 215 return response 216 -
django/utils/feedgenerator.py
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index c9445f9..bb74a8e 100644
a b For definitions of the different versions of RSS, see: 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/ref/contrib/syndication.txt
diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index cb9c22b..149f2ab 100644
a b The high-level framework 26 26 Overview 27 27 -------- 28 28 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. 29 The high-level feed-generating framework is supplied by the 30 :class:`~django.contrib.syndication.views.Feed` class. To create a feed, just write a :class:`~django.contrib.syndication.views.Feed` 31 class and point to an instance of it in your :ref:`URLconf <topics-http-urls>`. 81 32 82 33 Feed classes 83 34 ------------ 84 35 85 A :class:`~django.contrib.syndication. feeds.Feed` class is a simplePython class36 A :class:`~django.contrib.syndication.views.Feed` class is a Python class 86 37 that represents a syndication feed. A feed can be simple (e.g., a "site news" 87 38 feed, or a basic feed displaying the latest entries of a blog) or more complex 88 39 (e.g., a feed displaying all the blog entries in a particular category, where 89 40 the category is variable). 90 41 91 :class:`~django.contrib.syndication. feeds.Feed` classes mustsubclass92 ``django.contrib.syndication. feeds.Feed``. They can live anywhere in your42 :class:`~django.contrib.syndication.views.Feed` classes subclass 43 ``django.contrib.syndication.views.Feed``. They can live anywhere in your 93 44 codebase. 94 45 46 Instances of :class:`~django.contrib.syndication.views.Feed` classes are views 47 which can be used in your :ref:`URLconf <topics-http-urls>`. 48 95 49 A simple example 96 50 ---------------- 97 51 98 52 This simple example, taken from `chicagocrime.org`_, describes a feed of the 99 53 latest five news items:: 100 54 101 from django.contrib.syndication. feeds import Feed55 from django.contrib.syndication.views import Feed 102 56 from chicagocrime.models import NewsItem 103 57 104 class LatestEntries (Feed):58 class LatestEntriesFeed(Feed): 105 59 title = "Chicagocrime.org site news" 106 60 link = "/sitenews/" 107 61 description = "Updates on changes and additions to chicagocrime.org." 108 62 109 63 def items(self): 110 64 return NewsItem.objects.order_by('-pub_date')[:5] 65 66 def item_title(self, item): 67 return item.title 68 69 def item_description(self, item): 70 return item.description 71 72 To connect a URL to this feed, it needs to be put in your :ref:`URLconf <topics-http-urls>`. Here is a full example:: 73 74 from django.conf.urls.defaults import * 75 from myproject.feeds import LatestEntriesFeed 76 77 urlpatterns = patterns('', 78 # ... 79 (r'^latest/feed/$', LatestEntriesFeed()), 80 # ... 81 ) 111 82 112 83 Note: 113 84 114 * The class subclasses ``django.contrib.syndication. feeds.Feed``.85 * The class subclasses ``django.contrib.syndication.views.Feed``. 115 86 116 87 * :attr:`title`, :attr:`link` and :attr:`description` correspond to the 117 88 standard RSS ``<title>``, ``<link>`` and ``<description>`` elements, … … One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``, 133 104 ``<link>`` and ``<description>``. We need to tell the framework what data to put 134 105 into those elements. 135 106 136 * To specify the contents of ``<title>`` and ``<description>``, create 137 :ref:`Django templates <topics-templates>` called 138 :file:`feeds/latest_title.html` and 139 :file:`feeds/latest_description.html`, where :attr:`latest` is the 140 :attr:`slug` specified in the URLconf for the given feed. Note the 141 ``.html`` extension is required. The RSS system renders that template for 142 each item, passing it two template context variables: 107 * For the contents of ``<title>`` and ``<description>``, Django tries 108 calling the methods :meth:`item_title()` and :meth:`item_description()` on 109 the :class:`~django.contrib.syndication.views.Feed` class. They are passed 110 a single parameter, :attr:`item`, which is the object itself. These are 111 optional; by default, the unicode representation of the object is used for 112 both. 113 114 If you want to do any special formatting for either the title or 115 description, :ref:`Django templates <topics-templates>` can be used 116 instead. Their paths can be specified with the ``title_template`` and 117 ``description_template`` attributes on the 118 :class:`~django.contrib.syndication.views.Feed` class. The templates are 119 rendered for each item and are passed two template context variables: 143 120 144 121 * ``{{ obj }}`` -- The current object (one of whichever objects you 145 122 returned in :meth:`items()`). … … into those elements. 152 129 :ref:`RequestSite section of the sites framework documentation 153 130 <requestsite-objects>` for more. 154 131 155 If you don't create a template for either the title or description, the 156 framework will use the template ``"{{ obj }}"`` by default -- that is, the 157 normal string representation of the object. You can also change the names 158 of these two templates by specifying ``title_template`` and 159 ``description_template`` as attributes of your 160 :class:`~django.contrib.syndication.feeds.Feed` class. 132 See `a complex example`_ below that uses a description template. 161 133 162 134 * To specify the contents of ``<link>``, you have two options. For each item 163 in :meth:`items()`, Django first tries calling a method 164 :meth:`item_link()` in the :class:`~django.contrib.syndication.feeds.Feed` 165 class, passing it a single parameter, :attr:`item`, which is the object 166 itself. If that method doesn't exist, Django tries executing a 167 ``get_absolute_url()`` method on that object. . Both 168 ``get_absolute_url()`` and :meth:`item_link()` should return the item's 169 URL as a normal Python string. As with ``get_absolute_url()``, the result 170 of :meth:`item_link()` will be included directly in the URL, so you are 171 responsible for doing all necessary URL quoting and conversion to ASCII 172 inside the method itself. 173 174 * For the LatestEntries example above, we could have very simple feed 175 templates: 176 177 * latest_title.html: 178 179 .. code-block:: html+django 180 181 {{ obj.title }} 182 183 * latest_description.html: 184 185 .. code-block:: html+django 186 187 {{ obj.description }} 135 in :meth:`items()`, Django first tries calling the 136 :meth:`item_link()` method on the 137 :class:`~django.contrib.syndication.views.Feed` class. In a similar way to 138 the title and description, it is passed it a single parameter, 139 :attr:`item`. If that method doesn't exist, Django tries executing a 140 ``get_absolute_url()`` method on that object. Both 141 :meth:`get_absolute_url()` and :meth:`item_link()` should return the 142 item's URL as a normal Python string. As with ``get_absolute_url()``, the 143 result of :meth:`item_link()` will be included directly in the URL, so you 144 are responsible for doing all necessary URL quoting and conversion to 145 ASCII inside the method itself. 188 146 189 147 .. _chicagocrime.org: http://www.chicagocrime.org/ 190 148 191 149 A complex example 192 150 ----------------- 193 151 194 The framework also supports more complex feeds, via parameters.152 The framework also supports more complex feeds, via arguments. 195 153 196 154 For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every 197 155 police beat in Chicago. It'd be silly to create a separate 198 :class:`~django.contrib.syndication. feeds.Feed` class for each police beat; that156 :class:`~django.contrib.syndication.views.Feed` class for each police beat; that 199 157 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. 158 programming logic. Instead, the syndication framework lets you access the 159 arguments passed from your :ref:`URLconf <topics-http-urls>` so feeds can output 160 items based on information in the feed's URL. 202 161 203 162 On chicagocrime.org, the police-beat feeds are accessible via URLs like this: 204 163 205 * :file:`/rss/beats/0613/` -- Returns recent crimes for beat 0613. 206 * :file:`/rss/beats/1424/` -- Returns recent crimes for beat 1424. 164 * :file:`/beats/0613/rss/` -- Returns recent crimes for beat 0613. 165 * :file:`/beats/1424/rss/` -- Returns recent crimes for beat 1424. 166 167 These can be matched with a :ref:`URLconf <topics-http-urls>` line such as:: 207 168 208 The slug here is ``"beats"``. The syndication framework sees the extra URL bits 209 after the slug -- ``0613`` and ``1424`` -- and gives you a hook to tell it what 210 those URL bits mean, and how they should influence which items get published in 211 the feed. 169 (r'^beats/(?P<beat>\d+)/rss/$', BeatFeed()), 212 170 213 An example makes this clear. Here's the code for these beat-specific feeds:: 171 Like a view, the arguments in the URL are passed to the :meth:`get_object()` 172 method along with the request object. Here's the code for these beat-specific 173 feeds:: 214 174 215 from django.contrib.syndication.feeds import FeedDoesNotExist 216 from django.core.exceptions import ObjectDoesNotExist 175 from django.contrib.syndication.views import FeedDoesNotExist 217 176 218 177 class BeatFeed(Feed): 219 def get_object(self, bits): 220 # In case of "/rss/beats/0613/foo/bar/baz/", or other such clutter, 221 # check that bits has only one member. 222 if len(bits) != 1: 223 raise ObjectDoesNotExist 224 return Beat.objects.get(beat__exact=bits[0]) 178 description_template = 'feeds/beat_description.html' 179 180 def get_object(self, request, beat): 181 return Beat.objects.get(beat__exact=beat) 225 182 226 183 def title(self, obj): 227 184 return "Chicagocrime.org: Crimes for beat %s" % obj.beat 228 185 229 186 def link(self, obj): 230 if not obj:231 raise FeedDoesNotExist232 187 return obj.get_absolute_url() 233 188 234 189 def description(self, obj): 235 190 return "Crimes recently reported in police beat %s" % obj.beat 236 191 237 192 def items(self, obj): 238 return Crime.objects.filter(beat__id__exact=obj.id).order_by('-crime_date')[:30] 239 240 Here's the basic algorithm the RSS framework follows, given this class and a 241 request to the URL :file:`/rss/beats/0613/`: 242 243 * The framework gets the URL :file:`/rss/beats/0613/` and notices there's an 244 extra bit of URL after the slug. It splits that remaining string by the 245 slash character (``"/"``) and calls the 246 :class:`~django.contrib.syndication.feeds.Feed` class' 247 :meth:`get_object()` method, passing it the bits. In this case, bits is 248 ``['0613']``. For a request to :file:`/rss/beats/0613/foo/bar/`, bits 249 would be ``['0613', 'foo', 'bar']``. 250 251 * :meth:`get_object()` is responsible for retrieving the given beat, from 252 the given ``bits``. In this case, it uses the Django database API to 253 retrieve the beat. Note that :meth:`get_object()` should raise 254 :exc:`django.core.exceptions.ObjectDoesNotExist` if given invalid 255 parameters. There's no ``try``/``except`` around the 256 ``Beat.objects.get()`` call, because it's not necessary; that function 257 raises :exc:`Beat.DoesNotExist` on failure, and :exc:`Beat.DoesNotExist` 258 is a subclass of :exc:`ObjectDoesNotExist`. Raising 259 :exc:`ObjectDoesNotExist` in :meth:`get_object()` tells Django to produce 260 a 404 error for that request. 261 262 .. versionadded:: 1.0 263 :meth:`get_object()` can handle the :file:`/rss/beats/` url. 264 265 The :meth:`get_object()` method also has a chance to handle the 266 :file:`/rss/beats/` url. In this case, :data:`bits` will be an 267 empty list. In our example, ``len(bits) != 1`` and an 268 :exc:`ObjectDoesNotExist` exception will be raised, so 269 :file:`/rss/beats/` will generate a 404 page. But you can handle this case 270 however you like. For example, you could generate a combined feed for all 271 beats. 272 273 * To generate the feed's ``<title>``, ``<link>`` and ``<description>``, 274 Django uses the :meth:`title()`, :meth:`link()` and :meth:`description()` 275 methods. In the previous example, they were simple string class 276 attributes, but this example illustrates that they can be either strings 277 *or* methods. For each of :attr:`title`, :attr:`link` and 278 :attr:`description`, Django follows this algorithm: 279 280 * First, it tries to call a method, passing the ``obj`` argument, where 281 ``obj`` is the object returned by :meth:`get_object()`. 282 283 * Failing that, it tries to call a method with no arguments. 284 285 * Failing that, it uses the class attribute. 286 287 Inside the :meth:`link()` method, we handle the possibility that ``obj`` 288 might be ``None``, which can occur when the URL isn't fully specified. In 289 some cases, you might want to do something else in this case, which would 290 mean you'd need to check for ``obj`` existing in other methods as well. 291 (The :meth:`link()` method is called very early in the feed generation 292 process, so it's a good place to bail out early.) 293 294 * Finally, note that :meth:`items()` in this example also takes the ``obj`` 295 argument. The algorithm for :attr:`items` is the same as described in the 296 previous step -- first, it tries :meth:`items(obj)`, then :meth:`items()`, 297 then finally an :attr:`items` class attribute (which should be a list). 193 return Crime.objects.filter(beat=obj).order_by('-crime_date')[:30] 194 195 To generate the feed's ``<title>``, ``<link>`` and ``<description>``, Django 196 uses the :meth:`title()`, :meth:`link()` and :meth:`description()` methods. In 197 the previous example, they were simple string class attributes, but this example 198 illustrates that they can be either strings *or* methods. For each of 199 :attr:`title`, :attr:`link` and :attr:`description`, Django follows this 200 algorithm: 201 202 * First, it tries to call a method, passing the ``obj`` argument, where 203 ``obj`` is the object returned by :meth:`get_object()`. 204 205 * Failing that, it tries to call a method with no arguments. 206 207 * Failing that, it uses the class attribute. 208 209 Also note that :meth:`items()` also follows the same algorithm -- first, it 210 tries :meth:`items(obj)`, then :meth:`items()`, then finally an :attr:`items` 211 class attribute (which should be a list). 212 213 We are using a template for the item descriptions. It can be very simple: 214 215 .. code-block:: html+django 216 217 {{ obj.description }} 218 219 However, you are free to add formatting as required. 298 220 299 221 The ``ExampleFeed`` class below gives full documentation on methods and 300 attributes of :class:`~django.contrib.syndication. feeds.Feed` classes.222 attributes of :class:`~django.contrib.syndication.views.Feed` classes. 301 223 302 224 Specifying the type of feed 303 225 --------------------------- … … Specifying the type of feed 305 227 By default, feeds produced in this framework use RSS 2.0. 306 228 307 229 To change that, add a ``feed_type`` attribute to your 308 :class:`~django.contrib.syndication. feeds.Feed` class, like so::230 :class:`~django.contrib.syndication.views.Feed` class, like so:: 309 231 310 232 from django.utils.feedgenerator import Atom1Feed 311 233 … … Publishing Atom and RSS feeds in tandem 353 275 354 276 Some developers like to make available both Atom *and* RSS versions of their 355 277 feeds. That's easy to do with Django: Just create a subclass of your 356 :class:`~django.contrib.syndication. feeds.Feed`278 :class:`~django.contrib.syndication.views.Feed` 357 279 class and set the :attr:`feed_type` to something different. Then update your 358 280 URLconf to add the extra versions. 359 281 360 282 Here's a full example:: 361 283 362 from django.contrib.syndication. feeds import Feed284 from django.contrib.syndication.views import Feed 363 285 from chicagocrime.models import NewsItem 364 286 from django.utils.feedgenerator import Atom1Feed 365 287 … … Here's a full example:: 381 303 a feed-level "description," but they *do* provide for a "subtitle." 382 304 383 305 If you provide a :attr:`description` in your 384 :class:`~django.contrib.syndication. feeds.Feed` class, Django will *not*306 :class:`~django.contrib.syndication.views.Feed` class, Django will *not* 385 307 automatically put that into the :attr:`subtitle` element, because a 386 308 subtitle and description are not necessarily the same thing. Instead, you 387 309 should define a :attr:`subtitle` attribute. … … And the accompanying URLconf:: 394 316 from django.conf.urls.defaults import * 395 317 from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed 396 318 397 feeds = {398 'rss': RssSiteNewsFeed,399 'atom': AtomSiteNewsFeed,400 }401 402 319 urlpatterns = patterns('', 403 320 # ... 404 (r'^ feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',405 {'feed_dict': feeds}),321 (r'^sitenews/rss/$', RssSiteNewsFeed()), 322 (r'^sitenews/atom/$', AtomSiteNewsFeed()), 406 323 # ... 407 324 ) 408 325 409 326 Feed class reference 410 327 -------------------- 411 328 412 .. class:: django.contrib.syndication. feeds.Feed329 .. class:: django.contrib.syndication.views.Feed 413 330 414 331 This example illustrates all possible attributes and methods for a 415 :class:`~django.contrib.syndication. feeds.Feed` class::332 :class:`~django.contrib.syndication.views.Feed` class:: 416 333 417 from django.contrib.syndication. feeds import Feed334 from django.contrib.syndication.views import Feed 418 335 from django.utils import feedgenerator 419 336 420 337 class ExampleFeed(Feed): … … This example illustrates all possible attributes and methods for a 430 347 # TEMPLATE NAMES -- Optional. These should be strings representing 431 348 # names of Django templates that the system should use in rendering the 432 349 # title and description of your feed items. Both are optional. 433 # If you don't specify one, or either, Django will use the template 434 # 'feeds/SLUG_title.html' and 'feeds/SLUG_description.html', where SLUG 435 # is the slug you specify in the URL. 350 # If one is not specified, the item_title() or item_description() 351 # methods are used instead. 436 352 437 353 title_template = None 438 354 description_template = None … … This example illustrates all possible attributes and methods for a 572 488 # COPYRIGHT NOTICE -- One of the following three is optional. The 573 489 # framework looks for them in this order. 574 490 575 def copyright(self, obj):491 def feed_copyright(self, obj): 576 492 """ 577 493 Takes the object returned by get_object() and returns the feed's 578 494 copyright notice as a normal Python string. 579 495 """ 580 496 581 def copyright(self):497 def feed_copyright(self): 582 498 """ 583 499 Returns the feed's copyright notice as a normal Python string. 584 500 """ 585 501 586 copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice.502 feed_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice. 587 503 588 504 # TTL -- One of the following three is optional. The framework looks 589 505 # for them in this order. Ignored for Atom feeds. … … This example illustrates all possible attributes and methods for a 620 536 # GET_OBJECT -- This is required for feeds that publish different data 621 537 # for different URL parameters. (See "A complex example" above.) 622 538 623 def get_object(self, bits):539 def get_object(self, request, *args, **kwargs): 624 540 """ 625 Takes a list of strings gleaned from the URL and returns an object626 re presented by this feed. Raises541 Takes the current request and the arguments from the URL, and 542 returns an object represented by this feed. Raises 627 543 django.core.exceptions.ObjectDoesNotExist on error. 628 544 """ 545 546 # ITEM TITLE AND DESCRIPTION -- If title_template or 547 # description_template are not defined, these are used instead. Both are 548 # optional, by default they will use the unicode representation of the 549 # item. 550 551 def item_title(self, item): 552 """ 553 Takes an item, as returned by items(), and returns the item's 554 title as a normal Python string. 555 """ 556 557 def item_title(self): 558 """ 559 Returns the title for every item in the feed. 560 """ 561 562 item_title = 'Breaking News: Nothing Happening' # Hard-coded title. 563 564 def item_description(self, item): 565 """ 566 Takes an item, as returned by items(), and returns the item's 567 description as a normal Python string. 568 """ 569 570 def item_description(self): 571 """ 572 Returns the description for every item in the feed. 573 """ 629 574 575 item_description = 'A description of the item.' # Hard-coded description. 576 630 577 # ITEM LINK -- One of these three is required. The framework looks for 631 578 # them in this order. 632 579 … … This example illustrates all possible attributes and methods for a 686 633 687 634 item_author_email = 'test@example.com' # Hard-coded author e-mail. 688 635 689 # ITEM AUTHOR LINK -- One of the following three is optional. The636 # ITEM AUTHOR LINK -- One of the following three is optional. The 690 637 # framework looks for them in this order. In each case, the URL should 691 638 # include the "http://" and domain name. 692 639 # -
tests/regressiontests/syndication/feeds.py
diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py index 79837f9..95924cd 100644
a b 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 DepreciatedComplexFeed(feeds.Feed): 127 def get_object(self, bits): 128 if len(bits) != 1: 129 raise ObjectDoesNotExist 130 return None 131 132 133 class DepreciatedRssFeed(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 -
tests/regressiontests/syndication/tests.py
diff --git a/tests/regressiontests/syndication/tests.py b/tests/regressiontests/syndication/tests.py index 816cb44..3d641fd 100644
a b 1 # -*- coding: utf-8 -*-2 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 # Depreciated feeds 274 ###################################### 275 276 class DepreciatedSyndicationFeedTest(FeedTestCase): 277 """ 278 Tests for the depreciated API (feed() view and the feed_dict etc). 279 """ 280 281 def test_empty_feed_dict(self): 282 """ 283 Test that an empty feed_dict raises a 404. 284 """ 285 response = self.client.get('/syndication/depr-feeds-empty/aware-dates/') 286 self.assertEquals(response.status_code, 404) 287 288 def test_nonexistent_slug(self): 289 """ 290 Test that a non-existent slug raises a 404. 291 """ 292 response = self.client.get('/syndication/depr-feeds/foobar/') 293 self.assertEquals(response.status_code, 404) 294 295 def test_rss_feed(self): 296 """ 297 A simple test for Rss201rev2Feed feeds generated by the depreciated 298 system. 299 """ 300 response = self.client.get('/syndication/depr-feeds/rss/') 301 doc = minidom.parseString(response.content) 302 feed = doc.getElementsByTagName('rss')[0] 303 self.assertEqual(feed.getAttribute('version'), '2.0') 304 305 chan = feed.getElementsByTagName('channel')[0] 306 self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link']) 307 308 items = chan.getElementsByTagName('item') 309 self.assertEqual(len(items), Entry.objects.count()) 310 311 def test_complex_base_url(self): 312 """ 313 Tests that the base url for a complex feed doesn't raise a 500 314 exception. 315 """ 316 response = self.client.get('/syndication/depr-feeds/complex/') 317 self.assertEquals(response.status_code, 404) 318 -
tests/regressiontests/syndication/urls.py
diff --git a/tests/regressiontests/syndication/urls.py b/tests/regressiontests/syndication/urls.py index ec45026..d1017fc 100644
a b 1 import feeds 2 from django.conf.urls.defaults import patterns 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.DepreciatedComplexFeed, 7 'rss': feeds.DepreciatedRssFeed, 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 )