67 |
67 |
68 Operations |
68 Operations |
69 ~~~~~~~~~~ |
69 ~~~~~~~~~~ |
70 |
70 |
71 Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` class |
71 Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` class |
72 that may be created by hooks and scheduled to happen just before (or after) the |
72 that may be created by hooks and scheduled to happen on `precommit`, |
73 `precommit`, `postcommit` or `rollback` event. Hooks are being fired immediately |
73 `postcommit` or `rollback` event (i.e. respectivly before/after a commit or |
74 on data operations, and it is sometime necessary to delay the actual work down |
74 before a rollback of a transaction). |
75 to a time where all other hooks have run. Also while the order of execution of |
75 |
76 hooks is data dependant (and thus hard to predict), it is possible to force an |
76 Hooks are being fired immediately on data operations, and it is sometime |
77 order on operations. |
77 necessary to delay the actual work down to a time where we can expect all |
|
78 information to be there, or when all other hooks have run (though take case |
|
79 since operations may themselves trigger hooks). Also while the order of |
|
80 execution of hooks is data dependant (and thus hard to predict), it is possible |
|
81 to force an order on operations. |
|
82 |
|
83 So, for such case where you may miss some information that may be set later in |
|
84 the transaction, you should instantiate an operation in the hook. |
78 |
85 |
79 Operations may be used to: |
86 Operations may be used to: |
80 |
87 |
81 * implements a validation check which needs that all relations be already set on |
88 * implements a validation check which needs that all relations be already set on |
82 an entity |
89 an entity |
246 |
253 |
247 from warnings import warn |
254 from warnings import warn |
248 from logging import getLogger |
255 from logging import getLogger |
249 from itertools import chain |
256 from itertools import chain |
250 |
257 |
251 from logilab.common.decorators import classproperty |
258 from logilab.common.decorators import classproperty, cached |
252 from logilab.common.deprecation import deprecated, class_renamed |
259 from logilab.common.deprecation import deprecated, class_renamed |
253 from logilab.common.logging_ext import set_log_methods |
260 from logilab.common.logging_ext import set_log_methods |
254 |
261 |
255 from cubicweb import RegistryNotFound |
262 from cubicweb import RegistryNotFound |
256 from cubicweb.vregistry import classid |
263 from cubicweb.vregistry import classid |
257 from cubicweb.cwvreg import CWRegistry, VRegistry |
264 from cubicweb.cwvreg import CWRegistry, VRegistry |
258 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector, |
265 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector, |
259 is_instance) |
266 is_instance) |
260 from cubicweb.appobject import AppObject |
267 from cubicweb.appobject import AppObject, NotSelector, OrSelector |
261 from cubicweb.server.session import security_enabled |
268 from cubicweb.server.session import security_enabled |
262 |
269 |
263 ENTITIES_HOOKS = set(('before_add_entity', 'after_add_entity', |
270 ENTITIES_HOOKS = set(('before_add_entity', 'after_add_entity', |
264 'before_update_entity', 'after_update_entity', |
271 'before_update_entity', 'after_update_entity', |
265 'before_delete_entity', 'after_delete_entity')) |
272 'before_delete_entity', 'after_delete_entity')) |
316 entities = [] |
323 entities = [] |
317 eids_from_to = kwargs.pop('eids_from_to') |
324 eids_from_to = kwargs.pop('eids_from_to') |
318 else: |
325 else: |
319 entities = [] |
326 entities = [] |
320 eids_from_to = [] |
327 eids_from_to = [] |
|
328 pruned = self.get_pruned_hooks(session, event, |
|
329 entities, eids_from_to, kwargs) |
321 # by default, hooks are executed with security turned off |
330 # by default, hooks are executed with security turned off |
322 with security_enabled(session, read=False): |
331 with security_enabled(session, read=False): |
323 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs): |
332 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs): |
324 hooks = sorted(self.possible_objects(session, **_kwargs), |
333 hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs), |
325 key=lambda x: x.order) |
334 key=lambda x: x.order) |
326 with security_enabled(session, write=False): |
335 with security_enabled(session, write=False): |
327 for hook in hooks: |
336 for hook in hooks: |
328 #print hook.category, hook.__regid__ |
337 hook() |
329 hook() |
338 |
|
339 def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs): |
|
340 """return a set of hooks that should not be considered by filtered_possible objects |
|
341 |
|
342 the idea is to make a first pass over all the hooks in the |
|
343 registry and to mark put some of them in a pruned list. The |
|
344 pruned hooks are the one which: |
|
345 |
|
346 * are disabled at the session level |
|
347 * have a match_rtype or an is_instance selector which does not |
|
348 match the rtype / etype of the relations / entities for |
|
349 which we are calling the hooks. This works because the |
|
350 repository calls the hooks grouped by rtype or by etype when |
|
351 using the entities or eids_to_from keyword arguments |
|
352 |
|
353 Only hooks with a simple selector or an AndSelector of simple |
|
354 selectors are considered for disabling. |
|
355 |
|
356 """ |
|
357 if 'entity' in kwargs: |
|
358 entities = [kwargs['entity']] |
|
359 if len(entities): |
|
360 look_for_selector = is_instance |
|
361 etype = entities[0].__regid__ |
|
362 elif 'rtype' in kwargs: |
|
363 look_for_selector = match_rtype |
|
364 etype = None |
|
365 else: # nothing to prune, how did we get there ??? |
|
366 return set() |
|
367 cache_key = (event, kwargs.get('rtype'), etype) |
|
368 pruned = session.pruned_hooks_cache.get(cache_key) |
|
369 if pruned is not None: |
|
370 return pruned |
|
371 pruned = set() |
|
372 session.pruned_hooks_cache[cache_key] = pruned |
|
373 if look_for_selector is not None: |
|
374 for id, hooks in self.iteritems(): |
|
375 for hook in hooks: |
|
376 enabled_cat, main_filter = hook.filterable_selectors() |
|
377 if enabled_cat is not None: |
|
378 if not enabled_cat(hook, session): |
|
379 pruned.add(hook) |
|
380 continue |
|
381 if main_filter is not None: |
|
382 if isinstance(main_filter, match_rtype) and \ |
|
383 (main_filter.frometypes is not None or \ |
|
384 main_filter.toetypes is not None): |
|
385 continue |
|
386 first_kwargs = _iter_kwargs(entities, eids_from_to, kwargs).next() |
|
387 if not main_filter(hook, session, **first_kwargs): |
|
388 pruned.add(hook) |
|
389 return pruned |
|
390 |
|
391 |
|
392 def filtered_possible_objects(self, pruned, *args, **kwargs): |
|
393 for appobjects in self.itervalues(): |
|
394 if pruned: |
|
395 filtered_objects = [obj for obj in appobjects if obj not in pruned] |
|
396 if not filtered_objects: |
|
397 continue |
|
398 else: |
|
399 filtered_objects = appobjects |
|
400 obj = self._select_best(filtered_objects, |
|
401 *args, **kwargs) |
|
402 if obj is None: |
|
403 continue |
|
404 yield obj |
330 |
405 |
331 class HooksManager(object): |
406 class HooksManager(object): |
332 def __init__(self, vreg): |
407 def __init__(self, vreg): |
333 self.vreg = vreg |
408 self.vreg = vreg |
334 |
409 |
651 takes a session object as first argument (accessible as `.session` from the |
735 takes a session object as first argument (accessible as `.session` from the |
652 operation instance), and optionally all keyword arguments needed by the |
736 operation instance), and optionally all keyword arguments needed by the |
653 operation. These keyword arguments will be accessible as attributes from the |
737 operation. These keyword arguments will be accessible as attributes from the |
654 operation instance. |
738 operation instance. |
655 |
739 |
656 An operation is triggered on connections pool events related to |
740 An operation is triggered on connections set events related to commit / |
657 commit / rollback transations. Possible events are: |
741 rollback transations. Possible events are: |
658 |
742 |
659 * `precommit`: |
743 * `precommit`: |
660 |
744 |
661 the transaction is being prepared for commit. You can freely do any heavy |
745 the transaction is being prepared for commit. You can freely do any heavy |
662 computation, raise an exception if the commit can't go. or even add some |
746 computation, raise an exception if the commit can't go. or even add some |
726 % classid(self.__class__), DeprecationWarning) |
810 % classid(self.__class__), DeprecationWarning) |
727 self.commit_event() |
811 self.commit_event() |
728 getattr(self, event)() |
812 getattr(self, event)() |
729 |
813 |
730 def precommit_event(self): |
814 def precommit_event(self): |
731 """the observed connections pool is preparing a commit""" |
815 """the observed connections set is preparing a commit""" |
732 |
816 |
733 def revertprecommit_event(self): |
817 def revertprecommit_event(self): |
734 """an error went when pre-commiting this operation or a later one |
818 """an error went when pre-commiting this operation or a later one |
735 |
819 |
736 should revert pre-commit's changes but take care, they may have not |
820 should revert pre-commit's changes but take care, they may have not |
737 been all considered if it's this operation which failed |
821 been all considered if it's this operation which failed |
738 """ |
822 """ |
739 |
823 |
740 def rollback_event(self): |
824 def rollback_event(self): |
741 """the observed connections pool has been rollbacked |
825 """the observed connections set has been rollbacked |
742 |
826 |
743 do nothing by default, the operation will just be removed from the pool |
827 do nothing by default |
744 operation list |
|
745 """ |
828 """ |
746 |
829 |
747 def postcommit_event(self): |
830 def postcommit_event(self): |
748 """the observed connections pool has committed""" |
831 """the observed connections set has committed""" |
749 |
832 |
750 @property |
833 @property |
751 @deprecated('[3.6] use self.session.user') |
834 @deprecated('[3.6] use self.session.user') |
752 def user(self): |
835 def user(self): |
753 return self.session.user |
836 return self.session.user |