~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to atomformat.py

  • Committer: kaputtnik
  • Date: 2019-05-30 18:20:02 UTC
  • mto: This revision was merged to the branch mainline in revision 540.
  • Revision ID: kaputtnik-20190530182002-g7l91m1xo28clghv
adjusted README; first commit on the new server

Show diffs side-by-side

added added

removed removed

Lines of Context:
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)