1
2 """TurboFeed controllers for RSS/Atom feeds handling."""
3 __docformat__ = 'restructuredtext'
4
5 __all__ = [
6 'FeedController',
7 ]
8
9 import logging
10 from functools import partial
11
12 import turbogears as tg
13
14 from release import version as __version__
15 from util import xml_stylesheet, absolute_url
16
17
18 log = logging.getLogger('turbofeeds.controllers')
22 """Controller for generating feeds in multiple formats.
23
24 Must be subclassed and a ``get_feed_data`` method provided that returns
25 a dict with the feed info (see constructor doc strings) and an ``entries``
26 member, which should be a list filled with dicts each representing a feed
27 entry (see docstring for the default implementation).
28
29 """
30 formats = ["atom1.0", "atom0.3", "rss2.0", "mrss1.1.1"]
31
32
33 - def __init__(self, default="atom1.0", base_url='/feed', **feed_params):
34 """Constructor - Should be called with ``super()`` if overwritten.
35
36 The ``default`` arguments sets the default feed format when the
37 feed base URL is requested. Currently supported values are
38 ``atom1_0`` (the default), ``atom0_3``, ``rss2_0`` and ``mrss1_1_1``.
39
40 The ``base_url`` sets the URL where the feed controller is mounted
41 to the CherryPy object tree. The default is '/feed'. This is used
42 to construct the full feed URL for the ``link`` element in the feed,
43 if it is not overwritten by the ``link`` keyword argument.
44
45 Any extra keyword arguments will be assigned to the ``feed_params``
46 attribute and added to the feed data everytime a feed is requested.
47 This can be used to set the feed info in the direct child elements
48 of the ``feed`` (Atom) resp. ``channel`` (RSS) root element of the
49 feed. Possible names include:
50
51 * author (dict with ``name``, ``email`` and ``uri`` members)
52 * categories (list of strings, used by Atom 1.0 / RSS only)
53 * generator (string)
54 * updated (datetime)
55 * icon (URL, used by Atom 1.0 format only)
56 * id (string/URL, used by Atom formats only)
57 * logo (URL, used by Atom 1.0 / RSS format only)
58 * lang (string, used by RSS format only)
59 * link (URL)
60 * rights (string)
61 * subtitle (string)
62 * stylesheet (URL or dict with members ``href`` and ``type`` or a
63 callable returning either, used to place an appropriate
64 xml-stylesheet processing instruction at the top of the feed XML.
65 The stylesheet function will receive the format of the feed as the
66 first argument and all extra keyword arguments to this constructor
67 as keword arguments as well.)
68 * title (string)
69
70 For up-to-date information about supported elements and values, please
71 refer to the templates for the different feed formats in the
72 ``templates`` sub-package.
73
74 """
75 super(FeedController, self).__init__()
76 if not default in self.formats:
77 raise ValueError("Default format '%s' is not supported" % default)
78 self.default = default
79 self.base_url = base_url
80 self.feed_params = feed_params
81 log.info("TurboFeeds FeedController (%s) initialised", __version__)
82 log.debug("FeedController base_url: %s", base_url)
83
84
85 @tg.expose()
86 - def index(self, *args, **kwargs):
87 """Redirects to the default feed format rendering method."""
88
89 tg.redirect(self.default, kwargs)
90
91 @tg.expose(template="turbofeeds.templates.atom0_3",
92 format="xml", content_type="application/atom+xml")
93 - def atom0_3(self, *args, **kwargs):
94 """Renders Atom 0.3 XML feed."""
95
96 feed = self.init_feed("atom0_3")
97 feed.update(self.get_feed_data(*args, **kwargs))
98 self.format_dates(feed, 3339)
99 feed.setdefault('link', self.get_feed_url("atom0_3", *args, **kwargs))
100 log.debug(feed)
101 return feed
102
103 @tg.expose(template="turbofeeds.templates.atom1_0",
104 format="xml", content_type="application/atom+xml")
105 - def atom1_0(self, *args, **kwargs):
106 """Renders Atom 1.0 XML feed."""
107
108 feed = self.init_feed("atom1_0")
109 feed.update(self.get_feed_data(*args, **kwargs))
110 self.format_dates(feed, 3339)
111 feed.setdefault('link', self.get_feed_url("atom1_0", *args, **kwargs))
112 log.debug(feed)
113 return feed
114
115 @tg.expose(template="turbofeeds.templates.rss2_0",
116 format="xml", content_type="application/rss+xml")
118 """Renders RSS 2.0 XML feed."""
119
120 feed = self.init_feed("rss2_0")
121 feed.update(self.get_feed_data(*args, **kwargs))
122 self.format_dates(feed, 822)
123 feed.setdefault('link', self.get_feed_url("rss2_0", *args, **kwargs))
124 log.debug(feed)
125 return feed
126
127 @tg.expose(template="turbofeeds.templates.mrss1_1_1",
128 format="xml", content_type="application/rss+xml")
130 """Renders Media RSS 1.1.1 (extended from RSS 2.0) XML feed."""
131
132 feed = self.init_feed("mrss1_1_1")
133 feed.update(self.get_feed_data(*args, **kwargs))
134 self.format_dates(feed, 822)
135 feed.setdefault('link',
136 self.get_feed_url("mrss_1_1_1", *args, **kwargs))
137 log.debug(feed)
138 return feed
139
140
142 """Converts datatime object to RFC 3339 string representation."""
143
144 date = date.strftime("%Y-%m-%dT%H:%M:%SZ")
145 return date
146
148 """Converts datatime object to RFC 822 string representation."""
149
150 date = date.strftime("%a, %d %b %Y %H:%M:%S GMT")
151 return date
152
169
171 """Returns dict with feed info and a list of feed entries.
172
173 This method must be overwritten by a subclass.
174
175 It should return a dictionary with members for the feed info (see the
176 constructor doc string) and a member ``entries``, which is a list with
177 dict items for each feed entry. Supported members in each dict include:
178
179 * author (dict with ``name``, ``email`` and ``uri`` members)
180 * categories (list of strings, used by Atom 1.0 / RSS only)
181 * content (string or dict with ``type`` and ``value`` members,
182 Atom formats only)
183 * updated (datetime, Atom only)
184 * id (string/URL, Atom only)
185 * link (URL)
186 * published (datetime)
187 * rights (string, Atom 1.0 only)
188 * summary (string)
189 * title (string)
190
191 For up-to-date information about supported elements and values, please
192 refer to the templates for the different feed formats in the
193 ``templates`` sub-package.
194
195 """
196 raise NotImplementedError(
197 "This method must be overwritten by a subclass.")
198
199 - def get_feed_url(self, version='atom1_0', *args, **kwargs):
200 """Returns absolute URL (including server name) for the feed."""
201
202 if not self.base_url.endswith('/'):
203 base_url = self.base_url + '/'
204 else:
205 base_url = self.base_url
206 if args:
207 suffix = '/' + '/'.join([str(a) for a in args])
208 else:
209 suffix = ''
210 return absolute_url([base_url, version, suffix], **kwargs)
211
213 """Initializes feed data with the kwargs given to the constructor."""
214
215 engine = self.feed_params.get('engine',
216 tg.config.get('tg.defaultview', 'text'))
217 feed = {
218 'xml_stylesheet': partial(xml_stylesheet, engine=engine),
219 'generator': 'TurboFeeds FeedController (%s, TG %s, %s)' % (
220 __version__, tg.__version__, engine)
221 }
222 feed.update(self.feed_params)
223 if 'stylesheet' in feed and callable(feed['stylesheet']):
224 func = feed.pop('stylesheet')
225 feed['stylesheet'] = func(format, **feed)
226 log.debug("Using stylesheet: %r", feed['stylesheet'])
227 return feed
228