~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to notification/atomformat.py

  • Committer: Holger Rapp
  • Date: 2016-08-08 10:06:42 UTC
  • mto: This revision was merged to the branch mainline in revision 419.
  • Revision ID: sirver@gmx.de-20160808100642-z62vwqitxoyl5fh4
Added the apt-get update script I run every 30 days.

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)