13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
14 # details. |
14 # details. |
15 # |
15 # |
16 # You should have received a copy of the GNU Lesser General Public License along |
16 # You should have received a copy of the GNU Lesser General Public License along |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """Hooks management |
18 """ |
19 |
19 Generalities |
20 This module defined the `Hook` class and registry and a set of abstract classes |
20 ------------ |
21 for operations. |
21 |
22 |
22 Paraphrasing the `emacs`_ documentation, let us say that hooks are an important |
23 |
23 mechanism for customizing an application. A hook is basically a list of |
24 Hooks are called before / after any individual update of entities / relations |
24 functions to be called on some well-defined occasion (this is called `running |
25 in the repository and on special events such as server startup or shutdown. |
25 the hook`). |
26 |
26 |
27 |
27 .. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html |
28 Operations may be registered by hooks during a transaction, which will be |
28 |
29 fired when the pool is commited or rollbacked. |
29 Hooks |
30 |
30 ~~~~~ |
31 |
31 |
32 Entity hooks (eg before_add_entity, after_add_entity, before_update_entity, |
32 In |cubicweb|, hooks are subclasses of the :class:`~cubicweb.server.hook.Hook` |
33 after_update_entity, before_delete_entity, after_delete_entity) all have an |
33 class. They are selected over a set of pre-defined `events` (and possibly more |
34 `entity` attribute |
34 conditions, hooks being selectable appobjects like views and components). They |
35 |
35 should implement a :meth:`~cubicweb.server.hook.Hook.__call__` method that will |
36 Relation (eg before_add_relation, after_add_relation, before_delete_relation, |
36 be called when the hook is triggered. |
37 after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes. |
37 |
38 |
38 There are two families of events: data events (before / after any individual |
39 Server start/maintenance/stop hooks (eg server_startup, server_maintenance, |
39 update of an entity / or a relation in the repository) and server events (such |
40 server_shutdown) have a `repo` attribute, but *their `_cw` attribute is None*. |
40 as server startup or shutdown). In a typical application, most of the hooks are |
41 The `server_startup` is called on regular startup, while `server_maintenance` |
41 defined over data events. |
42 is called on cubicweb-ctl upgrade or shell commands. `server_shutdown` is |
42 |
43 called anyway. |
43 Also, some :class:`~cubicweb.server.hook.Operation` may be registered by hooks, |
44 |
44 which will be fired when the transaction is commited or rollbacked. |
45 Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a |
45 |
46 `timestamp` attributes, but *their `_cw` attribute is None*. |
46 The purpose of data event hooks is usually to complement the data model as |
47 |
47 defined in the schema, which is static by nature and only provide a restricted |
48 Session hooks (eg session_open, session_close) have no special attribute. |
48 builtin set of dynamic constraints, with dynamic or value driven behaviours. |
|
49 For instance they can serve the following purposes: |
|
50 |
|
51 * enforcing constraints that the static schema cannot express (spanning several |
|
52 entities/relations, exotic value ranges and cardinalities, etc.) |
|
53 |
|
54 * implement computed attributes |
|
55 |
|
56 It is functionally equivalent to a `database trigger`_, except that database |
|
57 triggers definition languages are not standardized, hence not portable (for |
|
58 instance, PL/SQL works with Oracle and PostgreSQL but not SqlServer nor Sqlite). |
|
59 |
|
60 .. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger |
|
61 |
|
62 |
|
63 Operations |
|
64 ~~~~~~~~~~ |
|
65 |
|
66 Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` class |
|
67 that may be created by hooks and scheduled to happen just before (or after) the |
|
68 `precommit`, `postcommit` or `rollback` event. Hooks are being fired immediately |
|
69 on data operations, and it is sometime necessary to delay the actual work down |
|
70 to a time where all other hooks have run. Also while the order of execution of |
|
71 hooks is data dependant (and thus hard to predict), it is possible to force an |
|
72 order on operations. |
|
73 |
|
74 Operations may be used to: |
|
75 |
|
76 * implements a validation check which needs that all relations be already set on |
|
77 an entity |
|
78 |
|
79 * process various side effects associated with a transaction such as filesystem |
|
80 udpates, mail notifications, etc. |
|
81 |
|
82 |
|
83 Events |
|
84 ------ |
|
85 |
|
86 Hooks are mostly defined and used to handle `dataflow`_ operations. It |
|
87 means as data gets in (entities added, updated, relations set or |
|
88 unset), specific events are issued and the Hooks matching these events |
|
89 are called. |
|
90 |
|
91 You can get the event that triggered a hook by accessing its :attr:event |
|
92 attribute. |
|
93 |
|
94 .. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow |
|
95 |
|
96 |
|
97 Entity modification related events |
|
98 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
99 |
|
100 When called for one of these events, hook will have an `entity` attribute |
|
101 containing the entity instance. |
|
102 |
|
103 * 'before_add_entity', 'before_update_entity': |
|
104 |
|
105 - on those events, you can check what attributes of the entity are modified in |
|
106 `entity.cw_edited` (by definition the database is not yet updated in a before |
|
107 event) |
|
108 |
|
109 - you are allowed to further modify the entity before database operations, |
|
110 using the dictionary notation. By doing this, you'll avoid the need for a |
|
111 whole new rql query processing, the only difference is that the underlying |
|
112 backend query (eg usually sql) will contains the additional data. For |
|
113 example: |
|
114 |
|
115 .. sourcecode:: python |
|
116 |
|
117 self.entity.set_attributes(age=42) |
|
118 |
|
119 will set the `age` attribute of the entity to 42. But to do so, it will |
|
120 generate a rql query that will have to be processed, then trigger some |
|
121 hooks, and so one (potentially leading to infinite hook loops or such |
|
122 awkward situations..) You can avoid this by doing the modification that way: |
|
123 |
|
124 .. sourcecode:: python |
|
125 |
|
126 self.entity.cw_edited['age'] = 42 |
|
127 |
|
128 Here the attribute will simply be edited in the same query that the |
|
129 one that triggered the hook. |
|
130 |
|
131 Similarly, removing an attribute from `cw_edited` will cancel its |
|
132 modification. |
|
133 |
|
134 - on 'before_update_entity' event, you can access to old and new values in |
|
135 this hook, by using `entity.cw_edited.oldnewvalue(attr)` |
|
136 |
|
137 |
|
138 * 'after_add_entity', 'after_update_entity' |
|
139 |
|
140 - on those events, you can still check what attributes of the entity are |
|
141 modified in `entity.cw_edited` but you can't get anymore the old value, nor |
|
142 modify it. |
|
143 |
|
144 * 'before_delete_entity', 'after_delete_entity' |
|
145 |
|
146 - on those events, the entity has no `cw_edited` set. |
|
147 |
|
148 |
|
149 Relation modification related events |
|
150 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
151 |
|
152 When called for one of these events, hook will have `eidfrom`, `rtype`, `eidto` |
|
153 attributes containing respectivly the eid of the subject entity, the relation |
|
154 type and the eid of the object entity. |
|
155 |
|
156 * 'before_add_relation', 'before_delete_relation' |
|
157 |
|
158 - on those events, you can still get original relation by issuing a rql query |
|
159 |
|
160 * 'after_add_relation', 'after_delete_relation' |
|
161 |
|
162 This is an occasion to remind us that relations support the add / delete |
|
163 operation, but no update. |
|
164 |
|
165 |
|
166 Non data events |
|
167 ~~~~~~~~~~~~~~~ |
|
168 |
|
169 Hooks called on server start/maintenance/stop event (eg 'server_startup', |
|
170 'server_maintenance', 'server_shutdown') have a `repo` attribute, but *their |
|
171 `_cw` attribute is None*. The `server_startup` is called on regular startup, |
|
172 while `server_maintenance` is called on cubicweb-ctl upgrade or shell |
|
173 commands. `server_shutdown` is called anyway. |
|
174 |
|
175 Hooks called on backup/restore event (eg 'server_backup', 'server_restore') have |
|
176 a `repo` and a `timestamp` attributes, but *their `_cw` attribute is None*. |
|
177 |
|
178 Hooks called on session event (eg 'session_open', 'session_close') have no |
|
179 special attribute. |
|
180 |
|
181 |
|
182 API |
|
183 --- |
|
184 |
|
185 Hooks control |
|
186 ~~~~~~~~~~~~~ |
|
187 |
|
188 It is sometimes convenient to explicitly enable or disable some hooks. For |
|
189 instance if you want to disable some integrity checking hook. This can be |
|
190 controlled more finely through the `category` class attribute, which is a string |
|
191 giving a category name. One can then uses the |
|
192 :class:`~cubicweb.server.session.hooks_control` context manager to explicitly |
|
193 enable or disable some categories. |
|
194 |
|
195 .. autoclass:: cubicweb.server.session.hooks_control |
|
196 |
|
197 |
|
198 The existing categories are: |
|
199 |
|
200 * ``security``, security checking hooks |
|
201 |
|
202 * ``worfklow``, workflow handling hooks |
|
203 |
|
204 * ``metadata``, hooks setting meta-data on newly created entities |
|
205 |
|
206 * ``notification``, email notification hooks |
|
207 |
|
208 * ``integrity``, data integrity checking hooks |
|
209 |
|
210 * ``activeintegrity``, data integrity consistency hooks, that you should *never* |
|
211 want to disable |
|
212 |
|
213 * ``syncsession``, hooks synchronizing existing sessions |
|
214 |
|
215 * ``syncschema``, hooks synchronizing instance schema (including the physical database) |
|
216 |
|
217 * ``email``, email address handling hooks |
|
218 |
|
219 * ``bookmark``, bookmark entities handling hooks |
|
220 |
|
221 |
|
222 Nothing precludes one to invent new categories and use the |
|
223 :class:`~cubicweb.server.session.hooks_control` context manager to filter them |
|
224 in or out. |
|
225 |
|
226 |
|
227 Hooks specific selector |
|
228 ~~~~~~~~~~~~~~~~~~~~~~~ |
|
229 .. autoclass:: cubicweb.server.hook.match_rtype |
|
230 .. autoclass:: cubicweb.server.hook.match_rtype_sets |
|
231 |
|
232 |
|
233 Hooks and operations classes |
|
234 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
235 .. autoclass:: cubicweb.server.hook.Hook |
|
236 .. autoclass:: cubicweb.server.hook.Operation |
|
237 .. autoclass:: cubicweb.server.hook.LateOperation |
|
238 .. autofunction:: cubicweb.server.hook.set_operation |
|
239 |
49 """ |
240 """ |
50 |
241 |
51 from __future__ import with_statement |
242 from __future__ import with_statement |
52 |
243 |
53 __docformat__ = "restructuredtext en" |
244 __docformat__ = "restructuredtext en" |
351 |
565 |
352 |
566 |
353 # abstract classes for operation ############################################### |
567 # abstract classes for operation ############################################### |
354 |
568 |
355 class Operation(object): |
569 class Operation(object): |
356 """an operation is triggered on connections pool events related to |
570 """Base class for operations. |
|
571 |
|
572 Operation may be instantiated in the hooks' `__call__` method. It always |
|
573 takes a session object as first argument (accessible as `.session` from the |
|
574 operation instance), and optionally all keyword arguments needed by the |
|
575 operation. These keyword arguments will be accessible as attributes from the |
|
576 operation instance. |
|
577 |
|
578 An operation is triggered on connections pool events related to |
357 commit / rollback transations. Possible events are: |
579 commit / rollback transations. Possible events are: |
358 |
580 |
359 precommit: |
581 * 'precommit': |
360 the pool is preparing to commit. You shouldn't do anything which |
582 |
361 has to be reverted if the commit fails at this point, but you can freely |
583 the transaction is being prepared for commit. You can freely do any heavy |
362 do any heavy computation or raise an exception if the commit can't go. |
584 computation, raise an exception if the commit can't go. or even add some |
363 You can add some new operations during this phase but their precommit |
585 new operations during this phase. If you do anything which has to be |
364 event won't be triggered |
586 reverted if the commit fails afterwards (eg altering the file system for |
365 |
587 instance), you'll have to support the 'revertprecommit' event to revert |
366 commit: |
588 things by yourself |
367 the pool is preparing to commit. You should avoid to do to expensive |
589 |
368 stuff or something that may cause an exception in this event |
590 * 'revertprecommit': |
369 |
591 |
370 revertcommit: |
592 if an operation failed while being pre-commited, this event is triggered |
371 if an operation failed while commited, this event is triggered for |
593 for all operations which had their 'precommit' event already fired to let |
372 all operations which had their commit event already to let them |
594 them revert things (including the operation which made the commit fail) |
373 revert things (including the operation which made fail the commit) |
595 |
374 |
596 * 'rollback': |
375 rollback: |
597 |
376 the transaction has been either rollbacked either: |
598 the transaction has been either rollbacked either: |
|
599 |
377 * intentionaly |
600 * intentionaly |
378 * a precommit event failed, all operations are rollbacked |
601 * a 'precommit' event failed, in which case all operations are rollbacked |
379 * a commit event failed, all operations which are not been triggered for |
602 once 'revertprecommit'' has been called |
380 commit are rollbacked |
603 |
381 |
604 * 'postcommit': |
382 postcommit: |
605 |
383 The transaction is over. All the ORM entities are |
606 the transaction is over. All the ORM entities accessed by the earlier |
384 invalid. If you need to work on the database, you need to stard |
607 transaction are invalid. If you need to work on the database, you need to |
385 a new transaction, for instance using a new internal_session, |
608 start a new transaction, for instance using a new internal session, which |
386 which you will need to commit (and close!). |
609 you will need to commit (and close!). |
387 |
610 |
388 order of operations may be important, and is controlled according to |
611 For an operation to support an event, one has to implement the `<event |
389 the insert_index's method output |
612 name>_event` method with no arguments. |
|
613 |
|
614 Notice order of operations may be important, and is controlled according to |
|
615 the insert_index's method output (whose implementation vary according to the |
|
616 base hook class used). |
390 """ |
617 """ |
391 |
618 |
392 def __init__(self, session, **kwargs): |
619 def __init__(self, session, **kwargs): |
393 self.session = session |
620 self.session = session |
394 self.__dict__.update(kwargs) |
621 self.__dict__.update(kwargs) |
466 |
693 |
467 def _container_add(container, value): |
694 def _container_add(container, value): |
468 {set: set.add, list: list.append}[container.__class__](container, value) |
695 {set: set.add, list: list.append}[container.__class__](container, value) |
469 |
696 |
470 def set_operation(session, datakey, value, opcls, containercls=set, **opkwargs): |
697 def set_operation(session, datakey, value, opcls, containercls=set, **opkwargs): |
471 """Search for session.transaction_data[`datakey`] (expected to be a set): |
698 """Function to ease applying a single operation on a set of data, avoiding |
472 |
699 to create as many as operation as they are individual modification. You |
473 * if found, simply append `value` |
700 should try to use this instead of creating on operation for each `value`, |
474 |
|
475 * else, initialize it to containercls([`value`]) and instantiate the given |
|
476 `opcls` operation class with additional keyword arguments. `containercls` |
|
477 is a set by default. Give `list` if you want to keep arrival ordering. |
|
478 |
|
479 You should use this instead of creating on operation for each `value`, |
|
480 since handling operations becomes coslty on massive data import. |
701 since handling operations becomes coslty on massive data import. |
481 """ |
702 |
|
703 Arguments are: |
|
704 |
|
705 * the `session` object |
|
706 |
|
707 * `datakey`, a specially forged key that will be used as key in |
|
708 session.transaction_data |
|
709 |
|
710 * `value` that is the actual payload of an individual operation |
|
711 |
|
712 * `opcls`, the class of the operation. An instance is created on the first |
|
713 call for the given key, and then subsequent calls will simply add the |
|
714 payload to the container (hence `opkwargs` is only used on that first |
|
715 call) |
|
716 |
|
717 * `containercls`, the container class that should be instantiated to hold |
|
718 payloads. An instance is created on the first call for the given key, and |
|
719 then subsequent calls will add the data to the existing container. Default |
|
720 to a set. Give `list` if you want to keep arrival ordering. |
|
721 |
|
722 * more optional parameters to give to the operation (here the rtype which do not |
|
723 vary accross operations). |
|
724 |
|
725 The body of the operation must then iterate over the values that have been mapped |
|
726 in the transaction_data dictionary to the forged key, e.g.: |
|
727 |
|
728 .. sourcecode:: python |
|
729 |
|
730 for value in self._cw.transaction_data.pop(datakey): |
|
731 ... |
|
732 |
|
733 .. Note:: |
|
734 **poping** the key from `transaction_data` is not an option, else you may |
|
735 get unexpected data loss in some case of nested hooks. |
|
736 """ |
|
737 |
|
738 |
|
739 |
482 try: |
740 try: |
|
741 # Search for session.transaction_data[`datakey`] (expected to be a set): |
|
742 # if found, simply append `value` |
483 _container_add(session.transaction_data[datakey], value) |
743 _container_add(session.transaction_data[datakey], value) |
484 except KeyError: |
744 except KeyError: |
|
745 # else, initialize it to containercls([`value`]) and instantiate the given |
|
746 # `opcls` operation class with additional keyword arguments |
485 opcls(session, **opkwargs) |
747 opcls(session, **opkwargs) |
486 session.transaction_data[datakey] = containercls() |
748 session.transaction_data[datakey] = containercls() |
487 _container_add(session.transaction_data[datakey], value) |
749 _container_add(session.transaction_data[datakey], value) |
488 |
750 |
489 |
751 |