root/tags/amplee-0.5.0/amplee/examples/cooker/core/recipe.py

Revision 437, 17.7 kB (checked in by sylvain, 1 year ago)

Small code fix

Line 
1 # -*- coding: utf-8 -*-
2
3 import threading
4 import sha, os, os.path
5 from StringIO import StringIO
6 from xml.sax.saxutils import unescape, escape
7
8 from bridge import Element as E
9 from bridge import Document, PI
10 from amplee.atompub.member.atom import EntryResource
11 from amplee.atompub.member.generic import MediaResource
12 from amplee.utils import parse_multiform_data, safe_quote, \
13      get_isodate, generate_uuid_uri, parse_query_string
14 from bridge.common import ATOM10_NS, ATOM10_PREFIX, THR_NS, THR_PREFIX
15
16
17 from core.utils import transform_member_resource
18
19 __all__ = ['RecipeEntryMember', 'RecipeEntryHandler',
20            'RecipeFormMember', 'RecipeFormHandler',]
21
22 ###########################################################
23 # The following two classes handle the
24 # application/atom+xml;type=entry media-type
25 ###########################################################
26 class RecipeEntryMember(EntryResource):
27     def __init__(self, collection, **kwargs):
28         EntryResource.__init__(self, collection, **kwargs)
29
30     def generate_atom_id(self, entry, slug=None):
31         return u'tag:%s:%s' % (self.collection.name_or_id,
32                                sha.new(slug).hexdigest())
33
34     def generate_resource_id(self, entry, slug=None):
35         return slug.replace(' ','_').decode('utf-8')
36        
37 class RecipeEntryHandler(object):
38     def __init__(self, member_type):
39         # Instance of amplee.handler.MemberType
40         self.member_type = member_type
41
42     def on_update_feed(self, member):
43         """Called anytime the member is created, edited or deleted,
44         so that you can decdie whether or not the collection feed of
45         must also be updated.
46
47         Here we always update it:
48         * member.collection.feed will force the reconstruction of the
49           feed.
50         * member.collection.feed_handler.set() will reset the feed
51         to its new state.
52
53         Since those two operations can be costly you may decide to avoid
54         them here and delay when the collection feed is updated by another
55         mean.
56         """
57         member.collection.feed_handler.set(member.collection.feed)
58        
59     def on_create(self, member, content):
60         """
61         Called by the HTTP handler to complete the creation of the
62         resource. The ``member`` parameter is an instance of
63         RecipeEntryMember and the ``content`` parameter is the POSTed
64         entry as a byte string.
65
66         This returns the member itself (that you could therefore alter
67         here if needed) and None as no content should be stored in
68         addition to the member resource.
69         """
70         return member, None
71    
72     def on_created(self, member):
73         # In case the POSTed atom entry had the
74         # app:control/app:draft set to 'yes'
75         # Then we don't want the recipe to appear into the
76         # public feed.
77         if not member.draft:
78             public = transform_member_resource(member)
79             if public:
80                 member.collection.feed_handler.add(public)
81
82     def on_update(self, existing_member, new_member, new_content):
83         """
84         On an update/edit operation, we receive three arguments.
85         The ``existing_parameter`` is the member as it currently
86         exists in the store. The ``new_member`` is the member as it was
87         generated from the nwly sent content but not yet persisted
88         into the store. The ``new_content`` is the content sent with
89         the request in case your handler needs to access it.
90
91         It will simply replace the existing member by the new one
92         without much more processing. However in a more advanced
93         application, one could perform some diff between the two
94         and decide whether or not it can realize the operation.
95         """
96         # We ensure that dates will be modified
97         new_member.update_dates()
98
99         # If the entry is a not a draft then we replace the
100         # existing one in the collection feed with the new one
101         # Otherwise we remove the existing one from the
102         # collection feed.
103         entry = new_member.atom
104         if not new_member.draft:
105             public = transform_member_resource(new_member)
106             if public:
107                 new_member.collection.feed_handler.replace(public)
108         else:
109             new_member.collection.feed_handler.remove(entry)
110        
111         return new_member, None
112
113     def on_delete(self, member):
114         """
115         On a delete operatuion we simply remove the atom entry
116         from the collection feed.
117         """
118         member.collection.feed_handler.remove(member.atom)
119  
120 ###########################################################
121 # The following two classes handle the
122 # multipart/form-data
123 ###########################################################
124 class RecipeFormMember(MediaResource):
125     def __init__(self, collection, **kwargs):
126         MediaResource.__init__(self, collection, **kwargs)
127
128     def generate_atom_id(self, entry=None, slug=None):
129         return u'tag:%s:%s' % (self.collection.name_or_id,
130                                sha.new(slug).hexdigest())
131
132     def generate_resource_id(self, entry=None, slug=None):
133         return slug.replace(' ','_').decode('utf-8')
134
135     def _generate_atom_entry(self, source, slug, params):
136         # We initialize the entry by calling the built-in
137         # handler for media resource
138         # This will at least generate the basis we need
139         MediaResource.create(self, source, slug)
140
141         # Now we can set the correct values for the entry created right above
142         title = self.entry.get_child('title', self.entry.xml_ns)
143         title.xml_text = params.get('title', '').decode('utf-8')
144
145         author = self.entry.get_child('author', self.entry.xml_ns)
146         name = author.get_child('name', self.entry.xml_ns)
147         name.xml_text = params.get('author', '').decode('utf-8')
148
149         tags = params.get('tags', '').split(',')
150         for tag in tags:
151             tag = tag.decode('utf-8')
152             E(u'category', attributes={u'term': tag, u'label': tag},
153               namespace=self.entry.xml_ns, prefix=self.entry.xml_prefix,
154               parent=self.entry)
155
156     def _generate_replies_feed(self, params):   
157         # Next we create the feed which will host the comments for this
158         # recipe.
159         replies_path = os.path.join('static/replies', self.media_id)
160         if not os.path.exists(replies_path):
161             d = Document()
162             PI(target=u'xml-stylesheet', data=u'href="/static/replies.xsl" type="text/xsl"', parent=d)
163             replies = E(u'feed', namespace=ATOM10_NS, prefix=ATOM10_PREFIX, parent=d)
164
165             # We generate a dummy id seeding the title of the recipe
166             replies_id = u'tag:replies:%s' % (sha.new(params.get('title', '')).hexdigest())
167             E(u'id', content=replies_id,
168               namespace=ATOM10_NS, prefix=ATOM10_PREFIX, parent=replies)
169             E(u'title', content=params.get('title', '').decode('utf-8'),
170               namespace=ATOM10_NS, prefix=ATOM10_PREFIX, parent=replies)
171             E(u'updated', content=get_isodate(),
172               namespace=ATOM10_NS, prefix=ATOM10_PREFIX, parent=replies)
173             author = E(u'author', namespace=ATOM10_NS, prefix=ATOM10_PREFIX, parent=replies)
174             E(u'name', content=params.get('author', '').decode('utf-8'),
175               namespace=ATOM10_NS, prefix=ATOM10_PREFIX, parent=author)
176             E(u'link', attributes={u'rel': u'self', u'type': u'application/atom+xml;type=feed',
177                                    u'href': u'%s/comments' % self.public_uri},
178               namespace=ATOM10_NS, prefix=ATOM10_PREFIX, parent=replies)
179
180             # This may allow aggregators that don't handle RFC 4685 to know where
181             # they could find the entry the comments are relating to
182             E(u'link', attributes={u'rel': u'related', u'type': u'application/atom+xml;type=entry',
183                                    u'href': u'%s.atom' % self.public_uri},
184               namespace=ATOM10_NS, prefix=ATOM10_PREFIX, parent=replies)
185
186             # We finally set the thr:in-reply-to element as per RFC 4685 to explain
187             # what this feed relates to
188             resource_id = self.entry.get_child('id', ATOM10_NS).xml_text
189             E(u'in-reply-to', attributes={u'ref': resource_id, u'type': u'application/atom+xml;type=entry',
190                                           u'href': u'%s.atom' % self.public_uri},
191               namespace=THR_NS, prefix=THR_PREFIX, parent=replies)
192                                              
193             file(replies_path, 'w').write(d.xml())
194                                
195     def create(self, source, slug=None):
196         """
197         We reimplement the amplee.atompub.member.generic.MediaResource
198         class 'create' method to process the request as per our specific
199         requirements for this application.
200         """
201         # First we parse the request body as a multiform/data
202         # This returns a dictionnary where the keys are the
203         # names of each form elements
204         # The content associated is always a string or, in
205         # case of a file upload, it is a cgi.FieldStorage
206         # The self.raw_headers contain a copy of the request
207         # headers as they will be needed to parse the request body.
208         params = parse_multiform_data(source, self.raw_headers)
209
210         # If the slug was not provided we use the title sent
211         # with the request. Some say it's not recommended but
212         # I haven't yet understood why so we'll leave with it.
213         if not slug:
214             slug = params.get('title', '')
215
216         slug = slug.replace(' ','_')
217
218         self._generate_atom_entry(source, slug, params)
219         self._generate_replies_feed(params)
220
221         # The content we return at this point will be propagated as-is
222         # to the RecipeFormHandler instance below during the
223         # request processing
224         return params
225
226     def update(self, source, existing_member):
227         """
228         We reimplement the amplee.atompub.member.generic.MediaResource
229         class 'update' method to process the request as per our specific
230         requirements for this application.
231         """
232         # Update operations are similar to create operations
233         # except that we don't regenerate the comments feed and
234         # we remove the existing atom:category elements to
235         # replace them with the new submitted tags.
236         params = parse_multiform_data(source, self.raw_headers)
237
238         # No slug needed in this case but we use the
239         # existing member that the AtomPub store holds already
240         MediaResource.update(self, source, existing_member)
241
242         title = self.entry.get_child('title', self.entry.xml_ns)
243         title.xml_text = params.get('title', '').decode('utf-8')
244
245         author = self.entry.get_child('author', self.entry.xml_ns)
246         name = author.get_child('name', self.entry.xml_ns)
247         name.xml_text = params.get('author', '').decode('utf-8')
248
249         categories = self.entry.get_children('category', self.entry.xml_ns)[:]
250         for cat in categories:
251             cat.forget()
252            
253         tags = params.get('tags', '').split(',')
254         for tag in tags:
255             tag = tag.decode('utf-8')
256             E(u'category', attributes={u'term': tag, u'label': tag},
257               namespace=self.entry.xml_ns, prefix=self.entry.xml_prefix,
258               parent=self.entry)
259
260         return params
261    
262 class RecipeFormHandler(object):
263     def __init__(self, member_type):
264         self.member_type = member_type
265         self.lock = threading.Lock()
266
267     def on_update_feed(self, member):
268         member.collection.feed_handler.set(member.collection.feed)
269        
270     def on_create(self, member, content):
271         entry = member.atom
272
273         # Note that at this stage the content is the parsed
274         # request body from  RecipeFormMember.create
275         # So it's a dictionnary mapping the multipart/form-data items
276         image = content.get('image', '')
277         if image != '':
278             image = image.file.read()
279             if image:
280                 try:
281                     # We also store the image sent out of the AtomPub
282                     # storage repository so that we can access it from
283                     # a public URI more easily
284                     self.lock.acquire()
285                     file(os.path.join(self.member_type.params['photos_path'],
286                                       member.media_id), 'wb').write(image)
287                 finally:
288                     self.lock.release()
289
290         title = content.get('title', '')
291         author = content.get('author', '')
292         tags = content.get('tags', '')
293         recipe = content.get('content', '')
294
295         # The TarFileStorage we are using expects the content to be
296         # of the form [('some_name', content_string, content_length)]
297         return member, [('title', title, len(title)),
298                         ('author', author, len(author)),
299                         ('tags', tags, len(tags)),
300                         ('recipe', recipe, len(recipe)),
301                         ('image', image, len(image))]
302
303     def on_created(self, member):
304         if not member.draft:
305             public = transform_member_resource(member)
306             if public:
307                 member.collection.feed_handler.add(public)
308        
309     def on_update(self, existing_member, new_member, new_content):
310         new_member.update_dates()
311        
312         image = new_content.get('image', '')
313         if image != '':
314             image = image.file.read()
315             if image:
316                 try:
317                     self.lock.acquire()
318                     file(os.path.join(self.member_type.params['photos_path'],
319                                       member.media_id), 'wb').write(image)
320                 finally:
321                     self.lock.release()
322         else:
323             try:
324                 self.lock.acquire()
325                 try:
326                     os.unlink(os.path.join(self.member_type.params['photos_path'],
327                                            member.media_id))
328                 except OSError:
329                     pass
330             finally:
331                 self.lock.release()
332
333         title = new_content.get('title', '')
334         author = new_content.get('author', '')
335         tags = new_content.get('tags', '')
336         recipe = new_content.get('content', '')
337        
338         return new_member, [('title', title, len(title)),
339                             ('author', author, len(author)),
340                             ('tags', tags, len(tags)),
341                             ('recipe', recipe, len(recipe)),
342                             ('image', image, len(image))]
343
344     def on_updated(self, member):
345         if not member.draft:
346             public = transform_member_resource(member)
347             member.collection.feed_handler.replace(public)
348         else:
349             member.collection.feed_handler.remove(member.atom)
350        
351
352     def on_delete(self, member):
353         member.collection.feed_handler.remove(member.atom)
354         try:
355             self.lock.acquire()
356             try:
357                 os.unlink(os.path.join(self.member_type.params['photos_path'],
358                                        member.media_id))
359             except OSError:
360                 pass
361         finally:
362             self.lock.release()
363          
364 ###########################################################
365 # The following two classes handle the
366 # application/x-www-form-urlencoded
367 ###########################################################   
368 class RecipeFormUrlEncodedMember(MediaResource):
369     def __init__(self, collection, **kwargs):
370         MediaResource.__init__(self, collection, **kwargs)
371
372     def generate_atom_id(self, entry, slug=None):
373         return u'tag:%s:%s' % (self.collection.name_or_id,
374                                sha.new(slug).hexdigest())
375
376     def generate_resource_id(self, entry, slug=None):
377         return slug.replace(' ','_').decode('utf-8')
378
379     def update(self, source, existing_member):
380         length = int(self.raw_headers['content-length'])
381         query = source.read(length)
382        
383         params = parse_query_string(query)
384        
385         MediaResource.update(self, source, existing_member)
386         title = self.entry.get_child('title', self.entry.xml_ns)
387         title.xml_text = params.get('title', '').decode('utf-8')
388
389         return params
390        
391 class RecipeFormUrlEncodedHandler(object):
392     def __init__(self, member_type):
393         self.member_type = member_type
394        
395     def on_update_feed(self, member):
396         member.collection.feed_handler.set(member.collection.feed)
397        
398     def on_update(self, existing_member, new_member, new_content):
399         new_member.update_dates()
400        
401         title = new_content.get('title', '')
402         author = new_content.get('author', '')
403         recipe = new_content.get('recipe', '')
404
405         path = existing_member.collection.get_content_info(existing_member.media_id)
406        
407         path.archive_sub_path = 'image'
408         image = existing_member.collection.get_content(path)[0][1]
409        
410         path.archive_sub_path = 'tags'
411         tags = existing_member.collection.get_content(path)[0][1]
412        
413         return new_member, [('title', title, len(title)),
414                             ('author', author, len(author)),
415                             ('tags', tags, len(tags)),
416                             ('recipe', recipe, len(recipe)),
417                             ('image', image, len(image))]
418
419
420     def on_updated(self, member):
421         if not member.draft:
422             public = transform_member_resource(member)
423             member.collection.feed_handler.replace(public)
424         else:
425             member.collection.feed_handler.remove(member.atom)
426        
427     def on_delete(self, member):
428         member.collection.feed_handler.remove(member.atom)
429         try:
430             os.unlink(os.path.join(self.member_type.params['photos_path'],
431                                    member.media_id))
432         except OSError:
433             pass
434          
Note: See TracBrowser for help on using the browser.