root/tags/amplee-0.5.0/amplee/atompub/member/__init__.py

Revision 429, 14.7 kB (checked in by sylvain, 1 year ago)

Coe cleanup to match the refactoring of the storage

Line 
1 # -*- coding: utf-8 -*-
2
3 __doc__ = """
4 APP establishes uses the term of members to describe
5 resources within an APP service context.
6
7 APP says:
8 '''
9 A resource whose IRI is listed in a Collection by a link
10 element with a relation of "edit" or "edit-media".
11 '''
12
13 The base member is the what amplee calls the EntryMember
14 which is the resource whose IRI is contained in the
15 link defined by rel="edit".
16
17 The EntryMember should be rarerly used directly has it is
18 only meaningful if your resource is an Atom entry document.
19
20 On the other hand the MediaMember describes the resource
21 whose IRI is contained in the link defined by rel="edit-media".
22
23 The Mediamember will certainly be the most common class to
24 inherit for your own member implementations.
25 """
26
27 __all__ = ['MemberResource']
28
29 import os.path
30 from xml.parsers.expat import ExpatError
31 from urlparse import urljoin, urlparse
32 from urllib import quote
33
34 from bridge import Element as E
35 from bridge import Attribute as A
36 from bridge import Document, PI
37 from bridge.filter.atom import lookup_links
38 from amplee.utils import generate_uuid_uri, get_isodate, safe_quote, safe_unquote, safe_url_join
39 from amplee.comparer import app_edited_comparer
40 from amplee.error import ResourceOperationException
41
42 from bridge.common import ATOM10_PREFIX, ATOMPUB_PREFIX, XML_PREFIX, XML_NS, \
43      ATOM10_NS, ATOMPUB_NS, XHTML1_NS, XHTML1_PREFIX, atom_as_attr, atom_as_list, \
44      atom_attribute_of_element
45
46 class MemberResource(object):
47     def __init__(self, collection, id=None, atom=None):
48         """
49         Keyword Arguments:
50         collection -- AtomPubCollection instance holding this member
51         id -- identifier for this member
52         atom -- bridge.Element instance
53         """
54         self.collection = collection
55         self.entry = atom
56         self.member_id = id
57         self.media_id = None
58         self.media_type = u'application/atom+xml;type=entry'
59         self.draft = False
60         self.comparer = app_edited_comparer
61         self.xslt_path = None
62        
63     def __cmp__(self, other):
64         """By default members are compared following their app:edited element"""
65         return self.comparer(self, other)
66        
67     def _getentry(self):
68         if self.entry is None:
69             # Lazy loading of the atom entry
70             info = self.collection.get_meta_data_info(self.member_id)
71             source = self.collection.get_meta_data(info)
72             self.entry = E.load(source, as_attribute=atom_as_attr, as_list=atom_as_list,
73                                 as_attribute_of_element=atom_attribute_of_element).xml_root
74         return self.entry
75
76     def _setentry(self, entry):
77         raise AttributeError("Cannot overwrite the underlying Atom entry")
78
79     def _delentry(self):
80         raise AttributeError("Cannot delete the underlying Atom entry")
81    
82     atom = property(_getentry, _setentry, _delentry)
83
84     def _getmediacontent(self):
85         return self.collection.get_content(self.collection.get_info(self.media_id))
86
87     def _setmediacontent(self, entry):
88         raise AttributeError("Cannot overwrite the underlying content")
89
90     def _delmediacontent(self):
91         raise AttributeError("Cannot delete the media content")
92    
93     content = property(_getmediacontent, _setmediacontent, _delmediacontent)
94
95     # For the pickling of this class
96     def __getstate__(self):
97         return {'member_id': self.member_id,
98                 'media_id': self.media_id,
99                 'media_type': self.media_type}
100                 #'entry': self.atom.xml(indent=False)}
101
102     def __setstate__(self, state):
103         self.entry = None
104         self.member_id = state['member_id']
105         self.media_id = state['media_id']
106         self.media_type = state['media_type']
107         #self.entry = E.load(state['entry'], as_attribute=atom_as_attr, as_list=atom_as_list,
108         #                    as_attribute_of_element=atom_attribute_of_element).xml_root
109
110     def from_entry(self, entry):
111         """Allows the filling of the current instance from an Atom entry.
112
113         This will lookup for two links:
114          * rel='edit'
115          * rel='edit-media'
116         """
117        
118         self.entry = entry
119        
120         edit_links = self.entry.filtrate(lookup_links, rel='edit')
121         if edit_links:
122             href = edit_links[0].get_attribute('href')
123             if href:
124                 self.member_id = safe_unquote(os.path.split(urlparse(unicode(href))[2])[-1])
125             self.media_type = u'application/atom+xml;type=entry'
126            
127         edit_media_links = self.entry.filtrate(lookup_links, rel='edit-media')
128         if edit_media_links:
129             href = edit_media_links[0].get_attribute('href')
130             if href:
131                 self.media_id = safe_unquote(os.path.split(urlparse(unicode(href))[2])[-1])
132             media_type = edit_media_links[0].get_attribute('type')
133             if media_type:
134                 self.media_type = unicode(media_type)
135         elif self.member_id:
136             self.media_id = self.collection.convert_id(self.member_id)[-1]
137
138         self.draft = self.is_draft()
139
140     def set_xslt(self, info):
141         """
142         Sets the XSLT to associate to the Atom entry. A processing
143         instruction will be inserted when tge self.xml() method is
144         called.
145
146         The ``path`` is a URI to the the XSLT resource.
147         """
148         self.xslt_path = path
149
150     def xml(self):
151         """Returns the serialization as a string of the atom entry."""
152         if self.xslt_path:
153             d = Document()
154             PI(target=u'xml-stylesheet', data=u'href="%s" type="text/xsl"' % self.xslt_path, parent=d)
155             d.xml_children.append(self.entry)
156             return d.xml()
157
158         return self.entry.xml()
159    
160     def load_document(self, source):
161         try:
162             doc = E.load(source, as_attribute=atom_as_attr, as_list=atom_as_list,
163                          as_attribute_of_element=atom_attribute_of_element).xml_root
164         except ExpatError, er:
165             raise ResourceOperationException("Could not parse posted Atom entry", 400, body=str(er))
166
167         # Let's ensure of the correctness of the namespace mapping
168         doc.update_prefix(ATOM10_PREFIX, doc.xml_ns, ATOM10_NS, update_attributes=False)
169
170         return doc
171
172     def update_dates(self):
173         entry = self.entry
174         isodate = get_isodate()
175        
176         updated = entry.get_child('updated', entry.xml_ns)
177         if updated is not None:
178             updated.xml_text = isodate
179         else:
180             E(u'updated', content=isodate, prefix=entry.xml_prefix,
181               namespace=entry.xml_ns, parent=entry)
182
183         edited = entry.get_child('edited', ATOMPUB_NS)
184         if edited is not None:
185             edited.xml_text = isodate
186         else:
187             E(u'edited', content=isodate, prefix=ATOMPUB_PREFIX,
188               namespace=ATOMPUB_NS, parent=entry)
189
190     def merge_dates(self, new_member):
191         entry = self.entry
192         new_entry = new_member.atom
193
194         updated = entry.get_child('updated', entry.xml_ns)
195         isodate = get_isodate()
196         if updated is not None:
197             updated.xml_text = isodate
198         else:
199             E(u'updated', content=isodate, prefix=entry.xml_prefix,
200               namespace=entry.xml_ns, parent=entry)
201            
202         new_edited = new_entry.get_child('edited', ATOMPUB_NS)
203         if new_edited:
204             edited = entry.get_child('edited', ATOMPUB_NS)
205             if edited is not None:
206                 edited.xml_text = new_edited.xml_text
207             else:
208                 E(u'edited', content=new_edited.xml_text, prefix=ATOMPUB_PREFIX,
209                   namespace=ATOMPUB_NS, parent=entry)
210
211     def prepare_for_public(self, content=None, external_content=False,
212                            rel=u'alternate', media_type=u'text/html',
213                            xslt_path=None):
214         clone = self.entry.clone().xml_root
215         xml_base = self.collection.get_xml_base()
216         if xml_base:
217             A(u'base', value=xml_base, namespace=XML_NS, prefix=XML_PREFIX, parent=clone)
218
219         edited = clone.get_child('edited', ATOMPUB_NS)
220         if edited: edited.forget()
221        
222         control = clone.get_child('control', ATOMPUB_NS)
223         if control: control.forget()
224
225         links = clone.filtrate(lookup_links, rel="edit")
226         if links: links[0].forget()
227        
228         links = clone.filtrate(lookup_links, rel="edit-media")
229         if links: links[0].forget()
230
231         # link rel="self"
232         if xml_base:
233             href = safe_url_join([self.collection.base_uri, self.member_id])
234         else:
235             href = safe_url_join([self.collection.get_base_uri(), self.member_id])
236         links = clone.filtrate(lookup_links, rel="self")
237         if links:
238             links[0].href = href
239             links[0].type = u'application/atom+xml;type=entry'
240         else:
241             E(u'link', attributes={u'rel': u'self', u'type': u'application/atom+xml;type=entry',
242                                    u'href': href},
243               namespace=clone.xml_ns, prefix=clone.xml_prefix, parent=clone)
244
245            
246         # link rel="%s" % rel
247         if self.media_id:
248             if xml_base:
249                 href = safe_url_join([self.collection.base_uri, self.media_id])
250             else:
251                 href = self.public_uri
252                
253             links = clone.filtrate(lookup_links, rel=rel)
254             if links:
255                 links[0].href = href
256                 links[0].type = media_type
257             else:
258                 E(u'link', attributes={u'rel': rel, u'type': media_type, u'href': href},
259                   namespace=clone.xml_ns, prefix=clone.xml_prefix, parent=clone)
260
261         ct = clone.get_child('content', clone.xml_ns)
262         if ct: ct.forget()
263
264         if external_content:
265             if xml_base:
266                 src = safe_url_join([self.collection.base_uri, self.media_id])
267             else:
268                 src = self.public_uri
269             attr = {u'src': src, u'type': media_type}
270             E(u'content', attributes=attr, namespace=clone.xml_ns,
271               prefix=clone.xml_prefix, parent=clone)
272         elif content:
273             ct = E(u'content', attributes={u'type': u'xhtml'},
274                    namespace=clone.xml_ns, prefix=clone.xml_prefix, parent=clone)
275             content = '<div xmlns="%s">%s</div>'  % (XHTML1_NS, content)
276             div = E.load(content).xml_root
277             ct.xml_children.append(div)
278             div.xml_parent = ct
279
280         d = Document()
281         if xslt_path:
282             PI(target=u'xml-stylesheet', data=u'href="%s" type="text/xsl"' % xslt_path, parent=d)
283
284         d.xml_children.append(clone)
285        
286         return d
287
288     def index(self):
289         """Passes the member to the collection indexers"""
290         if self.collection:
291             for indexer in self.collection.indexers:
292                 indexer.process(self)
293
294     def get_public_uri(self):
295         """Returns a URI suitable for public feed and independant from the collection URI"""
296         if self.member_id:
297             return safe_url_join([self.collection.get_base_uri(),
298                                   self.media_id], start_at=1)
299     public_uri = property(get_public_uri)
300
301     def get_public_stripped_uri(self):
302         """Returns a URI suitable for public feed and independant from the collection URI.
303         It removes any trailing extension there may be.
304         """
305         public_uri = self.public_uri
306         if public_uri:
307             public_uri, ext = os.path.splitext(public_uri)
308             return public_uri
309     public_uri_stripped = property(get_public_uri)
310
311     def get_edit_uri(self):
312         """Returns the URI of the member resource"""
313         if self.member_id:
314             return safe_url_join([self.collection.get_base_edit_uri(),
315                                   self.member_id], start_at=1)
316     member_uri = property(get_edit_uri)
317    
318     def get_edit_media_uri(self):
319         """Returns the URI of the media link entry (MLE)"""
320         if self.media_id:
321             return safe_url_join([self.collection.get_base_media_edit_uri(),
322                                   self.media_id], start_at=1)
323     media_uri = property(get_edit_media_uri)
324      
325     def is_draft(self):
326         """
327         Returns True is the entry has a app:control element with an
328         app:draft element which has a text value set to 'yes'. Returns False
329         otherwise.
330         """
331         app_ctrl_element = self.entry.get_child('control', ATOMPUB_NS)
332         if app_ctrl_element:
333             app_draft = app_ctrl_element.get_child('draft', ATOMPUB_NS)
334             if app_draft and app_draft.xml_text and app_draft.xml_text.lower() == u'yes':
335                 return True
336         return False
337    
338     ############################################################
339     # API to implement per your application requirements
340     ############################################################
341     def generate_atom_id(self, entry=None, slug=None):
342         """
343         Called to generate a valid atom identifier (a valid URI)
344         which will be used within the Atom entry itself.
345
346         By default this returns a random urn:uuid value.
347         """
348         return generate_uuid_uri()
349
350     def generate_resource_id(self, entry, slug=None):
351         """
352         Called to generate a token that will be used as the internal
353         identifier for this member within its collection. Also
354         used as the identifier within the storage it will be added
355         to which means the returned value should be valid for the
356         chosen storage.
357
358         By default this returns the atom:id value of the member entry.
359         """
360         atom_id = entry.get_child('id', ATOM10_NS)
361         return atom_id.xml_text
362
363     def create(self, source, slug=None):
364         """
365         Called to generate the member entry from the atom entry
366         provided in the source parameter.
367
368         This must return the source again after being processed
369         if need be within the method call. The content can be returned
370         either as a byte string, unicode or a file object.
371         
372         Keyword arguments:
373         source -- resource string to handle
374         
375         slug -- hint on how to generate the name of the resource and
376         used as the last part of the edit and edit-media IRIs (default:None)
377         """
378         raise NotImplemented()
379
380     def update(self, source, exisiting_member):
381         """
382         Called to generate the new member entry from the atom entry
383         provided in the source parameter as well as from the
384         existing member in the store.
385         
386         This must return the source again after being processed
387         if need be within the method call. The content can be returned
388         either as a byte string, unicode or a file object.
389         
390         Keyword arguments:
391         source -- resource string to handle
392         exisiting_member -- member instance of an existing member resource
393         """
394         raise NotImplemented()
Note: See TracBrowser for help on using the browser.