1 """
2 I define the common super-class of all package element classes.
3 """
4
5 import re
6 from itertools import islice
7
8 from advene.model.consts import _RAISE
9 from advene.model.core.meta import WithMetaMixin
10 from advene.model.events import ElementEventDelegate, WithEventsMixin
11 from advene.model.exceptions import ModelError, UnreachableImportError, \
12 NoSuchElementError
13 from advene.model.tales import tales_property, tales_use_as_context,\
14 WithAbsoluteUrlMixin as WithAbsUrlMixin
15 from advene.util.alias import alias
16 from advene.util.autoproperty import autoproperty
17 from advene.util.session import session
18
19
20
21 MEDIA = 'm'
22 ANNOTATION = 'a'
23 RELATION = 'r'
24 TAG = 't'
25 LIST = 'l'
26 IMPORT = 'i'
27 QUERY = 'q'
28 VIEW = 'v'
29 RESOURCE = 'R'
30
31 _package_event_template = {
32 MEDIA : 'media::%s',
33 ANNOTATION : 'annotation::%s',
34 RELATION : 'relation::%s',
35 TAG : 'tag::%s',
36 LIST : 'list::%s',
37 IMPORT : 'import::%s',
38 QUERY : 'query::%s',
39 VIEW : 'view::%s',
40 RESOURCE : 'resource::%s',
41 }
42
43 -class PackageElement(WithMetaMixin, WithEventsMixin, WithAbsUrlMixin, object):
44 """
45 I am the common subclass of all package element.
46
47
48 Package elements are unique volatile instances:
49
50 * unique, because it is enforced that the same element will never
51 be represented at a given time by two distinct instances; hence,
52 elements can be compared with the ``is`` operator as well as
53 ``==``
54
55 * volatile, because it is not guaranteed that, at two instants, the
56 instance representing a given element will be the same; unused instances
57 may be freed at any time, and a new instance will be created on demand.
58
59 This should normally normally be transparent for the user.
60
61 Developper note
62 ===============
63 So that volatility is indeed transparent to users, the `__setattr__` method
64 has been overridden: since custom attributes are not stored in the backend,
65 the instance should be kept in memory as long as it has custom attributes.
66
67 As a consequence, all "non-custom" attributes (i.e. those that will be
68 correctly re-generated when the element is re-instantiated) must be
69 declared as class attribute (usually with None as their default value).
70
71 This must also be true of subclasses of elements (NB: mixin classes should
72 normally already do that).
73 """
74
75
76
77 _id = None
78 _owner = None
79 _weight = 0
80
82 """
83 Must not be used directly, nor overridden.
84 Use class methods instantiate or create_new instead.
85 """
86 self._id = id
87 self._owner = owner
88 self._weight = 0
89 owner._elements[id] = self
90
91 @classmethod
93 """
94 Factory method to create an instance from backend data.
95
96 This method expect the exact data from the backend, so it does not
97 need to be tolerant or to check consistency (the backend is assumed to
98 be sane).
99 """
100 r = cls(owner, id)
101 return r
102
111
119
120 @classmethod
122 """
123 Factory method to create a new instance both in memory and backend.
124
125 This method will usually perform checks and conversions from its actual
126 arguments to the data expected to the backend. It is responsible for
127 1/ storing the data in the backend and 2/ initializing the instance
128 (for which it should reuse instantiate to reduce redundancy).
129
130 Note that this method *should* be tolerant w.r.t. its parameters,
131 especially accepting both element instances or ID-refs.
132
133 NB: this method does nothing and must not be invoked by superclasses
134 (indeed, it raises an exception).
135 """
136 raise NotImplementedError("must be overridden in subclasses")
137
138 @staticmethod
140 """
141 Raise a ModelError if element is not referenceable by pkg, and (if
142 provided) if it has not the given type. Furthermore, if required is set
143 to True, raise a ModelError if element is None (else None is silently
144 ignored).
145
146 Note that element may be a strict ID-ref, in which case this method
147 will do its best to check its type, but will *succeed silently* if the
148 element is unreachable (because parsers need to be able to add
149 unreachable elements).
150
151 Also, return the ID-ref of that element in this element's owner package,
152 for this information is usually useful in the situations where a check
153 is performed. If element is None, return "".
154 """
155 if element is None or element == "":
156 if required:
157 raise ModelError("required element")
158 else:
159 return ""
160
161 if isinstance(element, basestring):
162 assert element.find(":") > 0
163 element_id = element
164 element = pkg.get(element_id)
165 if element is not None and element.ADVENE_TYPE != type:
166 raise ModelError("type mismatch", element, type)
167 else:
168
169 return element_id
170
171 assert isinstance(element, PackageElement)
172 if type is not None:
173 elttype = element.ADVENE_TYPE
174 if elttype != type:
175 raise ModelError("type mismatch", element, type)
176 if not pkg._can_reference(element):
177 raise ModelError("can not reference", pkg, element)
178 return element.make_id_in(pkg)
179
181 """Compute an id-ref for this element in the context of the given package.
182 """
183 if self._owner is pkg:
184 return self._id
185
186
187 queue = pkg._imports_dict.items()
188 current = 0
189 visited = {pkg:True}
190 parent = {}
191 found = False
192 while not found and current < len(queue):
193 prefix,p = queue[current]
194 if p is self._owner:
195 found = True
196 else:
197 if p is not None:
198 visited[p] = True
199 for prefix2,p2 in p._imports_dict.iteritems():
200 if p2 not in visited:
201 queue.append((prefix2,p2))
202 parent[(prefix2,p2)] = (prefix,p)
203 current += 1
204 if not found:
205 raise ValueError("Element is not reachable from that package")
206 r = self._id
207 c = queue[current]
208 while c is not None:
209 r = "%s:%s" % (c[0], r)
210 c = parent.get(c)
211 return r
212
214 """
215 Iter over all references that are made to this element.
216
217 A reference is represented by a tuple of the form
218 * ('item', list)
219 * ('member', relation)
220 * ('meta', package_or_element, key)
221 * ('tagged', package, tag)
222 * ('tagging', package, element) -- for tags only
223 * (attribute_name, other_element)
224
225 References are searched in the given package. If no package is given,
226 references are searched in this element's owner package and in all
227 packages that are currently loaded and directly importing this
228 packages.
229 """
230 o = self._owner
231 if package is None:
232 referrers = o._get_referrers()
233 else:
234 referrers = {package._backend : {package._id : package}}
235 for be, d in referrers.iteritems():
236 for pid, eid, rel in be.iter_references(d, self._get_uriref()):
237 yield Reference(self, d[pid], eid, rel)
238
240 """
241 Delete this element.
242
243 If the element is known to be referenced by other elements,
244 all remaining references are cut (but the referer elements are
245 not deleted). Note that this does not guarantees that some
246 references the the deleted element will not continue to exist in
247 packages that are not currently loaded.
248 """
249 self.emit("pre-deleted")
250 for r in self.iter_references():
251 r.cut()
252 self._owner._backend.delete_element(self._owner._id, self._id,
253 self.ADVENE_TYPE)
254 del self._owner._elements[self._id]
255 self.emit("deleted")
256 self.__class__ = DeletedPackageElement
257
258 @autoproperty
260 """
261 The identifier of this element in the context of its owner package.
262 """
263 return self._id
264
265 @autoproperty
267 """
268 Rename this element to `new_id`, if it is not already in use in the
269 package, else raises an AssertionError.
270 """
271 o = self._owner
272 importers = o._importers
273 assert not o.has_element(new_id)
274 old_id = self._id
275 self.emit("pre-renamed")
276
277 o._backend.rename_element(o._id, old_id, self.ADVENE_TYPE, new_id)
278
279 old_uriref = self.uriref
280 ibe_dict = o._get_referrers()
281 for be, d in ibe_dict.iteritems():
282 be.rename_references(d, old_uriref, new_id)
283
284 del o._elements[old_id]
285 o._elements[new_id] = self
286 self._id = new_id
287
288 new_uriref = self._get_uriref()
289 for be, d in ibe_dict.iteritems():
290 for pid, eid, rel in be.iter_references(d, new_uriref):
291 p = d[pid]
292 prefix = importers.get(p, "")
293 old_idref = prefix and "%s:%s" % (prefix, old_id) or old_id
294 new_idref = prefix and "%s:%s" % (prefix, new_id) or new_id
295 if eid == "":
296 p._update_caches(old_idref, new_idref, self, rel)
297 else:
298 e = p._elements.get(eid)
299 if e is not None:
300 e._update_caches(old_idref, new_idref, self, rel)
301
302
303
304
305
306 if self.ADVENE_TYPE is IMPORT:
307 del o._imports_dict[old_id]
308 o._imports_dict[new_id] = self._imported
309 self._imported._importers[o] = new_id
310 for eid, rel, ref \
311 in o._backend.iter_references_with_import(o._id, new_id):
312 old_idref = "%s:%s" % (old_id, ref)
313 new_idref = "%s:%s" % (new_id, ref)
314 if eid == "":
315 o._update_caches(old_idref, new_idref, None, rel)
316 else:
317 e = o._elements.get(eid)
318 if e is not None:
319 e._update_caches(old_idref, new_idref, None, rel)
320 self.emit("renamed")
321
323 """
324 This cooperative method is used to update all caches when an element
325 in the cache is renamed. The old_idref and new_idref are provided,
326 as well as the relation (as represented by backend methods
327 `iter_references` and `iter_references_with_import`) with this element.
328 The renamed element may be provided or be None, depending on the
329 situation.
330 """
331 super(PackageElement, self) \
332 ._update_caches(old_idref, new_idref, element, relation)
333
334
335 @autoproperty
337 """
338 The URI-ref identifying this element.
339
340 It is built from the URI of its owner package, suffixed with the id
341 of the element as a fragment-id (#).
342 """
343 o = self._owner
344 u = o._uri or o._url
345 return "%s#%s" % (u, self._id)
346
347 @autoproperty
349 """
350 The package containing (or owner package) this element.
351 """
352 return self._owner
353
354
355
370
372 """Iter over the id-refs of the tags associated with this element in
373 ``package``.
374
375 If ``package`` is not set, the session variable ``package`` is used
376 instead. If the latter is not set, a TypeError is raised.
377
378 If ``inherited`` is set to False, the tags associated by imported
379 packages of ``package`` will not be yielded.
380
381 See also `iter_my_tags`.
382 """
383
384
385 if package is None:
386 package = session.package
387 if package is None:
388 raise TypeError("no package set in session, must be specified")
389 u = self._get_uriref()
390 if not inherited:
391 pids = (package._id,)
392 get_element = package.get_element
393 for pid, tid in package._backend.iter_tags_with_element(pids, u):
394 if _get:
395 y = package.get_element(tid, None)
396 else:
397 y = tid
398 yield y
399 else:
400 for be, pdict in package._backends_dict.iteritems():
401 for pid, tid in be.iter_tags_with_element(pdict, u):
402 p = pdict[pid]
403 if _get:
404 y = p.get_element(tid, None)
405 else:
406 y = package.make_id_for(p, tid)
407 yield y
408
409 @alias(iter_my_tag_ids)
419
421 """Iter over all the packages associating this element to ``tag``.
422
423 ``package`` is the top-level package. If not provided, the ``package``
424 session variable is used. If the latter is unset, a TypeError is
425 raised.
426 """
427 if package is None:
428 package = session.package
429 if package is None:
430 raise TypeError("no package set in session, must be specified")
431 eu = self._get_uriref()
432 tu = tag._get_uriref()
433 for be, pdict in package._backends_dict.iteritems():
434 for pid in be.iter_taggers(pdict, eu, tu):
435 yield pdict[pid]
436
437 - def has_tag(self, tag, package=None, inherited=True):
438 """Is this element associated to ``tag`` by ``package``.
439
440 If ``package`` is not provided, the ``package`` session variable is
441 used. If the latter is unset, a TypeError is raised.
442
443 If ``inherited`` is set to False, only return True if ``package``
444 itself associates this element to ``tag``; else return True also if
445 the association is inherited from an imported package.
446 """
447 if package is None:
448 package = session.package
449 if package is None:
450 raise TypeError("no package set in session, must be specified")
451 if not inherited:
452 eu = self._get_uriref()
453 tu = tag._get_uriref()
454 it = package._backend.iter_taggers((package._id,), eu, tu)
455 return bool(list(it))
456 else:
457 return list(self.iter_taggers(tag, package))
458
459
460
462 """
463 Elements are created with weight 0. Increasing its weight is equivalent
464 to creating a strong reference to it, making it not volatile. Once the
465 reason for keeping the element is gone, the weight should be decreased
466 again with `_decrease_weight`.
467 """
468
469 self._weight += 1
470 if self._weight == 1:
471 self._owner._heavy_elements.add(self)
472
474 """
475 :see: _increase_weight
476 """
477
478 self._weight -= 1
479 if self._weight == 0:
480 self._owner._heavy_elements.remove(self)
481
482
483
489
490 - def emit(self, detailed_signal, *args):
491 """
492 Override WithEventsMixin.emit in order to automatically emit the
493 package signal corresponding to each element signal.
494 """
495 WithEventsMixin.emit(self, detailed_signal, *args)
496 def lazy_params():
497 colon = detailed_signal.find(":")
498 if colon > 0: s = detailed_signal[:colon]
499 else: s = detailed_signal
500 yield _package_event_template[self.ADVENE_TYPE] % s
501 yield self
502 yield s
503 yield args
504 self._owner.emit_lazy(lazy_params)
505
506 - def connect(self, detailed_signal, handler, *args):
507 """
508 Connect a handler to a signal.
509
510 Note that an element with connected signals becomes heavier (i.e. less
511 volatile).
512
513 :see: `WithEventsMixin.connect`
514 """
515 r = super(PackageElement, self).connect(detailed_signal, handler, *args)
516 self._increase_weight()
517 return r
518
520 """
521 Disconnect a handler from a signal.
522
523 :see: `connect`
524 :see: `WithMetaMixin.disconnect`
525 """
526 r = super(PackageElement, self).disconnect(handler_id)
527 self._decrease_weight()
528 return r
529
531 """
532 This alternative to `connect` can only be used by the element itself.
533 It connects the handler to the signal but *does not* make the element
534 heavier (since if the handler will disappear at the same time as the
535 element...).
536 """
537 return super(PackageElement, self) \
538 .connect(detailed_signal, handler, *args)
539
540
541 @tales_property
542 @tales_use_as_context("package")
547 return TagCollection(self._owner)
548
550 base = self._owner._compute_absolute_url(aliases)
551 if base[-8:] == "/package":
552
553 return "%s:%s" % (base[:-8], self._id)
554 else:
555 return "%s/%s" % (base, self._id)
556
557 @tales_property
559 """Return a concise representation for the element.
560 """
561 c=context.globals['options']['controller']
562 return c.get_title(self)
563
564 @tales_property
566 """Return the color of the element.
567 """
568 c=context.globals['options']['controller']
569 col=c.get_element_color(self)
570 if col is None:
571 return col
572 m=re.search('#(..)..(..)..(..)..', col)
573 if m:
574
575
576 return '#'+''.join(m.groups())
577 else:
578 return col
579
581 """
582 I am just a dummy class to which deleted elements are mutated.
583
584 That way, they are no longer usable, preventing their owner from
585 unknowingly handling an element that has actually been deleted.
586
587 Note however that good practices should be to register to the deletion
588 event on the elements you reference, so as to be notified as soon as they
589 are deleted.
590 """
591 pass
592
595 """
596 A base-class for coder-friendly and TAL-friendly element collections.
597
598 Subclasses must override either __iter__ or both __len__ and __getitem__.
599
600 In most cases, it is a good idea to override __contains__, and __len__
601 (even if the subclass is overriding __iter__).
602
603 The class attribute _allow_filtering can also be overridden to disallow
604 the use of the filter method.
605 """
607 """
608 Initialise the element collection.
609
610 `owner_package`is used only in the `get` method, to provide a context
611 to the ID-ref.
612 """
613 self._owner = owner_package
614
616 try:
617 o=tuple(other)
618 except TypeError:
619 return False
620 return tuple(self) == tuple(other)
621
623 """
624 Default implementation relying on __len__ and __getitem__.
625 """
626 for i in xrange(len(self)):
627 yield self[i]
628
630 """
631 Default (and inefficient) implementation relying on __iter__.
632 """
633 return len(list(self))
634
636 """
637 Default implementation relying on __iter__.
638 """
639 if isinstance(key, (int, long)):
640 if key >= 0:
641 for i,j in enumerate(self):
642 if i == key:
643 return j
644 raise IndexError, key
645 else:
646 return list(self)[key]
647 elif isinstance(key, slice):
648 if key.step is None or key.step > 0:
649 key = key.indices(self.__len__())
650 return list(islice(self, *key))
651 else:
652 return list(self)[key]
653 else:
654 r = self.get(key)
655 if r is None:
656 raise KeyError(key)
657 return r
658
660 return "[" + ",".join(self.keys()) + "]"
661
662 - def get(self, key, default=None):
663 e = self._owner.get(key)
664 if e is None:
665 return default
666 elif e in self:
667 return e
668 else:
669 return default
670
673
674 _allow_filter = True
675
676 - def filter(collection, **kw):
677 """
678 Use underlying iter method with the given keywords to make a filtered
679 version of that collection.
680 """
681 if not collection._allow_filter:
682 raise TypeError("filtering is not allowed on %r") % collection
683 class FilteredCollection(ElementCollection):
684 def __iter__ (self):
685 return collection.__iter__(**kw)
686 def __len__(self):
687 return collection.__len__(**kw)
688 def filter(self, **kw):
689 raise NotImplementedError("can not filter twice")
690 return FilteredCollection(collection._owner)
691
692 @property
694 """Return the size of the group.
695 """
696 return self.__len__()
697
698 @property
700 try:
701 return self.__iter__().next()
702 except StopIteration:
703 return None
704
705 @property
707 class RestCollection(ElementCollection):
708 def __iter__(self):
709 it = self.__iter__()
710 it.next()
711 for i in it: yield i
712 def __len__(self):
713 return self.__len__()-1
714 def filter(self, **kw):
715 raise NotImplementedError("RestCollection can not be filtered")
716 return RestCollection(self)
717
719 """Wrap an ElementCollection around an existing list.
720 """
724
726 return len(self._wrapped)
727
730
733
735 """
736 An object representing a reference from an element or package to an
737 element.
738 """
739 - def __init__(self, referree, package, element_id, relation):
740 self._f = referree
741 self._p = package
742 self._e = element_id
743 self._r = relation
744
745 @property
747 eid = self._e
748 if eid == "":
749 return self._p
750 else:
751 return self._p.get(eid, _RAISE)
752
753 @property
755 return self._r.split(" ")[0]
756
757 @property
759 L = self._r.split(" ")
760 if len(L) == 1:
761 return None
762 elif L[0] in (":item", ":member"):
763 return int(L[1])
764 elif L[0].startswith(":tag"):
765 try:
766 return self._p.get(L[1])
767 except UnreachableImportError:
768 return L[1]
769 except NoSuchElementError:
770 return L[1]
771 else:
772 return L[1]
773
775 L = self._r.split(" ")
776 typ = L[0]
777 referrer = self.referrer
778 if typ in (":item", ":member"):
779 del referrer[int(L[1])]
780 elif typ == ":tag":
781 p = self._p
782 tagged = p.get(L[1]) or L[1]
783 p.dissociate_tag(tagged, self._f)
784 elif typ == ":tagged":
785 p = self._p
786 tag = p.get(L[1]) or L[1]
787 p.dissociate_tag(self._f, tag)
788 elif typ == ":meta":
789 referrer.del_meta(L[1])
790 else:
791 setattr(referrer, typ, None)
792
794 raise NotImplementedError()
795