~widelands-dev/widelands-website/django_staticfiles

173.2.3 by Holger Rapp
Made the site compatible to django 1.1 and all the various packages
1
# 
2
# django-atompub by James Tauber <http://jtauber.com/>
3
# http://code.google.com/p/django-atompub/
4
# An implementation of the Atom format and protocol for Django
5
# 
6
# For instructions on how to use this module to generate Atom feeds,
7
# see http://code.google.com/p/django-atompub/wiki/UserGuide
8
# 
9
# 
10
# Copyright (c) 2007, James Tauber
11
# 
12
# Permission is hereby granted, free of charge, to any person obtaining a copy
13
# of this software and associated documentation files (the "Software"), to deal
14
# in the Software without restriction, including without limitation the rights
15
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
# copies of the Software, and to permit persons to whom the Software is
17
# furnished to do so, subject to the following conditions:
18
# 
19
# The above copyright notice and this permission notice shall be included in
20
# all copies or substantial portions of the Software.
21
# 
22
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
# THE SOFTWARE.
29
# 
30
31
from xml.sax.saxutils import XMLGenerator
32
from datetime import datetime
33
34
35
GENERATOR_TEXT = 'django-atompub'
36
GENERATOR_ATTR = {
37
    'uri': 'http://code.google.com/p/django-atompub/',
38
    'version': 'r33'
39
}
40
41
42
43
## based on django.utils.xmlutils.SimplerXMLGenerator
44
class SimplerXMLGenerator(XMLGenerator):
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 = {}
48
        self.startElement(name, attrs)
49
        if contents is not None:
50
            self.characters(contents)
51
        self.endElement(name)
52
53
54
55
## based on django.utils.feedgenerator.rfc3339_date
56
def rfc3339_date(date):
57
    return date.strftime('%Y-%m-%dT%H:%M:%SZ')
58
59
60
61
## based on django.utils.feedgenerator.get_tag_uri
62
def get_tag_uri(url, date):
63
    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
64
    tag = re.sub('^http://', '', url)
65
    if date is not None:
66
        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
67
    tag = re.sub('#', '/', tag)
68
    return 'tag:' + tag
69
70
71
72
## based on django.contrib.syndication.feeds.Feed
73
class Feed(object):
74
    
75
    
76
    VALIDATE = True
77
    
78
    
79
    def __init__(self, slug, feed_url):
80
        # @@@ slug and feed_url are not used yet
81
        pass
82
    
83
    
84
    def __get_dynamic_attr(self, attname, obj, default=None):
85
        try:
86
            attr = getattr(self, attname)
87
        except AttributeError:
88
            return default
89
        if callable(attr):
90
            # Check func_code.co_argcount rather than try/excepting the
91
            # function and catching the TypeError, because something inside
92
            # the function may raise the TypeError. This technique is more
93
            # accurate.
94
            if hasattr(attr, 'func_code'):
95
                argcount = attr.func_code.co_argcount
96
            else:
97
                argcount = attr.__call__.func_code.co_argcount
98
            if argcount == 2: # one argument is 'self'
99
                return attr(obj)
100
            else:
101
                return attr()
102
        return attr
103
    
104
    
105
    def get_feed(self, extra_params=None):
106
        
107
        if extra_params:
108
            try:
109
                obj = self.get_object(extra_params.split('/'))
110
            except (AttributeError, LookupError):
111
                raise LookupError('Feed does not exist')
112
        else:
113
            obj = None
114
        
115
        feed = AtomFeed(
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)
129
        )
130
        
131
        items = self.__get_dynamic_attr('items', obj)
132
        if items is None:
133
            raise LookupError('Feed has no items field')
134
        
135
        for item in items:
136
            feed.add_item(
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={}),
150
            )
151
        
152
        if self.VALIDATE:
153
            feed.validate()
154
        return feed
155
156
157
158
class ValidationError(Exception):
159
    pass
160
161
162
163
## based on django.utils.feedgenerator.SyndicationFeed and django.utils.feedgenerator.Atom1Feed
164
class AtomFeed(object):
165
    
166
    
167
    mime_type = 'application/atom+xml'
168
    ns = u'http://www.w3.org/2005/Atom'
169
    
170
    
171
    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):
173
        if atom_id is None:
174
            raise LookupError('Feed has no feed_id field')
175
        if title is None:
176
            raise LookupError('Feed has no feed_title field')
177
        # if updated == None, we'll calculate it
178
        self.feed = {
179
            'id': atom_id,
180
            'title': title,
181
            'updated': updated,
182
            'icon': icon,
183
            'logo': logo,
184
            'rights': rights,
185
            'subtitle': subtitle,
186
            'authors': authors,
187
            'categories': categories,
188
            'contributors': contributors,
189
            'links': links,
190
            'extra_attrs': extra_attrs,
191
            'hide_generator': hide_generator,
192
        }
193
        self.items = []
194
    
195
    
196
    def add_item(self, atom_id, title, updated, content=None, published=None, rights=None, source=None, summary=None,
197
        authors=[], categories=[], contributors=[], links=[], extra_attrs={}):
198
        if atom_id is None:
199
            raise LookupError('Feed has no item_id method')
200
        if title is None:
201
            raise LookupError('Feed has no item_title method')
202
        if updated is None:
203
            raise LookupError('Feed has no item_updated method')
204
        self.items.append({
205
            'id': atom_id,
206
            'title': title,
207
            'updated': updated,
208
            'content': content,
209
            'published': published,
210
            'rights': rights,
211
            'source': source,
212
            'summary': summary,
213
            'authors': authors,
214
            'categories': categories,
215
            'contributors': contributors,
216
            'links': links,
217
            'extra_attrs': extra_attrs,
218
        })
219
    
220
    
221
    def latest_updated(self):
222
        """
223
        Returns the latest item's updated or the current time if there are no items.
224
        """
225
        updates = [item['updated'] for item in self.items]
226
        if len(updates) > 0:
227
            updates.sort()
228
            return updates[-1]
229
        else:
230
            return datetime.now() # @@@ really we should allow a feed to define its "start" for this case
231
    
232
    
233
    def write_text_construct(self, handler, element_name, data):
234
        if isinstance(data, tuple):
235
            text_type, text = data
236
            if text_type == 'xhtml':
237
                handler.startElement(element_name, {'type': text_type})
238
                handler._write(text) # write unescaped -- it had better be well-formed XML
239
                handler.endElement(element_name)
240
            else:
241
                handler.addQuickElement(element_name, text, {'type': text_type})
242
        else:
243
            handler.addQuickElement(element_name, data)
244
    
245
    
246
    def write_person_construct(self, handler, element_name, person):
247
        handler.startElement(element_name, {})
248
        handler.addQuickElement(u'name', person['name'])
249
        if 'uri' in person:
250
            handler.addQuickElement(u'uri', person['uri'])
251
        if 'email' in person:
252
            handler.addQuickElement(u'email', person['email'])
253
        handler.endElement(element_name)
254
    
255
    
256
    def write_link_construct(self, handler, link):
257
        if 'length' in link:
258
            link['length'] = str(link['length'])
259
        handler.addQuickElement(u'link', None, link)
260
    
261
    
262
    def write_category_construct(self, handler, category):
263
        handler.addQuickElement(u'category', None, category)
264
    
265
    
266
    def write_source(self, handler, data):
267
        handler.startElement(u'source', {})
268
        if data.get('id'):
269
            handler.addQuickElement(u'id', data['id'])
270
        if data.get('title'):
271
            self.write_text_construct(handler, u'title', data['title'])
272
        if data.get('subtitle'):
273
            self.write_text_construct(handler, u'subtitle', data['subtitle'])
274
        if data.get('icon'):
275
            handler.addQuickElement(u'icon', data['icon'])
276
        if data.get('logo'):
277
            handler.addQuickElement(u'logo', data['logo'])
278
        if data.get('updated'):
279
            handler.addQuickElement(u'updated', rfc3339_date(data['updated']))
280
        for category in data.get('categories', []):
281
            self.write_category_construct(handler, category)
282
        for link in data.get('links', []):
283
            self.write_link_construct(handler, link)
284
        for author in data.get('authors', []):
285
            self.write_person_construct(handler, u'author', author)
286
        for contributor in data.get('contributors', []):
287
            self.write_person_construct(handler, u'contributor', contributor)
288
        if data.get('rights'):
289
            self.write_text_construct(handler, u'rights', data['rights'])
290
        handler.endElement(u'source')
291
    
292
    
293
    def write_content(self, handler, data):
294
        if isinstance(data, tuple):
295
            content_dict, text = data
296
            if content_dict.get('type') == 'xhtml':
297
                handler.startElement(u'content', content_dict)
298
                handler._write(text) # write unescaped -- it had better be well-formed XML
299
                handler.endElement(u'content')
300
            else:
301
                handler.addQuickElement(u'content', text, content_dict)
302
        else:
303
            handler.addQuickElement(u'content', data)
304
    
305
    
306
    def write(self, outfile, encoding):
307
        handler = SimplerXMLGenerator(outfile, encoding)
308
        handler.startDocument()
309
        feed_attrs = {u'xmlns': self.ns}
310
        if self.feed.get('extra_attrs'):
311
            feed_attrs.update(self.feed['extra_attrs'])
312
        handler.startElement(u'feed', feed_attrs)
313
        handler.addQuickElement(u'id', self.feed['id'])
314
        self.write_text_construct(handler, u'title', self.feed['title'])
315
        if self.feed.get('subtitle'):
316
            self.write_text_construct(handler, u'subtitle', self.feed['subtitle'])
317
        if self.feed.get('icon'):
318
            handler.addQuickElement(u'icon', self.feed['icon'])
319
        if self.feed.get('logo'):
320
            handler.addQuickElement(u'logo', self.feed['logo'])
321
        if self.feed['updated']:
322
            handler.addQuickElement(u'updated', rfc3339_date(self.feed['updated']))
323
        else:
324
            handler.addQuickElement(u'updated', rfc3339_date(self.latest_updated()))
325
        for category in self.feed['categories']:
326
            self.write_category_construct(handler, category)
327
        for link in self.feed['links']:
328
            self.write_link_construct(handler, link)
329
        for author in self.feed['authors']:
330
            self.write_person_construct(handler, u'author', author)
331
        for contributor in self.feed['contributors']:
332
            self.write_person_construct(handler, u'contributor', contributor)
333
        if self.feed.get('rights'):
334
            self.write_text_construct(handler, u'rights', self.feed['rights'])
335
        if not self.feed.get('hide_generator'):
336
            handler.addQuickElement(u'generator', GENERATOR_TEXT, GENERATOR_ATTR)
337
        
338
        self.write_items(handler)
339
        
340
        handler.endElement(u'feed')
341
    
342
    
343
    def write_items(self, handler):
344
        for item in self.items:
345
            entry_attrs = item.get('extra_attrs', {})
346
            handler.startElement(u'entry', entry_attrs)
347
            
348
            handler.addQuickElement(u'id', item['id'])
349
            self.write_text_construct(handler, u'title', item['title'])
350
            handler.addQuickElement(u'updated', rfc3339_date(item['updated']))
351
            if item.get('published'):
352
                handler.addQuickElement(u'published', rfc3339_date(item['published']))
353
            if item.get('rights'):
354
                self.write_text_construct(handler, u'rights', item['rights'])
355
            if item.get('source'):
356
                self.write_source(handler, item['source'])
357
            
358
            for author in item['authors']:
359
                self.write_person_construct(handler, u'author', author)
360
            for contributor in item['contributors']:
361
                self.write_person_construct(handler, u'contributor', contributor)
362
            for category in item['categories']:
363
                self.write_category_construct(handler, category)
364
            for link in item['links']:
365
                self.write_link_construct(handler, link)
366
            if item.get('summary'):
367
                self.write_text_construct(handler, u'summary', item['summary'])
368
            if item.get('content'):
369
                self.write_content(handler, item['content'])
370
            
371
            handler.endElement(u'entry')
372
    
373
    
374
    def validate(self):
375
        
376
        def validate_text_construct(obj):
377
            if isinstance(obj, tuple):
378
                if obj[0] not in ['text', 'html', 'xhtml']:
379
                    return False
380
            # @@@ no validation is done that 'html' text constructs are valid HTML
381
            # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
382
            
383
            return True
384
        
385
        if not validate_text_construct(self.feed['title']):
386
            raise ValidationError('feed title has invalid type')
387
        if self.feed.get('subtitle'):
388
            if not validate_text_construct(self.feed['subtitle']):
389
                raise ValidationError('feed subtitle has invalid type')
390
        if self.feed.get('rights'):
391
            if not validate_text_construct(self.feed['rights']):
392
                raise ValidationError('feed rights has invalid type')
393
        
394
        alternate_links = {}
395
        for link in self.feed.get('links'):
396
            if link.get('rel') == 'alternate' or link.get('rel') == None:
397
                key = (link.get('type'), link.get('hreflang'))
398
                if key in alternate_links:
399
                    raise ValidationError('alternate links must have unique type/hreflang')
400
                alternate_links[key] = link
401
        
402
        if self.feed.get('authors'):
403
            feed_author = True
404
        else:
405
            feed_author = False
406
        
407
        for item in self.items:
408
            if not feed_author and not item.get('authors'):
409
                if item.get('source') and item['source'].get('authors'):
410
                    pass
411
                else:
412
                    raise ValidationError('if no feed author, all entries must have author (possibly in source)')
413
            
414
            if not validate_text_construct(item['title']):
415
                raise ValidationError('entry title has invalid type')
416
            if item.get('rights'):
417
                if not validate_text_construct(item['rights']):
418
                    raise ValidationError('entry rights has invalid type')
419
            if item.get('summary'):
420
                if not validate_text_construct(item['summary']):
421
                    raise ValidationError('entry summary has invalid type')
422
            source = item.get('source')
423
            if source:
424
                if source.get('title'):
425
                    if not validate_text_construct(source['title']):
426
                        raise ValidationError('source title has invalid type')
427
                if source.get('subtitle'):
428
                    if not validate_text_construct(source['subtitle']):
429
                        raise ValidationError('source subtitle has invalid type')
430
                if source.get('rights'):
431
                    if not validate_text_construct(source['rights']):
432
                        raise ValidationError('source rights has invalid type')
433
            
434
            alternate_links = {}
435
            for link in item.get('links'):
436
                if link.get('rel') == 'alternate' or link.get('rel') == None:
437
                    key = (link.get('type'), link.get('hreflang'))
438
                    if key in alternate_links:
439
                        raise ValidationError('alternate links must have unique type/hreflang')
440
                    alternate_links[key] = link
441
            
442
            if not item.get('content'):
443
                if not alternate_links:
444
                    raise ValidationError('if no content, entry must have alternate link')
445
            
446
            if item.get('content') and isinstance(item.get('content'), tuple):
447
                content_type = item.get('content')[0].get('type')
448
                if item.get('content')[0].get('src'):
449
                    if item.get('content')[1]:
450
                        raise ValidationError('content with src should be empty')
451
                    if not item.get('summary'):
452
                        raise ValidationError('content with src requires a summary too')
453
                    if content_type in ['text', 'html', 'xhtml']:
454
                        raise ValidationError('content with src cannot have type of text, html or xhtml')
455
                if content_type:
456
                    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']:
460
                        # @@@ check content is Base64
461
                        if not item.get('summary'):
462
                            raise ValidationError('content in Base64 requires a summary too')
463
                    if content_type not in ['text', 'html', 'xhtml'] and '/' not in content_type:
464
                        raise ValidationError('content type does not appear to be valid')
465
                    
466
                    # @@@ no validation is done that 'html' text constructs are valid HTML
467
                    # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
468
                    
469
                    return
470
        
471
        return
472
473
474
475
class LegacySyndicationFeed(AtomFeed):
476
    """
477
    Provides an SyndicationFeed-compatible interface in its __init__ and
478
    add_item but is really a new AtomFeed object.
479
    """
480
    
481
    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):
484
        
485
        atom_id = link
486
        title = title
487
        updated = None # will be calculated
488
        rights = feed_copyright
489
        subtitle = subtitle
490
        author_dict = {'name': author_name}
491
        if author_link:
492
            author_dict['uri'] = author_uri
493
        if author_email:
494
            author_dict['email'] = author_email
495
        authors = [author_dict]
496
        if categories:
497
            categories = [{'term': term} for term in categories]
498
        links = [{'rel': 'alternate', 'href': link}]
499
        if feed_url:
500
            links.append({'rel': 'self', 'href': feed_url})
501
        if language:
502
            extra_attrs = {'xml:lang': language}
503
        else:
504
            extra_attrs = {}
505
        
506
        # description ignored (as with Atom1Feed)
507
        
508
        AtomFeed.__init__(self, atom_id, title, updated, rights=rights, subtitle=subtitle,
509
                authors=authors, categories=categories, links=links, extra_attrs=extra_attrs)
510
    
511
    
512
    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):
515
        
516
        if unique_id:
517
            atom_id = unique_id
518
        else:
519
            atom_id = get_tag_uri(link, pubdate)
520
        title = title
521
        updated = pubdate
522
        if item_copyright:
523
            rights = item_copyright
524
        else:
525
            rights = None
526
        if description:
527
            summary = 'html', description
528
        else:
529
            summary = None
530
        author_dict = {'name': author_name}
531
        if author_link:
532
            author_dict['uri'] = author_uri
533
        if author_email:
534
            author_dict['email'] = author_email
535
        authors = [author_dict]
536
        categories = [{'term': term} for term in categories]
537
        links = [{'rel': 'alternate', 'href': link}]
538
        if enclosure:
539
            links.append({'rel': 'enclosure', 'href': enclosure.url, 'length': enclosure.length, 'type': enclosure.mime_type})
540
        
541
        AtomFeed.add_item(self, atom_id, title, updated, rights=rights, summary=summary,
542
                authors=authors, categories=categories, links=links)