server/hook.py
changeset 6147 95c604ec89bf
parent 6142 8bc6eac1fac1
child 6279 42079f752a9c
equal deleted inserted replaced
6146:f3d82f25ab61 6147:95c604ec89bf
    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"
   190 
   381 
   191 
   382 
   192 # base class for hook ##########################################################
   383 # base class for hook ##########################################################
   193 
   384 
   194 class Hook(AppObject):
   385 class Hook(AppObject):
       
   386     """Base class for hook.
       
   387 
       
   388     Hooks being appobjects like views, they have a `__regid__` and a `__select__`
       
   389     class attribute. Like all appobjects, hooks have the `self._cw` attribute which
       
   390     represents the current session. In entity hooks, a `self.entity` attribute is
       
   391     also present.
       
   392 
       
   393     The `events` tuple is used by the base class selector to dispatch the hook
       
   394     on the right events. It is possible to dispatch on multiple events at once
       
   395     if needed (though take care as hook attribute may vary as described above).
       
   396 
       
   397     .. Note::
       
   398 
       
   399       Do not forget to extend the base class selectors as in ::
       
   400 
       
   401       .. sourcecode:: python
       
   402 
       
   403           class MyHook(Hook):
       
   404             __regid__ = 'whatever'
       
   405             __select__ = Hook.__select__ & implements('Person')
       
   406 
       
   407       else your hooks will be called madly, whatever the event.
       
   408     """
   195     __select__ = enabled_category()
   409     __select__ = enabled_category()
   196     # set this in derivated classes
   410     # set this in derivated classes
   197     events = None
   411     events = None
   198     category = None
   412     category = None
   199     order = 0
   413     order = 0
   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