201 |
201 |
202 It is sometimes convenient to explicitly enable or disable some hooks. For |
202 It is sometimes convenient to explicitly enable or disable some hooks. For |
203 instance if you want to disable some integrity checking hook. This can be |
203 instance if you want to disable some integrity checking hook. This can be |
204 controlled more finely through the `category` class attribute, which is a string |
204 controlled more finely through the `category` class attribute, which is a string |
205 giving a category name. One can then uses the |
205 giving a category name. One can then uses the |
206 :meth:`~cubicweb.server.session.Session.deny_all_hooks_but` and |
206 :meth:`~cubicweb.server.session.Connection.deny_all_hooks_but` and |
207 :meth:`~cubicweb.server.session.Session.allow_all_hooks_but` context managers to |
207 :meth:`~cubicweb.server.session.Connection.allow_all_hooks_but` context managers to |
208 explicitly enable or disable some categories. |
208 explicitly enable or disable some categories. |
209 |
209 |
210 The existing categories are: |
210 The existing categories are: |
211 |
211 |
212 * ``security``, security checking hooks |
212 * ``security``, security checking hooks |
293 |
293 |
294 def register(self, obj, **kwargs): |
294 def register(self, obj, **kwargs): |
295 obj.check_events() |
295 obj.check_events() |
296 super(HooksRegistry, self).register(obj, **kwargs) |
296 super(HooksRegistry, self).register(obj, **kwargs) |
297 |
297 |
298 def call_hooks(self, event, session=None, **kwargs): |
298 def call_hooks(self, event, cnx=None, **kwargs): |
299 """call `event` hooks for an entity or a list of entities (passed |
299 """call `event` hooks for an entity or a list of entities (passed |
300 respectively as the `entity` or ``entities`` keyword argument). |
300 respectively as the `entity` or ``entities`` keyword argument). |
301 """ |
301 """ |
302 kwargs['event'] = event |
302 kwargs['event'] = event |
303 if session is None: # True for events such as server_start |
303 if cnx is None: # True for events such as server_start |
304 for hook in sorted(self.possible_objects(session, **kwargs), |
304 for hook in sorted(self.possible_objects(cnx, **kwargs), |
305 key=lambda x: x.order): |
305 key=lambda x: x.order): |
306 hook() |
306 hook() |
307 else: |
307 else: |
308 if 'entities' in kwargs: |
308 if 'entities' in kwargs: |
309 assert 'entity' not in kwargs, \ |
309 assert 'entity' not in kwargs, \ |
316 entities = [] |
316 entities = [] |
317 eids_from_to = kwargs.pop('eids_from_to') |
317 eids_from_to = kwargs.pop('eids_from_to') |
318 else: |
318 else: |
319 entities = [] |
319 entities = [] |
320 eids_from_to = [] |
320 eids_from_to = [] |
321 pruned = self.get_pruned_hooks(session, event, |
321 pruned = self.get_pruned_hooks(cnx, event, |
322 entities, eids_from_to, kwargs) |
322 entities, eids_from_to, kwargs) |
323 # by default, hooks are executed with security turned off |
323 # by default, hooks are executed with security turned off |
324 with session.security_enabled(read=False): |
324 with cnx.security_enabled(read=False): |
325 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs): |
325 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs): |
326 hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs), |
326 hooks = sorted(self.filtered_possible_objects(pruned, cnx, **_kwargs), |
327 key=lambda x: x.order) |
327 key=lambda x: x.order) |
328 debug = server.DEBUG & server.DBG_HOOKS |
328 debug = server.DEBUG & server.DBG_HOOKS |
329 with session.security_enabled(write=False): |
329 with cnx.security_enabled(write=False): |
330 for hook in hooks: |
330 for hook in hooks: |
331 if debug: |
331 if debug: |
332 print event, _kwargs, hook |
332 print event, _kwargs, hook |
333 hook() |
333 hook() |
334 |
334 |
335 def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs): |
335 def get_pruned_hooks(self, cnx, event, entities, eids_from_to, kwargs): |
336 """return a set of hooks that should not be considered by filtered_possible objects |
336 """return a set of hooks that should not be considered by filtered_possible objects |
337 |
337 |
338 the idea is to make a first pass over all the hooks in the |
338 the idea is to make a first pass over all the hooks in the |
339 registry and to mark put some of them in a pruned list. The |
339 registry and to mark put some of them in a pruned list. The |
340 pruned hooks are the one which: |
340 pruned hooks are the one which: |
341 |
341 |
342 * are disabled at the session level |
342 * are disabled at the connection level |
343 |
343 |
344 * have a selector containing a :class:`match_rtype` or an |
344 * have a selector containing a :class:`match_rtype` or an |
345 :class:`is_instance` predicate which does not match the rtype / etype |
345 :class:`is_instance` predicate which does not match the rtype / etype |
346 of the relations / entities for which we are calling the hooks. This |
346 of the relations / entities for which we are calling the hooks. This |
347 works because the repository calls the hooks grouped by rtype or by |
347 works because the repository calls the hooks grouped by rtype or by |
360 look_for_selector = match_rtype |
360 look_for_selector = match_rtype |
361 etype = None |
361 etype = None |
362 else: # nothing to prune, how did we get there ??? |
362 else: # nothing to prune, how did we get there ??? |
363 return set() |
363 return set() |
364 cache_key = (event, kwargs.get('rtype'), etype) |
364 cache_key = (event, kwargs.get('rtype'), etype) |
365 pruned = session.pruned_hooks_cache.get(cache_key) |
365 pruned = cnx.pruned_hooks_cache.get(cache_key) |
366 if pruned is not None: |
366 if pruned is not None: |
367 return pruned |
367 return pruned |
368 pruned = set() |
368 pruned = set() |
369 session.pruned_hooks_cache[cache_key] = pruned |
369 cnx.pruned_hooks_cache[cache_key] = pruned |
370 if look_for_selector is not None: |
370 if look_for_selector is not None: |
371 for id, hooks in self.iteritems(): |
371 for id, hooks in self.iteritems(): |
372 for hook in hooks: |
372 for hook in hooks: |
373 enabled_cat, main_filter = hook.filterable_selectors() |
373 enabled_cat, main_filter = hook.filterable_selectors() |
374 if enabled_cat is not None: |
374 if enabled_cat is not None: |
375 if not enabled_cat(hook, session): |
375 if not enabled_cat(hook, cnx): |
376 pruned.add(hook) |
376 pruned.add(hook) |
377 continue |
377 continue |
378 if main_filter is not None: |
378 if main_filter is not None: |
379 if isinstance(main_filter, match_rtype) and \ |
379 if isinstance(main_filter, match_rtype) and \ |
380 (main_filter.frometypes is not None or \ |
380 (main_filter.frometypes is not None or \ |
381 main_filter.toetypes is not None): |
381 main_filter.toetypes is not None): |
382 continue |
382 continue |
383 first_kwargs = _iter_kwargs(entities, eids_from_to, kwargs).next() |
383 first_kwargs = _iter_kwargs(entities, eids_from_to, kwargs).next() |
384 if not main_filter(hook, session, **first_kwargs): |
384 if not main_filter(hook, cnx, **first_kwargs): |
385 pruned.add(hook) |
385 pruned.add(hook) |
386 return pruned |
386 return pruned |
387 |
387 |
388 |
388 |
389 def filtered_possible_objects(self, pruned, *args, **kwargs): |
389 def filtered_possible_objects(self, pruned, *args, **kwargs): |
402 |
402 |
403 class HooksManager(object): |
403 class HooksManager(object): |
404 def __init__(self, vreg): |
404 def __init__(self, vreg): |
405 self.vreg = vreg |
405 self.vreg = vreg |
406 |
406 |
407 def call_hooks(self, event, session=None, **kwargs): |
407 def call_hooks(self, event, cnx=None, **kwargs): |
408 try: |
408 try: |
409 registry = self.vreg['%s_hooks' % event] |
409 registry = self.vreg['%s_hooks' % event] |
410 except RegistryNotFound: |
410 except RegistryNotFound: |
411 return # no hooks for this event |
411 return # no hooks for this event |
412 registry.call_hooks(event, session, **kwargs) |
412 registry.call_hooks(event, cnx, **kwargs) |
413 |
413 |
414 |
414 |
415 for event in ALL_HOOKS: |
415 for event in ALL_HOOKS: |
416 CWRegistryStore.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry |
416 CWRegistryStore.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry |
417 |
417 |
505 class Hook(AppObject): |
505 class Hook(AppObject): |
506 """Base class for hook. |
506 """Base class for hook. |
507 |
507 |
508 Hooks being appobjects like views, they have a `__regid__` and a `__select__` |
508 Hooks being appobjects like views, they have a `__regid__` and a `__select__` |
509 class attribute. Like all appobjects, hooks have the `self._cw` attribute which |
509 class attribute. Like all appobjects, hooks have the `self._cw` attribute which |
510 represents the current session. In entity hooks, a `self.entity` attribute is |
510 represents the current connection. In entity hooks, a `self.entity` attribute is |
511 also present. |
511 also present. |
512 |
512 |
513 The `events` tuple is used by the base class selector to dispatch the hook |
513 The `events` tuple is used by the base class selector to dispatch the hook |
514 on the right events. It is possible to dispatch on multiple events at once |
514 on the right events. It is possible to dispatch on multiple events at once |
515 if needed (though take care as hook attribute may vary as described above). |
515 if needed (though take care as hook attribute may vary as described above). |
683 |
683 |
684 class Operation(object): |
684 class Operation(object): |
685 """Base class for operations. |
685 """Base class for operations. |
686 |
686 |
687 Operation may be instantiated in the hooks' `__call__` method. It always |
687 Operation may be instantiated in the hooks' `__call__` method. It always |
688 takes a session object as first argument (accessible as `.session` from the |
688 takes a connection object as first argument (accessible as `.cnx` from the |
689 operation instance), and optionally all keyword arguments needed by the |
689 operation instance), and optionally all keyword arguments needed by the |
690 operation. These keyword arguments will be accessible as attributes from the |
690 operation. These keyword arguments will be accessible as attributes from the |
691 operation instance. |
691 operation instance. |
692 |
692 |
693 An operation is triggered on connections set events related to commit / |
693 An operation is triggered on connections set events related to commit / |
718 |
718 |
719 * `postcommit`: |
719 * `postcommit`: |
720 |
720 |
721 the transaction is over. All the ORM entities accessed by the earlier |
721 the transaction is over. All the ORM entities accessed by the earlier |
722 transaction are invalid. If you need to work on the database, you need to |
722 transaction are invalid. If you need to work on the database, you need to |
723 start a new transaction, for instance using a new internal session, which |
723 start a new transaction, for instance using a new internal connection, |
724 you will need to commit (and close!). |
724 which you will need to commit. |
725 |
725 |
726 For an operation to support an event, one has to implement the `<event |
726 For an operation to support an event, one has to implement the `<event |
727 name>_event` method with no arguments. |
727 name>_event` method with no arguments. |
728 |
728 |
729 The order of operations may be important, and is controlled according to |
729 The order of operations may be important, and is controlled according to |
730 the insert_index's method output (whose implementation vary according to the |
730 the insert_index's method output (whose implementation vary according to the |
731 base hook class used). |
731 base hook class used). |
732 """ |
732 """ |
733 |
733 |
734 def __init__(self, session, **kwargs): |
734 def __init__(self, cnx, **kwargs): |
735 self.session = session |
735 self.cnx = cnx |
736 self.__dict__.update(kwargs) |
736 self.__dict__.update(kwargs) |
737 self.register(session) |
737 self.register(cnx) |
738 # execution information |
738 # execution information |
739 self.processed = None # 'precommit', 'commit' |
739 self.processed = None # 'precommit', 'commit' |
740 self.failed = False |
740 self.failed = False |
741 |
741 |
742 def register(self, session): |
742 @property |
743 session.add_operation(self, self.insert_index()) |
743 @deprecated('[3.19] Operation.session is deprecated, use Operation.cnx instead') |
|
744 def session(self): |
|
745 return self.cnx |
|
746 |
|
747 def register(self, cnx): |
|
748 cnx.add_operation(self, self.insert_index()) |
744 |
749 |
745 def insert_index(self): |
750 def insert_index(self): |
746 """return the index of the lastest instance which is not a |
751 """return the index of the latest instance which is not a |
747 LateOperation instance |
752 LateOperation instance |
748 """ |
753 """ |
749 # faster by inspecting operation in reverse order for heavy transactions |
754 # faster by inspecting operation in reverse order for heavy transactions |
750 i = None |
755 i = None |
751 for i, op in enumerate(reversed(self.session.pending_operations)): |
756 for i, op in enumerate(reversed(self.cnx.pending_operations)): |
752 if isinstance(op, (LateOperation, SingleLastOperation)): |
757 if isinstance(op, (LateOperation, SingleLastOperation)): |
753 continue |
758 continue |
754 return -i or None |
759 return -i or None |
755 if i is None: |
760 if i is None: |
756 return None |
761 return None |
847 @classproperty |
852 @classproperty |
848 def data_key(cls): |
853 def data_key(cls): |
849 return ('cw.dataops', cls.__name__) |
854 return ('cw.dataops', cls.__name__) |
850 |
855 |
851 @classmethod |
856 @classmethod |
852 def get_instance(cls, session, **kwargs): |
857 def get_instance(cls, cnx, **kwargs): |
853 # no need to lock: transaction_data already comes from thread's local storage |
858 # no need to lock: transaction_data already comes from thread's local storage |
854 try: |
859 try: |
855 return session.transaction_data[cls.data_key] |
860 return cnx.transaction_data[cls.data_key] |
856 except KeyError: |
861 except KeyError: |
857 op = session.transaction_data[cls.data_key] = cls(session, **kwargs) |
862 op = cnx.transaction_data[cls.data_key] = cls(cnx, **kwargs) |
858 return op |
863 return op |
859 |
864 |
860 def __init__(self, *args, **kwargs): |
865 def __init__(self, *args, **kwargs): |
861 super(DataOperationMixIn, self).__init__(*args, **kwargs) |
866 super(DataOperationMixIn, self).__init__(*args, **kwargs) |
862 self._container = self._build_container() |
867 self._container = self._build_container() |
890 def get_data(self): |
895 def get_data(self): |
891 assert not self._processed, """Trying to get data from a closed operation. |
896 assert not self._processed, """Trying to get data from a closed operation. |
892 Iterating over operation data closed it and should be reserved to precommit / |
897 Iterating over operation data closed it and should be reserved to precommit / |
893 postcommit method of the operation.""" |
898 postcommit method of the operation.""" |
894 self._processed = True |
899 self._processed = True |
895 op = self.session.transaction_data.pop(self.data_key) |
900 op = self.cnx.transaction_data.pop(self.data_key) |
896 assert op is self, "Bad handling of operation data, found %s instead of %s for key %s" % ( |
901 assert op is self, "Bad handling of operation data, found %s instead of %s for key %s" % ( |
897 op, self, self.data_key) |
902 op, self, self.data_key) |
898 return self._container |
903 return self._container |
899 |
904 |
900 |
905 |
901 @deprecated('[3.10] use opcls.get_instance(session, **opkwargs).add_data(value)') |
906 @deprecated('[3.10] use opcls.get_instance(cnx, **opkwargs).add_data(value)') |
902 def set_operation(session, datakey, value, opcls, containercls=set, **opkwargs): |
907 def set_operation(cnx, datakey, value, opcls, containercls=set, **opkwargs): |
903 """Function to ease applying a single operation on a set of data, avoiding |
908 """Function to ease applying a single operation on a set of data, avoiding |
904 to create as many as operation as they are individual modification. You |
909 to create as many as operation as they are individual modification. You |
905 should try to use this instead of creating on operation for each `value`, |
910 should try to use this instead of creating on operation for each `value`, |
906 since handling operations becomes coslty on massive data import. |
911 since handling operations becomes coslty on massive data import. |
907 |
912 |
908 Arguments are: |
913 Arguments are: |
909 |
914 |
910 * the `session` object |
915 * `cnx`, the current connection |
911 |
916 |
912 * `datakey`, a specially forged key that will be used as key in |
917 * `datakey`, a specially forged key that will be used as key in |
913 session.transaction_data |
918 cnx.transaction_data |
914 |
919 |
915 * `value` that is the actual payload of an individual operation |
920 * `value` that is the actual payload of an individual operation |
916 |
921 |
917 * `opcls`, the class of the operation. An instance is created on the first |
922 * `opcls`, the class of the operation. An instance is created on the first |
918 call for the given key, and then subsequent calls will simply add the |
923 call for the given key, and then subsequent calls will simply add the |
938 .. Note:: |
943 .. Note:: |
939 **poping** the key from `transaction_data` is not an option, else you may |
944 **poping** the key from `transaction_data` is not an option, else you may |
940 get unexpected data loss in some case of nested hooks. |
945 get unexpected data loss in some case of nested hooks. |
941 """ |
946 """ |
942 try: |
947 try: |
943 # Search for session.transaction_data[`datakey`] (expected to be a set): |
948 # Search for cnx.transaction_data[`datakey`] (expected to be a set): |
944 # if found, simply append `value` |
949 # if found, simply append `value` |
945 _container_add(session.transaction_data[datakey], value) |
950 _container_add(cnx.transaction_data[datakey], value) |
946 except KeyError: |
951 except KeyError: |
947 # else, initialize it to containercls([`value`]) and instantiate the given |
952 # else, initialize it to containercls([`value`]) and instantiate the given |
948 # `opcls` operation class with additional keyword arguments |
953 # `opcls` operation class with additional keyword arguments |
949 opcls(session, **opkwargs) |
954 opcls(cnx, **opkwargs) |
950 session.transaction_data[datakey] = containercls() |
955 cnx.transaction_data[datakey] = containercls() |
951 _container_add(session.transaction_data[datakey], value) |
956 _container_add(cnx.transaction_data[datakey], value) |
952 |
957 |
953 |
958 |
954 class LateOperation(Operation): |
959 class LateOperation(Operation): |
955 """special operation which should be called after all possible (ie non late) |
960 """special operation which should be called after all possible (ie non late) |
956 operations |
961 operations |
959 """return the index of the lastest instance which is not a |
964 """return the index of the lastest instance which is not a |
960 SingleLastOperation instance |
965 SingleLastOperation instance |
961 """ |
966 """ |
962 # faster by inspecting operation in reverse order for heavy transactions |
967 # faster by inspecting operation in reverse order for heavy transactions |
963 i = None |
968 i = None |
964 for i, op in enumerate(reversed(self.session.pending_operations)): |
969 for i, op in enumerate(reversed(self.cnx.pending_operations)): |
965 if isinstance(op, SingleLastOperation): |
970 if isinstance(op, SingleLastOperation): |
966 continue |
971 continue |
967 return -i or None |
972 return -i or None |
968 if i is None: |
973 if i is None: |
969 return None |
974 return None |
974 class SingleLastOperation(Operation): |
979 class SingleLastOperation(Operation): |
975 """special operation which should be called once and after all other |
980 """special operation which should be called once and after all other |
976 operations |
981 operations |
977 """ |
982 """ |
978 |
983 |
979 def register(self, session): |
984 def register(self, cnx): |
980 """override register to handle cases where this operation has already |
985 """override register to handle cases where this operation has already |
981 been added |
986 been added |
982 """ |
987 """ |
983 operations = session.pending_operations |
988 operations = cnx.pending_operations |
984 index = self.equivalent_index(operations) |
989 index = self.equivalent_index(operations) |
985 if index is not None: |
990 if index is not None: |
986 equivalent = operations.pop(index) |
991 equivalent = operations.pop(index) |
987 else: |
992 else: |
988 equivalent = None |
993 equivalent = None |
989 session.add_operation(self, self.insert_index()) |
994 cnx.add_operation(self, self.insert_index()) |
990 return equivalent |
995 return equivalent |
991 |
996 |
992 def equivalent_index(self, operations): |
997 def equivalent_index(self, operations): |
993 """return the index of the equivalent operation if any""" |
998 """return the index of the equivalent operation if any""" |
994 for i, op in enumerate(reversed(operations)): |
999 for i, op in enumerate(reversed(operations)): |
999 def insert_index(self): |
1004 def insert_index(self): |
1000 return None |
1005 return None |
1001 |
1006 |
1002 |
1007 |
1003 class SendMailOp(SingleLastOperation): |
1008 class SendMailOp(SingleLastOperation): |
1004 def __init__(self, session, msg=None, recipients=None, **kwargs): |
1009 def __init__(self, cnx, msg=None, recipients=None, **kwargs): |
1005 # may not specify msg yet, as |
1010 # may not specify msg yet, as |
1006 # `cubicweb.sobjects.supervision.SupervisionMailOp` |
1011 # `cubicweb.sobjects.supervision.SupervisionMailOp` |
1007 if msg is not None: |
1012 if msg is not None: |
1008 assert recipients |
1013 assert recipients |
1009 self.to_send = [(msg, recipients)] |
1014 self.to_send = [(msg, recipients)] |
1010 else: |
1015 else: |
1011 assert recipients is None |
1016 assert recipients is None |
1012 self.to_send = [] |
1017 self.to_send = [] |
1013 super(SendMailOp, self).__init__(session, **kwargs) |
1018 super(SendMailOp, self).__init__(cnx, **kwargs) |
1014 |
1019 |
1015 def register(self, session): |
1020 def register(self, cnx): |
1016 previous = super(SendMailOp, self).register(session) |
1021 previous = super(SendMailOp, self).register(cnx) |
1017 if previous: |
1022 if previous: |
1018 self.to_send = previous.to_send + self.to_send |
1023 self.to_send = previous.to_send + self.to_send |
1019 |
1024 |
1020 def postcommit_event(self): |
1025 def postcommit_event(self): |
1021 self.session.repo.threaded_task(self.sendmails) |
1026 self.cnx.repo.threaded_task(self.sendmails) |
1022 |
1027 |
1023 def sendmails(self): |
1028 def sendmails(self): |
1024 self.session.vreg.config.sendmails(self.to_send) |
1029 self.cnx.vreg.config.sendmails(self.to_send) |
1025 |
1030 |
1026 |
1031 |
1027 class RQLPrecommitOperation(Operation): |
1032 class RQLPrecommitOperation(Operation): |
1028 # to be defined in concrete classes |
1033 # to be defined in concrete classes |
1029 rqls = None |
1034 rqls = None |
1030 |
1035 |
1031 def precommit_event(self): |
1036 def precommit_event(self): |
1032 execute = self.session.execute |
1037 execute = self.cnx.execute |
1033 for rql in self.rqls: |
1038 for rql in self.rqls: |
1034 execute(*rql) |
1039 execute(*rql) |
1035 |
1040 |
1036 |
1041 |
1037 class CleanupNewEidsCacheOp(DataOperationMixIn, SingleLastOperation): |
1042 class CleanupNewEidsCacheOp(DataOperationMixIn, SingleLastOperation): |
1049 def rollback_event(self): |
1054 def rollback_event(self): |
1050 """the observed connections set has been rolled back, |
1055 """the observed connections set has been rolled back, |
1051 remove inserted eid from repository type/source cache |
1056 remove inserted eid from repository type/source cache |
1052 """ |
1057 """ |
1053 try: |
1058 try: |
1054 self.session.repo.clear_caches(self.get_data()) |
1059 self.cnx.repo.clear_caches(self.get_data()) |
1055 except KeyError: |
1060 except KeyError: |
1056 pass |
1061 pass |
1057 |
1062 |
1058 class CleanupDeletedEidsCacheOp(DataOperationMixIn, SingleLastOperation): |
1063 class CleanupDeletedEidsCacheOp(DataOperationMixIn, SingleLastOperation): |
1059 """on commit of delete query, we have to remove from repository's |
1064 """on commit of delete query, we have to remove from repository's |