##// END OF EJS Templates
feedgenerator: fixed missing utc definition.
marcink -
r4274:10b9ffbb default
parent child Browse files
Show More
@@ -1,444 +1,446 b''
1 1 # Copyright (c) Django Software Foundation and individual contributors.
2 2 # All rights reserved.
3 3 #
4 4 # Redistribution and use in source and binary forms, with or without modification,
5 5 # are permitted provided that the following conditions are met:
6 6 #
7 7 # 1. Redistributions of source code must retain the above copyright notice,
8 8 # this list of conditions and the following disclaimer.
9 9 #
10 10 # 2. Redistributions in binary form must reproduce the above copyright
11 11 # notice, this list of conditions and the following disclaimer in the
12 12 # documentation and/or other materials provided with the distribution.
13 13 #
14 14 # 3. Neither the name of Django nor the names of its contributors may be used
15 15 # to endorse or promote products derived from this software without
16 16 # specific prior written permission.
17 17 #
18 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 19 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 21 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 22 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 25 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 27 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 29 """
30 30 For definitions of the different versions of RSS, see:
31 31 http://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss
32 32 """
33 33 from __future__ import unicode_literals
34 34
35 35 import datetime
36 36 from StringIO import StringIO
37
38 import pytz
37 39 from six.moves.urllib import parse as urlparse
38 40
39 41 from rhodecode.lib.feedgenerator import datetime_safe
40 42 from rhodecode.lib.feedgenerator.utils import SimplerXMLGenerator, iri_to_uri, force_text
41 43
42 44
43 45 #### The following code comes from ``django.utils.feedgenerator`` ####
44 46
45 47
46 48 def rfc2822_date(date):
47 49 # We can't use strftime() because it produces locale-dependent results, so
48 50 # we have to map english month and day names manually
49 51 months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',)
50 52 days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
51 53 # Support datetime objects older than 1900
52 54 date = datetime_safe.new_datetime(date)
53 55 # We do this ourselves to be timezone aware, email.Utils is not tz aware.
54 56 dow = days[date.weekday()]
55 57 month = months[date.month - 1]
56 58 time_str = date.strftime('%s, %%d %s %%Y %%H:%%M:%%S ' % (dow, month))
57 59
58 60 time_str = time_str.decode('utf-8')
59 61 offset = date.utcoffset()
60 62 # Historically, this function assumes that naive datetimes are in UTC.
61 63 if offset is None:
62 64 return time_str + '-0000'
63 65 else:
64 66 timezone = (offset.days * 24 * 60) + (offset.seconds // 60)
65 67 hour, minute = divmod(timezone, 60)
66 68 return time_str + '%+03d%02d' % (hour, minute)
67 69
68 70
69 71 def rfc3339_date(date):
70 72 # Support datetime objects older than 1900
71 73 date = datetime_safe.new_datetime(date)
72 74 time_str = date.strftime('%Y-%m-%dT%H:%M:%S')
73 75
74 76 time_str = time_str.decode('utf-8')
75 77 offset = date.utcoffset()
76 78 # Historically, this function assumes that naive datetimes are in UTC.
77 79 if offset is None:
78 80 return time_str + 'Z'
79 81 else:
80 82 timezone = (offset.days * 24 * 60) + (offset.seconds // 60)
81 83 hour, minute = divmod(timezone, 60)
82 84 return time_str + '%+03d:%02d' % (hour, minute)
83 85
84 86
85 87 def get_tag_uri(url, date):
86 88 """
87 89 Creates a TagURI.
88 90
89 91 See http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
90 92 """
91 93 bits = urlparse(url)
92 94 d = ''
93 95 if date is not None:
94 96 d = ',%s' % datetime_safe.new_datetime(date).strftime('%Y-%m-%d')
95 97 return 'tag:%s%s:%s/%s' % (bits.hostname, d, bits.path, bits.fragment)
96 98
97 99
98 100 class SyndicationFeed(object):
99 101 """Base class for all syndication feeds. Subclasses should provide write()"""
100 102
101 103 def __init__(self, title, link, description, language=None, author_email=None,
102 104 author_name=None, author_link=None, subtitle=None, categories=None,
103 105 feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs):
104 106 def to_unicode(s):
105 107 return force_text(s, strings_only=True)
106 108 if categories:
107 109 categories = [force_text(c) for c in categories]
108 110 if ttl is not None:
109 111 # Force ints to unicode
110 112 ttl = force_text(ttl)
111 113 self.feed = {
112 114 'title': to_unicode(title),
113 115 'link': iri_to_uri(link),
114 116 'description': to_unicode(description),
115 117 'language': to_unicode(language),
116 118 'author_email': to_unicode(author_email),
117 119 'author_name': to_unicode(author_name),
118 120 'author_link': iri_to_uri(author_link),
119 121 'subtitle': to_unicode(subtitle),
120 122 'categories': categories or (),
121 123 'feed_url': iri_to_uri(feed_url),
122 124 'feed_copyright': to_unicode(feed_copyright),
123 125 'id': feed_guid or link,
124 126 'ttl': ttl,
125 127 }
126 128 self.feed.update(kwargs)
127 129 self.items = []
128 130
129 131 def add_item(self, title, link, description, author_email=None,
130 132 author_name=None, author_link=None, pubdate=None, comments=None,
131 133 unique_id=None, unique_id_is_permalink=None, enclosure=None,
132 134 categories=(), item_copyright=None, ttl=None, updateddate=None,
133 135 enclosures=None, **kwargs):
134 136 """
135 137 Adds an item to the feed. All args are expected to be Python Unicode
136 138 objects except pubdate and updateddate, which are datetime.datetime
137 139 objects, and enclosures, which is an iterable of instances of the
138 140 Enclosure class.
139 141 """
140 142 def to_unicode(s):
141 143 return force_text(s, strings_only=True)
142 144 if categories:
143 145 categories = [to_unicode(c) for c in categories]
144 146 if ttl is not None:
145 147 # Force ints to unicode
146 148 ttl = force_text(ttl)
147 149 if enclosure is None:
148 150 enclosures = [] if enclosures is None else enclosures
149 151
150 152 item = {
151 153 'title': to_unicode(title),
152 154 'link': iri_to_uri(link),
153 155 'description': to_unicode(description),
154 156 'author_email': to_unicode(author_email),
155 157 'author_name': to_unicode(author_name),
156 158 'author_link': iri_to_uri(author_link),
157 159 'pubdate': pubdate,
158 160 'updateddate': updateddate,
159 161 'comments': to_unicode(comments),
160 162 'unique_id': to_unicode(unique_id),
161 163 'unique_id_is_permalink': unique_id_is_permalink,
162 164 'enclosures': enclosures,
163 165 'categories': categories or (),
164 166 'item_copyright': to_unicode(item_copyright),
165 167 'ttl': ttl,
166 168 }
167 169 item.update(kwargs)
168 170 self.items.append(item)
169 171
170 172 def num_items(self):
171 173 return len(self.items)
172 174
173 175 def root_attributes(self):
174 176 """
175 177 Return extra attributes to place on the root (i.e. feed/channel) element.
176 178 Called from write().
177 179 """
178 180 return {}
179 181
180 182 def add_root_elements(self, handler):
181 183 """
182 184 Add elements in the root (i.e. feed/channel) element. Called
183 185 from write().
184 186 """
185 187 pass
186 188
187 189 def item_attributes(self, item):
188 190 """
189 191 Return extra attributes to place on each item (i.e. item/entry) element.
190 192 """
191 193 return {}
192 194
193 195 def add_item_elements(self, handler, item):
194 196 """
195 197 Add elements on each item (i.e. item/entry) element.
196 198 """
197 199 pass
198 200
199 201 def write(self, outfile, encoding):
200 202 """
201 203 Outputs the feed in the given encoding to outfile, which is a file-like
202 204 object. Subclasses should override this.
203 205 """
204 206 raise NotImplementedError('subclasses of SyndicationFeed must provide a write() method')
205 207
206 208 def writeString(self, encoding):
207 209 """
208 210 Returns the feed in the given encoding as a string.
209 211 """
210 212 s = StringIO()
211 213 self.write(s, encoding)
212 214 return s.getvalue()
213 215
214 216 def latest_post_date(self):
215 217 """
216 218 Returns the latest item's pubdate or updateddate. If no items
217 219 have either of these attributes this returns the current UTC date/time.
218 220 """
219 221 latest_date = None
220 222 date_keys = ('updateddate', 'pubdate')
221 223
222 224 for item in self.items:
223 225 for date_key in date_keys:
224 226 item_date = item.get(date_key)
225 227 if item_date:
226 228 if latest_date is None or item_date > latest_date:
227 229 latest_date = item_date
228 230
229 231 # datetime.now(tz=utc) is slower, as documented in django.utils.timezone.now
230 return latest_date or datetime.datetime.utcnow().replace(tzinfo=utc)
232 return latest_date or datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
231 233
232 234
233 235 class Enclosure(object):
234 "Represents an RSS enclosure"
236 """Represents an RSS enclosure"""
235 237 def __init__(self, url, length, mime_type):
236 "All args are expected to be Python Unicode objects"
238 """All args are expected to be Python Unicode objects"""
237 239 self.length, self.mime_type = length, mime_type
238 240 self.url = iri_to_uri(url)
239 241
240 242
241 243 class RssFeed(SyndicationFeed):
242 244 content_type = 'application/rss+xml; charset=utf-8'
243 245
244 246 def write(self, outfile, encoding):
245 247 handler = SimplerXMLGenerator(outfile, encoding)
246 248 handler.startDocument()
247 249 handler.startElement("rss", self.rss_attributes())
248 250 handler.startElement("channel", self.root_attributes())
249 251 self.add_root_elements(handler)
250 252 self.write_items(handler)
251 253 self.endChannelElement(handler)
252 254 handler.endElement("rss")
253 255
254 256 def rss_attributes(self):
255 257 return {"version": self._version,
256 258 "xmlns:atom": "http://www.w3.org/2005/Atom"}
257 259
258 260 def write_items(self, handler):
259 261 for item in self.items:
260 262 handler.startElement('item', self.item_attributes(item))
261 263 self.add_item_elements(handler, item)
262 264 handler.endElement("item")
263 265
264 266 def add_root_elements(self, handler):
265 267 handler.addQuickElement("title", self.feed['title'])
266 268 handler.addQuickElement("link", self.feed['link'])
267 269 handler.addQuickElement("description", self.feed['description'])
268 270 if self.feed['feed_url'] is not None:
269 271 handler.addQuickElement("atom:link", None, {"rel": "self", "href": self.feed['feed_url']})
270 272 if self.feed['language'] is not None:
271 273 handler.addQuickElement("language", self.feed['language'])
272 274 for cat in self.feed['categories']:
273 275 handler.addQuickElement("category", cat)
274 276 if self.feed['feed_copyright'] is not None:
275 277 handler.addQuickElement("copyright", self.feed['feed_copyright'])
276 278 handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date()))
277 279 if self.feed['ttl'] is not None:
278 280 handler.addQuickElement("ttl", self.feed['ttl'])
279 281
280 282 def endChannelElement(self, handler):
281 283 handler.endElement("channel")
282 284
283 285
284 286 class RssUserland091Feed(RssFeed):
285 287 _version = "0.91"
286 288
287 289 def add_item_elements(self, handler, item):
288 290 handler.addQuickElement("title", item['title'])
289 291 handler.addQuickElement("link", item['link'])
290 292 if item['description'] is not None:
291 293 handler.addQuickElement("description", item['description'])
292 294
293 295
294 296 class Rss201rev2Feed(RssFeed):
295 297 # Spec: http://blogs.law.harvard.edu/tech/rss
296 298 _version = "2.0"
297 299
298 300 def add_item_elements(self, handler, item):
299 301 handler.addQuickElement("title", item['title'])
300 302 handler.addQuickElement("link", item['link'])
301 303 if item['description'] is not None:
302 304 handler.addQuickElement("description", item['description'])
303 305
304 306 # Author information.
305 307 if item["author_name"] and item["author_email"]:
306 308 handler.addQuickElement("author", "%s (%s)" % (item['author_email'], item['author_name']))
307 309 elif item["author_email"]:
308 310 handler.addQuickElement("author", item["author_email"])
309 311 elif item["author_name"]:
310 312 handler.addQuickElement(
311 313 "dc:creator", item["author_name"], {"xmlns:dc": "http://purl.org/dc/elements/1.1/"}
312 314 )
313 315
314 316 if item['pubdate'] is not None:
315 317 handler.addQuickElement("pubDate", rfc2822_date(item['pubdate']))
316 318 if item['comments'] is not None:
317 319 handler.addQuickElement("comments", item['comments'])
318 320 if item['unique_id'] is not None:
319 321 guid_attrs = {}
320 322 if isinstance(item.get('unique_id_is_permalink'), bool):
321 323 guid_attrs['isPermaLink'] = str(item['unique_id_is_permalink']).lower()
322 324 handler.addQuickElement("guid", item['unique_id'], guid_attrs)
323 325 if item['ttl'] is not None:
324 326 handler.addQuickElement("ttl", item['ttl'])
325 327
326 328 # Enclosure.
327 329 if item['enclosures']:
328 330 enclosures = list(item['enclosures'])
329 331 if len(enclosures) > 1:
330 332 raise ValueError(
331 333 "RSS feed items may only have one enclosure, see "
332 334 "http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
333 335 )
334 336 enclosure = enclosures[0]
335 337 handler.addQuickElement('enclosure', '', {
336 338 'url': enclosure.url,
337 339 'length': enclosure.length,
338 340 'type': enclosure.mime_type,
339 341 })
340 342
341 343 # Categories.
342 344 for cat in item['categories']:
343 345 handler.addQuickElement("category", cat)
344 346
345 347
346 348 class Atom1Feed(SyndicationFeed):
347 349 # Spec: https://tools.ietf.org/html/rfc4287
348 350 content_type = 'application/atom+xml; charset=utf-8'
349 351 ns = "http://www.w3.org/2005/Atom"
350 352
351 353 def write(self, outfile, encoding):
352 354 handler = SimplerXMLGenerator(outfile, encoding)
353 355 handler.startDocument()
354 356 handler.startElement('feed', self.root_attributes())
355 357 self.add_root_elements(handler)
356 358 self.write_items(handler)
357 359 handler.endElement("feed")
358 360
359 361 def root_attributes(self):
360 362 if self.feed['language'] is not None:
361 363 return {"xmlns": self.ns, "xml:lang": self.feed['language']}
362 364 else:
363 365 return {"xmlns": self.ns}
364 366
365 367 def add_root_elements(self, handler):
366 368 handler.addQuickElement("title", self.feed['title'])
367 369 handler.addQuickElement("link", "", {"rel": "alternate", "href": self.feed['link']})
368 370 if self.feed['feed_url'] is not None:
369 371 handler.addQuickElement("link", "", {"rel": "self", "href": self.feed['feed_url']})
370 372 handler.addQuickElement("id", self.feed['id'])
371 373 handler.addQuickElement("updated", rfc3339_date(self.latest_post_date()))
372 374 if self.feed['author_name'] is not None:
373 375 handler.startElement("author", {})
374 376 handler.addQuickElement("name", self.feed['author_name'])
375 377 if self.feed['author_email'] is not None:
376 378 handler.addQuickElement("email", self.feed['author_email'])
377 379 if self.feed['author_link'] is not None:
378 380 handler.addQuickElement("uri", self.feed['author_link'])
379 381 handler.endElement("author")
380 382 if self.feed['subtitle'] is not None:
381 383 handler.addQuickElement("subtitle", self.feed['subtitle'])
382 384 for cat in self.feed['categories']:
383 385 handler.addQuickElement("category", "", {"term": cat})
384 386 if self.feed['feed_copyright'] is not None:
385 387 handler.addQuickElement("rights", self.feed['feed_copyright'])
386 388
387 389 def write_items(self, handler):
388 390 for item in self.items:
389 391 handler.startElement("entry", self.item_attributes(item))
390 392 self.add_item_elements(handler, item)
391 393 handler.endElement("entry")
392 394
393 395 def add_item_elements(self, handler, item):
394 396 handler.addQuickElement("title", item['title'])
395 397 handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"})
396 398
397 399 if item['pubdate'] is not None:
398 400 handler.addQuickElement('published', rfc3339_date(item['pubdate']))
399 401
400 402 if item['updateddate'] is not None:
401 403 handler.addQuickElement('updated', rfc3339_date(item['updateddate']))
402 404
403 405 # Author information.
404 406 if item['author_name'] is not None:
405 407 handler.startElement("author", {})
406 408 handler.addQuickElement("name", item['author_name'])
407 409 if item['author_email'] is not None:
408 410 handler.addQuickElement("email", item['author_email'])
409 411 if item['author_link'] is not None:
410 412 handler.addQuickElement("uri", item['author_link'])
411 413 handler.endElement("author")
412 414
413 415 # Unique ID.
414 416 if item['unique_id'] is not None:
415 417 unique_id = item['unique_id']
416 418 else:
417 419 unique_id = get_tag_uri(item['link'], item['pubdate'])
418 420 handler.addQuickElement("id", unique_id)
419 421
420 422 # Summary.
421 423 if item['description'] is not None:
422 424 handler.addQuickElement("summary", item['description'], {"type": "html"})
423 425
424 426 # Enclosures.
425 427 for enclosure in item['enclosures']:
426 428 handler.addQuickElement('link', '', {
427 429 'rel': 'enclosure',
428 430 'href': enclosure.url,
429 431 'length': enclosure.length,
430 432 'type': enclosure.mime_type,
431 433 })
432 434
433 435 # Categories.
434 436 for cat in item['categories']:
435 437 handler.addQuickElement("category", "", {"term": cat})
436 438
437 439 # Rights.
438 440 if item['item_copyright'] is not None:
439 441 handler.addQuickElement("rights", item['item_copyright'])
440 442
441 443
442 444 # This isolates the decision of what the system default is, so calling code can
443 445 # do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
444 446 DefaultFeed = Rss201rev2Feed No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now