1
2 __docformat__ = 'epytext en'
3
4 __doc__ = """
5 @see: Collection from RFC 5023 at U{http://tools.ietf.org/html/rfc5023#section-9}
6 @undocumented: __doc__
7 """
8
9 __all__ = ['MemberResource']
10
11 import copy
12 import os.path
13 from xml.sax import SAXParseException
14 from urlparse import urljoin, urlparse
15 from urllib import quote
16
17 import amara
18 from amplee.utils import generate_uuid_uri, get_isodate, safe_quote, \
19 safe_unquote, safe_url_join, qname
20 from amplee.comparer import app_edited_comparer
21 from amplee.error import ResourceOperationException
22 from amplee.atompub.member.helper import MemberHelper
23
24 from amplee.utils import ATOM10_PREFIX, ATOMPUB_PREFIX, XML_PREFIX, XML_NS, \
25 ATOM10_NS, ATOMPUB_NS, XHTML1_NS, XHTML1_PREFIX
26
28 - def __init__(self, collection, id=None, atom=None, media_type=None):
29 """
30 Represents in amplee an AtomPub member resource and provides
31 a fairly extensive API to manipulate it.
32
33 @type collection: L{AtomPubCollection}
34 @param collection: collection holding the reference to this member
35
36 @type id: string
37 @param id: identifier for this member
38
39 @type atom: L{amara.bindery.root_base}
40 @param atom: amara instance representing the Atom entry
41
42 @type media_type: string
43 @param media_type: media type of the associated resource
44 """
45 self.collection = collection
46 self.entry = atom
47 self.member_id = id
48 self.media_id = None
49 self.media_type = media_type or u'application/atom+xml;type=entry'
50 self.draft = False
51 self.comparer = app_edited_comparer
52 self.xslt_path = None
53
55 """
56 By default members are compared following their app:edited element,
57 but you can change that behavior by setting the C{self.comparer} attribute
58 to a callable that will perform the comparison.
59
60 @rtype: int
61 @return:
62 """
63 return self.comparer(self, other)
64
65 - def _getentry(self):
66 if self.entry is None:
67
68 info = self.collection.get_meta_data_info(self.member_id)
69 source = self.collection.get_meta_data(info)
70 self.entry = amara.parse(source, prefixes={ATOM10_PREFIX: ATOM10_NS,
71 ATOMPUB_PREFIX: ATOMPUB_NS})
72 return self.entry
73
74 - def _setentry(self, entry):
75 raise AttributeError("Cannot overwrite the underlying Atom entry")
76
77 - def _delentry(self):
78 raise AttributeError("Cannot delete the underlying Atom entry")
79
80 atom = property(_getentry, _setentry, _delentry)
81
84
86 raise AttributeError("Cannot overwrite the underlying content")
87
89 raise AttributeError("Cannot delete the media content")
90
91 content = property(_getmediacontent, _setmediacontent, _delmediacontent)
92
93
95 return {'member_id': self.member_id,
96 'media_id': self.media_id,
97 'media_type': self.media_type}
98
99
101 self.entry = None
102 self.member_id = state['member_id']
103 self.media_id = state['media_id']
104 self.media_type = state['media_type']
105
106
107
108 - def from_entry(self, entry, info=None):
109 """
110 Allows the filling of the current instance from an Atom entry.
111
112 This will lookup for two links:
113 - rel='edit'
114 - rel='edit-media'
115
116 @type entry: L{amara.bindery.root_base}
117 @param entry: atom entry attached to the L{MemberResource} instance.
118 Sets the instance attribute to the values extracted from the entry.
119 """
120 self.entry = copy.deepcopy(entry).ownerDocument
121
122 entry.ownerDocument.xmlns_prefixes.update({ATOM10_PREFIX: ATOM10_NS,
123 ATOMPUB_PREFIX: ATOMPUB_NS})
124 self.member_id = self.generate_resource_id(entry=entry, info=info)
125 self.media_type = u'application/atom+xml;type=entry'
126 self.media_id = self.collection.convert_id(self.member_id)[-1]
127
128 edit_media_links = entry.xml_xpath('./atom:link[@rel="edit-media"]')
129 if edit_media_links:
130 if 'type' in edit_media_links[0].attributes:
131 self.media_type = unicode(edit_media_links[0].type)
132
133 self.draft = self.is_draft()
134
136 """
137 Sets the XSLT to associate to the Atom entry. A processing
138 instruction will be inserted when the C{self.xml()} method is
139 called.
140
141 @type path: string
142 @param path: URI to the the XSLT resource
143 """
144 self.xslt_path = path
145
146 - def xml(self, indent=False):
147 """
148 Serializes the entry as a string.
149
150 @type indent: bool
151 @param indent: indicates if the serialization should be indented
152
153 @rtype: string
154 @return: serialized representation of the atom entry
155 """
156 if self.xslt_path:
157 pi = amara.bindery.pi_base(u"xml-stylesheet", u'href="%s" type="text/xsl"' % self.xslt_path)
158
159 if isinstance(self.entry, amara.bindery.root_base):
160 d = self.entry.ownerDocument
161 d.xml_insert_before(d.childNodes[0], pi)
162 else:
163 d = amara.create_document()
164 d.xmlns_prefixes.update({ATOM10_PREFIX: ATOM10_NS,
165 ATOMPUB_PREFIX: ATOMPUB_NS})
166 d.xml_append(pi)
167 d.xml_append(self.entry)
168
169 return d.xml(indent=indent, force_nsdecls=d.xmlns_prefixes)
170
171 return self.entry.xml(indent=indent, force_nsdecls=self.entry.xmlns_prefixes)
172
174 """
175 Loads a source document into an amara instance or throws a
176 L{ResourceOperationException} if the operation failed.
177
178 @type source: string or file-like object
179 @param source: L{amara.bindery.root_base} instance of the document
180 """
181 try:
182 doc = amara.parse(source, prefixes={ATOM10_PREFIX: ATOM10_NS,
183 ATOMPUB_PREFIX: ATOMPUB_NS})
184 except SAXParseException, er:
185 raise ResourceOperationException("Could not parse posted Atom entry", 400, body=str(er))
186
187 doc.xmlns_prefixes[u'app'] = u"http://www.w3.org/2007/app"
188 doc.xmlns_prefixes[u'atom'] = u"http://www.w3.org/2005/Atom"
189
190 return doc
191
193 """
194 Inserts the collection categories into the Atom entry.
195 """
196 categories = self.collection.categories or None
197 if categories:
198 entry = self.entry.entry
199 doc = entry.ownerDocument
200 for category in categories:
201 entry.xml_append(doc.xml_create_element(qname(u"category", ATOM10_PREFIX),
202 ns=ATOM10_NS, attributes=category))
203
205 """
206 Sets the current instance atom entry published element value to the
207 one from the provided member.
208
209 @type other_member: L{MemberResource} or subclass
210 @param other_member: member from which the published date value must be taken from
211 """
212 entry = self.entry.entry
213 other_entry = other_member.atom.entry
214
215 pub = entry.xml_xpath('atom:published')
216 other_pub = other_entry.xml_xpath('atom:published')
217 if other_pub and pub:
218 entry.published = unicode(other_pub[0])
219 elif other_pub:
220 d = entry.ownerDocument
221 entry.xml_append(d.xml_create_element(qname(u"published", ATOM10_PREFIX),
222 ns=ATOM10_NS, content=unicode(other_pub[0])))
223
225 """
226 Sets the updated and edited date values to the current UTC time.
227 """
228 entry = self.entry.entry
229 isodate = get_isodate()
230 d = entry.ownerDocument
231
232 updated = entry.xml_xpath('atom:updated')
233 if not updated:
234 entry.xml_append(d.xml_create_element(qname(u"updated", ATOM10_PREFIX),
235 ns=ATOM10_NS))
236 entry.updated = isodate
237
238 edited = entry.xml_child_elements.get('edited')
239 if not edited:
240 entry.xml_append(d.xml_create_element(qname(u"edited", ATOMPUB_PREFIX),
241 ns=ATOMPUB_NS))
242 entry.edited = isodate
243
245 """
246 Sets the updated and edited date values to the values from the member
247 provided.
248
249 @type new_member: L{MemberResource} or subclass
250 @param new_member: member from which the date values must be taken from
251 """
252 entry = self.entry.entry
253 isodate = get_isodate()
254 d = entry.ownerDocument
255
256 updated = entry.xml_child_elements.get('updated')
257 if updated:
258 updated = isodate
259 else:
260 entry.xml_append(d.xml_create_element(qname(u"updated", ATOM10_PREFIX),
261 ns=ATOM10_NS, content=isodate))
262
263 new_entry = new_member.atom
264 new_edited = new_entry.xml_child_elements.get('edited')
265 if new_edited:
266 isodate = unicode(new_edited)
267 edited = entry.xml_child_elements.get('edited')
268 if edited:
269 edited = isodate
270 else:
271 entry.xml_append(d.xml_create_element(qname(u"edited", ATOMPUB_PREFIX),
272 ns=ATOMPUB_NS, content=isodate))
273
274
275 - def prepare_for_public(self, content=None, external_content=False,
276 rel=u'alternate', media_type=u'text/html',
277 xslt_path=None):
278 """
279 Generates and returns an atom entry that is appropriate to be used in
280 a public atom feed while retaining information from the member atom entry.
281
282 @type content: string (not unicode)
283 @param content: the XHTML content, as a string, to be set as the atom:content value
284
285 @type external_content: bool
286 @param external_content: if C{True}, the atom:content element will have
287 a C{src} attribute and no body. The URI is generated automatically.
288
289 @type rel: string
290 @param rel: defines the C{rel} attribute of the atom:link element
291
292 @type media_type: string
293 @param media_type: defines the C{type} attribute value of the atom:link element
294
295 @type xslt_path: string
296 @param xslt_path: URI of the XSLT associated with the entry as a processing-instruction
297
298 @rtype: L{amara.bindery.root_base}
299 @return: the amara instance representing the atom entry
300 """
301 clone = copy.deepcopy(self.entry)
302 if isinstance(clone, amara.bindery.root_base):
303 clone = clone.entry
304 d = clone.ownerDocument
305
306 xml_base = self.collection.get_xml_base()
307 if xml_base:
308 clone.xml_set_attribute((qname(u'base', XML_PREFIX), XML_NS), xml_base)
309
310 edited = clone.xml_child_elements.get('edited')
311 if edited: clone.xml_remove_child(edited)
312
313 control = clone.xml_child_elements.get('control')
314 if control: clone.xml_remove_child(control)
315
316 links = clone.xml_xpath('./atom:link[@rel="edit"]')
317 for link in links:
318 clone.xml_remove_child(link)
319
320 links = clone.xml_xpath('./atom:link[@rel="edit-media"]')
321 for link in links:
322 clone.xml_remove_child(link)
323
324
325 if xml_base:
326 href = safe_url_join([self.collection.base_uri, self.member_id])
327 else:
328 href = safe_url_join([self.collection.get_base_uri(), self.member_id])
329 links = clone.xml_xpath('./atom:link[@rel="self"]')
330 if links:
331 links[0].href = href
332 links[0].type = u'application/atom+xml;type=entry'
333 else:
334 clone.xml_append(d.xml_create_element(qname(u"link", ATOM10_PREFIX),
335 ns=ATOM10_NS,
336 attributes={u'rel': u'self',
337 u'type': u'application/atom+xml;type=entry',
338 u'href': href}))
339
340
341 if self.media_id:
342 if xml_base:
343 href = safe_url_join([self.collection.base_uri, self.media_id])
344 else:
345 href = self.public_uri
346
347 links = clone.xml_xpath('./atom:link[@rel="%s"]' % rel)
348 if links:
349 links[0].href = href
350 links[0].type = media_type
351 else:
352 clone.xml_append(d.xml_create_element(qname(u"link", ATOM10_PREFIX),
353 ns=ATOM10_NS,
354 attributes={u'rel': rel,
355 u'type': media_type,
356 u'href': href}))
357
358 ct = clone.xml_child_elements.get('content')
359 if ct: clone.xml_remove_child(ct)
360
361 i