1
2
3 __doc__ = """
4 Collections are the container of member and media resources.
5
6 Collections are important to amplee as they will map eventually
7 to a physical container in the storage.
8
9 For instance a collection named 'notes' would map to:
10
11 - a table named 'notes' in a database
12 - a directory names 'notes' on the filesystem
13 - a bucket named 'notes' on Amazon S3
14
15 etc.
16
17 So it is very important to carefully choose the 'name_or_id' parameter
18 in the class AtomPubCollection
19 """
20
21 from bridge import Element, Attribute
22 from bridge.common import ATOM10_PREFIX, ATOMPUB_PREFIX, XML_PREFIX, XML_NS, \
23 ATOM10_NS, ATOMPUB_NS
24
25 from amplee.utils import generate_uuid_uri, get_isodate
26 from amplee.atompub.member import EntryMember
27 from amplee.error import UnsupportedMediaType
28
30 - def __init__(self, workspace, name_or_id, title, base_uri,
31 base_edit_uri, base_media_edit_uri=None, xml_attrs=None,
32 member_extension=u'atom', member_media_type=u'application/atom+xml',
33 accept_media_types=None, categories=None,
34 fixed_categories=None, favorite=False):
35 """
36 Atom Publishing Protocol collection handler.
37
38 Keyword arguments:
39 workspace -- AtomPubWorkspace instance carrying this collection
40 name_or_id -- Name or identifier by which you reference this
41 collection within the store [eg: 'audio']
42 base_uri -- Base URI of the content used in links and external content
43 base_edit_uri -- Base URI used for the edit links
44 base_media_edit_uri -- Base URI used for the edit-media links. If None will
45 use base_edit_uri.
46 xml_attrs -- dictionary of XML attributes belonging to the
47 xml namespace (http://www.w3.org/XML/1998/namespace). They will be passed
48 to the top level Atom elements (feed, entry, etc.)
49 member_extension -- Extension used for the Atom entries representing
50 the member resource (default to 'atom')
51 member_media_type -- Media type for the Atom entries representing the
52 member resource default to 'application/atom+xml')
53 accept_media_types -- List of a strings of acceptable media-types for
54 this collection
55 categories -- List of bridge.Element instances
56 fixed_categories -- True => 'yes', False => 'no', None => undefined
57 favorite -- if True means thiscollection is the preferred one in the workspace
58
59 Once an instance created you can set on_create, on_update and on_delete
60 attributes to a callback that will be applied before the
61 new member is being attached to the collection.
62
63 If the callback raise amplee.error.ResourceOperationException
64 it will not attach the resource but instead will force the
65 HTTP method handler to returns the error code and message
66 defines in the exception. This allows extra processing on the
67 member and resource before it being stored.
68
69 on_create take two arguments:
70 amplee.atompub.member.* -- newly created or existing member
71 string object -- new resource content
72 returns member, content to be persisted into the store
73
74 on_update takes three arguments:
75 amplee.atompub.member.EntryMember -- existing member in the store
76 amplee.atompub.member.* -- newly created or existing member
77 string object -- new resource content
78 returns member, content to be persisted into the store
79
80 on_delete takes:
81 amplee.atompub.member.* -- existing member
82 returns None
83
84 Becareful when you update a resource, the on_update member provides
85 the existing entry member as well as the new member generated frm the
86 updated content provided. Amplee will not try to be smarter than you.
87 This means that if your callback does nothing at all the existing
88 member will be entirely replaced by the one generated. It would be wise
89 that your callback keeps at least the id and published elements.
90
91 For instance:
92
93 def on_update_cb(existing_member, new_member, new_content):
94 # Ensure that atom:id stays the same throughout of the
95 # life of the resource
96 if new_member.atom.has_element('id', ATOM10_NS):
97 del new_member.atom.id
98 Element(u'id', content=unicode(existing_member.atom.id),
99 prefix=new_member.atom.xml_prefix, namespace=new_member.atom.xml_ns,
100 parent=new_member.atom)
101
102 if new_member.atom.has_element('published', ATOM10_NS):
103 del new_member.atom.published
104 Element(u'published', content=unicode(existing_member.atom.published),
105 prefix=new_member.atom.xml_prefix, namespace=new_member.atom.xml_ns,
106 parent=new_member.atom)
107
108 new_member.member_id = existing_member.member_id
109 new_member.media_id = existing_member.media_id
110
111 return new_member, new_content
112
113 Although this seems heavy work it allows the developer to have a very fine
114 granularity over resources.
115 """
116 self.workspace = workspace
117 self.workspace.collections.append(self)
118 self.name_or_id = name_or_id
119 self.base_uri = base_uri
120 xml_attrs = xml_attrs or {}
121 self.xml_base = xml_attrs.get('base', None)
122 self.xml_id = xml_attrs.get('id', None)
123 self.xml_lang = xml_attrs.get('lang', None)
124 self.title = title
125 self.categories = categories
126 self.favorite = favorite
127 self.fixed_categories = fixed_categories
128 self.member_extension = member_extension
129 self.member_media_type = member_media_type
130 self.base_edit_uri = base_edit_uri
131 if not base_media_edit_uri:
132 base_media_edit_uri = base_edit_uri
133 self.base_media_edit_uri = base_media_edit_uri
134 self.members = {}
135
136 if not accept_media_types:
137 accept_media_types = []
138 if isinstance(accept_media_types, basestring):
139 accept_media_types = [accept_media_types]
140 accept_media_types.append('application/atom+xml')
141 self.accept_media_types = accept_media_types
142
143
144
145 self.on_create = None
146 self.on_update = None
147 self.on_delete = None
148
154 store = property(store_container)
155
156 - def attach(self, member, member_content,
157 member_id=None, member_path=None,
158 media_id=None, media_path=None,
159 media_content=None, check_media_type=True):
160 """
161 Add a member to this collection by
162 * adding it to the store
163 * adding it to the self.members dictionary
164
165 You are not forced to pass the resource content through this
166 method if you prefer storing it in a different location
167 without using amplee.
168
169 This method will throw a amplee.error.UnsupportedMediaType
170 exception if check_media_type is True and the media type of
171 the media resource does not fall into the allowed list.
172
173 Keyword arguments:
174 member -- amplee.atompub.member.* instance
175 member_content -- Content to be persisted into the storage.
176 Usually an XML string of the Atom entry
177 member_id -- Internal id used to reference this member.
178 Usually equals to the resource_name + collection.member_extension
179 member_path -- Path under this member is stored
180 media_id -- Internal id used to reference the resource associated
181 to this member.
182 media_path -- Path under the media resource is stored
183 media_content -- Resource content
184 check_media_type -- If False the resource media type will not be
185 checked against collection.accept_media_types
186 """
187 if not member_id:
188 member_id = member.member_id
189 if not media_id:
190 media_id = member.media_id
191
192 if media_content and check_media_type:
193 if member.media_type not in self.accept_media_types:
194 raise UnsupportedMediaType
195
196 if not member_path:
197 member_path = self.get_meta_data_path(member_id)
198
199 self.store.add_meta_data(member_path, member_content,
200 media_type=self.member_media_type,
201 member_id=member_id,
202 media_id=media_id,
203 collection_name=self.name_or_id)
204 if media_content:
205 if not media_path:
206 media_path = self.get_content_path(media_id)
207
208 self.store.add_content(media_path, media_content,
209 media_type=member.media_type,
210 member_id=member_id,
211 media_id=media_id,
212 collection_name=self.name_or_id)
213
214 self.members[member_id] = member
215
216 - def prune(self, member_id=None, media_id=None):
217 """
218 Removes a member from a collection and the underlying stor.
219 Removes only objects passed in the id parameters.
220
221 Keyword arguments:
222 member_id -- Identifier of the member
223 media_id -- Identifier of the media resource
224 """
225 if member_id:
226 if member_id in self.members:
227 del self.members[member_id]
228 path = self.get_meta_data_path(member_id)
229 self.store.remove_meta_data(path)
230
231 if media_id:
232 path = self.get_content_path(media_id)
233 self.store.remove_content(path)
234
236 """
237 Take the parameter and returns a tuple such as (member_id, media_id).
238
239 Keyword arguments:
240 id -- Can be either member_id or media_id
241
242 """
243 ext = '.%s' % self.member_extension
244 pos_ext = 0 - len(ext)
245 if id[pos_ext:] == ext:
246 return (id, id[:pos_ext])
247 return ('%s.%s' % (id, self.member_extension), id)
248
250 """
251 Does a path belong to the store
252 """
253 return self.store.exists(path)
254
261
262 - def get_content_path(self, id):
263 """
264 Constructs and returns the path to the resource
265 pointed by the id parameter.
266 """
267 return self.store.get_content_path(self.name_or_id, id)
268
277
278 - def get_content(self, path):
279 """
280 Returns the content of the media resource or None.
281
282 Does not check the existence of the resource.
283 """
284 return self.store.fetch_content(path)
285
286 - def to_feed(self, prefix=ATOM10_PREFIX, namespace=ATOM10_NS):
287 """
288 Transforms and returns the collection into an bridge.Element instance
289 """
290 feed = Element(u'feed', prefix=prefix, namespace=namespace)
291 uuid = unicode(generate_uuid_uri(seed=self.base_uri.encode('utf-8')))
292 Element(u'id', content=uuid, prefix=prefix, namespace=namespace, parent=feed)
293 Element(u'updated', content=get_isodate(),
294 prefix=prefix, namespace=namespace, parent=feed)
295 title = Element(u'title', content=unicode(self.title), attributes={u'type': u'text'},
296 prefix=prefix, namespace=namespace, parent=feed)
297 if self.xml_base:
298 Attribute(u'base', self.xml_base, prefix=XML_PREFIX,
299 namespace=XML_NS, parent=feed)
300 feed.entry = []
301 for name in self.members:
302 member = self.members[name]
303 member.atom.parent = feed
304 feed.xml_children.append(member.atom)
305 feed.entry.append(member.atom)
306
307 return feed
308 feed = property(to_feed)
309
310 - def to_collection(self, prefix=ATOMPUB_PREFIX, namespace=ATOMPUB_NS):
311 """
312 Tranforms and returns the collection into a bridge.Element instance
313 """
314 collection = Element(u'collection', attributes={u'href': self.base_edit_uri},
315 prefix=prefix, namespace=namespace)
316 if self.xml_base:
317 Attribute(u'base', self.xml_base, prefix=XML_PREFIX,
318 namespace=XML_NS, parent=collection)
319 Element(u'title', content=self.title, attributes={u'type': u'text'},
320 prefix=ATOM10_PREFIX, namespace=ATOM10_NS, parent=collection)
321 Element(u'accept', content=u','.join(self.accept_media_types),
322 prefix=prefix, namespace=namespace, parent=collection)
323 categories = self.categories or []
324 if categories:
325 attr = {}
326 if self.fixed_categories:
327 if self.fixed_categories: fixed = u'yes'
328 else: fixed = u'no'
329 attr[u'fixed'] = fixed
330 cats = Element(u'categories', attributes=attr, prefix=prefix,
331 namespace=namespace, parent=collection)
332 for category in categories:
333 Element(u'category', attributes={u'term': unicode(category.term)},
334 prefix=ATOM10_PREFIX, namespace=ATOM10_NS, parent=cats)
335
336 return collection
337 collection = property(to_collection)
338
340 """
341 Reloads every existing members into self.members.
342 Call this at server startup to refresh the collection.
343 Careful as this could be a fairly long process.
344 """
345 members = self.store.list_members(self.name_or_id, self.member_extension)
346 member_ext = None
347 if self.member_extension:
348 member_ext = '.%s' % self.member_extension
349 for member_id in members:
350 member_path = members[member_id]['path']
351 data = self.get_meta_data(member_path)
352
353 if data:
354 entry = Element.load(data)
355 member = EntryMember(self, id=member_id, atom=entry)
356 if member_ext:
357 pos = member.member_id.rfind(member_ext)
358 if pos == -1:
359 member.media_id = member.member_id
360 else:
361 member.media_id = member.member_id[:pos]
362 else:
363 member.media_id = member.member_id
364 self.members[member_id] = member
365