2
2
# django-atompub by James Tauber <http://jtauber.com/>
3
3
# http://code.google.com/p/django-atompub/
4
4
# An implementation of the Atom format and protocol for Django
6
6
# For instructions on how to use this module to generate Atom feeds,
7
7
# see http://code.google.com/p/django-atompub/wiki/UserGuide
10
10
# Copyright (c) 2007, James Tauber
12
12
# Permission is hereby granted, free of charge, to any person obtaining a copy
13
13
# of this software and associated documentation files (the "Software"), to deal
14
14
# in the Software without restriction, including without limitation the rights
15
15
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
16
# copies of the Software, and to permit persons to whom the Software is
17
17
# furnished to do so, subject to the following conditions:
19
19
# The above copyright notice and this permission notice shall be included in
20
20
# all copies or substantial portions of the Software.
22
22
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
23
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
24
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
## based on django.utils.xmlutils.SimplerXMLGenerator
42
# based on django.utils.xmlutils.SimplerXMLGenerator
44
43
class SimplerXMLGenerator(XMLGenerator):
45
45
def addQuickElement(self, name, contents=None, attrs=None):
46
"Convenience method for adding an element with no children"
47
if attrs is None: attrs = {}
46
"""Convenience method for adding an element with no children."""
48
49
self.startElement(name, attrs)
49
50
if contents is not None:
50
51
self.characters(contents)
51
52
self.endElement(name)
55
## based on django.utils.feedgenerator.rfc3339_date
55
# based on django.utils.feedgenerator.rfc3339_date
56
56
def rfc3339_date(date):
57
57
return date.strftime('%Y-%m-%dT%H:%M:%SZ')
61
## based on django.utils.feedgenerator.get_tag_uri
60
# based on django.utils.feedgenerator.get_tag_uri
62
61
def get_tag_uri(url, date):
63
"Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
64
See http://diveintomark.org/archives/2004/05/28/howto-atom-id
64
67
tag = re.sub('^http://', '', url)
65
68
if date is not None:
66
69
tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
111
109
raise LookupError('Feed does not exist')
116
atom_id = self.__get_dynamic_attr('feed_id', obj),
117
title = self.__get_dynamic_attr('feed_title', obj),
118
updated = self.__get_dynamic_attr('feed_updated', obj),
119
icon = self.__get_dynamic_attr('feed_icon', obj),
120
logo = self.__get_dynamic_attr('feed_logo', obj),
121
rights = self.__get_dynamic_attr('feed_rights', obj),
122
subtitle = self.__get_dynamic_attr('feed_subtitle', obj),
123
authors = self.__get_dynamic_attr('feed_authors', obj, default=[]),
124
categories = self.__get_dynamic_attr('feed_categories', obj, default=[]),
125
contributors = self.__get_dynamic_attr('feed_contributors', obj, default=[]),
126
links = self.__get_dynamic_attr('feed_links', obj, default=[]),
127
extra_attrs = self.__get_dynamic_attr('feed_extra_attrs', obj),
128
hide_generator = self.__get_dynamic_attr('hide_generator', obj, default=False)
114
atom_id=self.__get_dynamic_attr('feed_id', obj),
115
title=self.__get_dynamic_attr('feed_title', obj),
116
updated=self.__get_dynamic_attr('feed_updated', obj),
117
icon=self.__get_dynamic_attr('feed_icon', obj),
118
logo=self.__get_dynamic_attr('feed_logo', obj),
119
rights=self.__get_dynamic_attr('feed_rights', obj),
120
subtitle=self.__get_dynamic_attr('feed_subtitle', obj),
121
authors=self.__get_dynamic_attr('feed_authors', obj, default=[]),
122
categories=self.__get_dynamic_attr(
123
'feed_categories', obj, default=[]),
124
contributors=self.__get_dynamic_attr(
125
'feed_contributors', obj, default=[]),
126
links=self.__get_dynamic_attr('feed_links', obj, default=[]),
127
extra_attrs=self.__get_dynamic_attr('feed_extra_attrs', obj),
128
hide_generator=self.__get_dynamic_attr(
129
'hide_generator', obj, default=False)
131
132
items = self.__get_dynamic_attr('items', obj)
132
133
if items is None:
133
134
raise LookupError('Feed has no items field')
135
136
for item in items:
137
atom_id = self.__get_dynamic_attr('item_id', item),
138
title = self.__get_dynamic_attr('item_title', item),
139
updated = self.__get_dynamic_attr('item_updated', item),
140
content = self.__get_dynamic_attr('item_content', item),
141
published = self.__get_dynamic_attr('item_published', item),
142
rights = self.__get_dynamic_attr('item_rights', item),
143
source = self.__get_dynamic_attr('item_source', item),
144
summary = self.__get_dynamic_attr('item_summary', item),
145
authors = self.__get_dynamic_attr('item_authors', item, default=[]),
146
categories = self.__get_dynamic_attr('item_categories', item, default=[]),
147
contributors = self.__get_dynamic_attr('item_contributors', item, default=[]),
148
links = self.__get_dynamic_attr('item_links', item, default=[]),
149
extra_attrs = self.__get_dynamic_attr('item_extra_attrs', None, default={}),
138
atom_id=self.__get_dynamic_attr('item_id', item),
139
title=self.__get_dynamic_attr('item_title', item),
140
updated=self.__get_dynamic_attr('item_updated', item),
141
content=self.__get_dynamic_attr('item_content', item),
142
published=self.__get_dynamic_attr('item_published', item),
143
rights=self.__get_dynamic_attr('item_rights', item),
144
source=self.__get_dynamic_attr('item_source', item),
145
summary=self.__get_dynamic_attr('item_summary', item),
146
authors=self.__get_dynamic_attr(
147
'item_authors', item, default=[]),
148
categories=self.__get_dynamic_attr(
149
'item_categories', item, default=[]),
150
contributors=self.__get_dynamic_attr(
151
'item_contributors', item, default=[]),
152
links=self.__get_dynamic_attr('item_links', item, default=[]),
153
extra_attrs=self.__get_dynamic_attr(
154
'item_extra_attrs', None, default={}),
152
157
if self.VALIDATE:
158
162
class ValidationError(Exception):
163
## based on django.utils.feedgenerator.SyndicationFeed and django.utils.feedgenerator.Atom1Feed
166
# based on django.utils.feedgenerator.SyndicationFeed and
167
# django.utils.feedgenerator.Atom1Feed
164
168
class AtomFeed(object):
167
170
mime_type = 'application/atom+xml'
168
171
ns = u'http://www.w3.org/2005/Atom'
171
173
def __init__(self, atom_id, title, updated=None, icon=None, logo=None, rights=None, subtitle=None,
172
authors=[], categories=[], contributors=[], links=[], extra_attrs={}, hide_generator=False):
174
authors=[], categories=[], contributors=[], links=[], extra_attrs={}, hide_generator=False):
173
175
if atom_id is None:
174
176
raise LookupError('Feed has no feed_id field')
175
177
if title is None:
217
218
'extra_attrs': extra_attrs,
221
221
def latest_updated(self):
223
Returns the latest item's updated or the current time if there are no items.
222
"""Returns the latest item's updated or the current time if there are
225
224
updates = [item['updated'] for item in self.items]
226
225
if len(updates) > 0:
228
227
return updates[-1]
230
return datetime.now() # @@@ really we should allow a feed to define its "start" for this case
229
# @@@ really we should allow a feed to define its "start" for this case
230
return datetime.now()
233
232
def write_text_construct(self, handler, element_name, data):
234
233
if isinstance(data, tuple):
235
234
text_type, text = data
236
235
if text_type == 'xhtml':
237
236
handler.startElement(element_name, {'type': text_type})
238
handler._write(text) # write unescaped -- it had better be well-formed XML
237
# write unescaped -- it had better be well-formed XML
239
239
handler.endElement(element_name)
241
handler.addQuickElement(element_name, text, {'type': text_type})
241
handler.addQuickElement(
242
element_name, text, {'type': text_type})
243
244
handler.addQuickElement(element_name, data)
246
246
def write_person_construct(self, handler, element_name, person):
247
247
handler.startElement(element_name, {})
248
248
handler.addQuickElement(u'name', person['name'])
333
332
if self.feed.get('rights'):
334
333
self.write_text_construct(handler, u'rights', self.feed['rights'])
335
334
if not self.feed.get('hide_generator'):
336
handler.addQuickElement(u'generator', GENERATOR_TEXT, GENERATOR_ATTR)
335
handler.addQuickElement(
336
u'generator', GENERATOR_TEXT, GENERATOR_ATTR)
338
338
self.write_items(handler)
340
340
handler.endElement(u'feed')
343
342
def write_items(self, handler):
344
343
for item in self.items:
345
344
entry_attrs = item.get('extra_attrs', {})
346
345
handler.startElement(u'entry', entry_attrs)
348
347
handler.addQuickElement(u'id', item['id'])
349
348
self.write_text_construct(handler, u'title', item['title'])
350
349
handler.addQuickElement(u'updated', rfc3339_date(item['updated']))
351
350
if item.get('published'):
352
handler.addQuickElement(u'published', rfc3339_date(item['published']))
351
handler.addQuickElement(
352
u'published', rfc3339_date(item['published']))
353
353
if item.get('rights'):
354
354
self.write_text_construct(handler, u'rights', item['rights'])
355
355
if item.get('source'):
356
356
self.write_source(handler, item['source'])
358
358
for author in item['authors']:
359
359
self.write_person_construct(handler, u'author', author)
360
360
for contributor in item['contributors']:
361
self.write_person_construct(handler, u'contributor', contributor)
361
self.write_person_construct(
362
handler, u'contributor', contributor)
362
363
for category in item['categories']:
363
364
self.write_category_construct(handler, category)
364
365
for link in item['links']:
426
428
raise ValidationError('source title has invalid type')
427
429
if source.get('subtitle'):
428
430
if not validate_text_construct(source['subtitle']):
429
raise ValidationError('source subtitle has invalid type')
431
raise ValidationError(
432
'source subtitle has invalid type')
430
433
if source.get('rights'):
431
434
if not validate_text_construct(source['rights']):
432
435
raise ValidationError('source rights has invalid type')
434
437
alternate_links = {}
435
438
for link in item.get('links'):
436
439
if link.get('rel') == 'alternate' or link.get('rel') == None:
437
440
key = (link.get('type'), link.get('hreflang'))
438
441
if key in alternate_links:
439
raise ValidationError('alternate links must have unique type/hreflang')
442
raise ValidationError(
443
'alternate links must have unique type/hreflang')
440
444
alternate_links[key] = link
442
446
if not item.get('content'):
443
447
if not alternate_links:
444
raise ValidationError('if no content, entry must have alternate link')
448
raise ValidationError(
449
'if no content, entry must have alternate link')
446
451
if item.get('content') and isinstance(item.get('content'), tuple):
447
452
content_type = item.get('content')[0].get('type')
448
453
if item.get('content')[0].get('src'):
449
454
if item.get('content')[1]:
450
raise ValidationError('content with src should be empty')
455
raise ValidationError(
456
'content with src should be empty')
451
457
if not item.get('summary'):
452
raise ValidationError('content with src requires a summary too')
458
raise ValidationError(
459
'content with src requires a summary too')
453
460
if content_type in ['text', 'html', 'xhtml']:
454
raise ValidationError('content with src cannot have type of text, html or xhtml')
461
raise ValidationError(
462
'content with src cannot have type of text, html or xhtml')
456
464
if '/' in content_type and \
457
not content_type.startswith('text/') and \
458
not content_type.endswith('/xml') and not content_type.endswith('+xml') and \
459
not content_type in ['application/xml-external-parsed-entity', 'application/xml-dtd']:
465
not content_type.startswith('text/') and \
466
not content_type.endswith('/xml') and not content_type.endswith('+xml') and \
467
not content_type in ['application/xml-external-parsed-entity', 'application/xml-dtd']:
460
468
# @@@ check content is Base64
461
469
if not item.get('summary'):
462
raise ValidationError('content in Base64 requires a summary too')
470
raise ValidationError(
471
'content in Base64 requires a summary too')
463
472
if content_type not in ['text', 'html', 'xhtml'] and '/' not in content_type:
464
raise ValidationError('content type does not appear to be valid')
473
raise ValidationError(
474
'content type does not appear to be valid')
466
476
# @@@ no validation is done that 'html' text constructs are valid HTML
467
477
# @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
475
484
class LegacySyndicationFeed(AtomFeed):
477
486
Provides an SyndicationFeed-compatible interface in its __init__ and
478
487
add_item but is really a new AtomFeed object.
481
490
def __init__(self, title, link, description, language=None, author_email=None,
482
author_name=None, author_link=None, subtitle=None, categories=[],
483
feed_url=None, feed_copyright=None):
491
author_name=None, author_link=None, subtitle=None, categories=[],
492
feed_url=None, feed_copyright=None):
487
updated = None # will be calculated
496
updated = None # will be calculated
488
497
rights = feed_copyright
489
498
subtitle = subtitle
490
499
author_dict = {'name': author_name}
502
511
extra_attrs = {'xml:lang': language}
506
515
# description ignored (as with Atom1Feed)
508
517
AtomFeed.__init__(self, atom_id, title, updated, rights=rights, subtitle=subtitle,
509
authors=authors, categories=categories, links=links, extra_attrs=extra_attrs)
518
authors=authors, categories=categories, links=links, extra_attrs=extra_attrs)
512
520
def add_item(self, title, link, description, author_email=None,
513
author_name=None, author_link=None, pubdate=None, comments=None,
514
unique_id=None, enclosure=None, categories=[], item_copyright=None):
521
author_name=None, author_link=None, pubdate=None, comments=None,
522
unique_id=None, enclosure=None, categories=[], item_copyright=None):
517
525
atom_id = unique_id