doc/book/devrepo/repo/hooks.rst
changeset 10491 c67bcee93248
parent 9184 b982e88e4836
child 12352 1a0159426def
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/hooks.rst	Thu Jan 08 22:11:06 2015 +0100
@@ -0,0 +1,279 @@
+.. -*- coding: utf-8 -*-
+.. _hooks:
+
+Hooks and Operations
+====================
+
+.. autodocstring:: cubicweb.server.hook
+
+
+Example using dataflow hooks
+----------------------------
+
+We will use a very simple example to show hooks usage. Let us start with the
+following schema.
+
+.. sourcecode:: python
+
+   class Person(EntityType):
+       age = Int(required=True)
+
+We would like to add a range constraint over a person's age. Let's write an hook
+(supposing yams can not handle this nativly, which is wrong). It shall be placed
+into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
+`mycube/hooks/... package` containing hooks in various modules.
+
+.. sourcecode:: python
+
+   from cubicweb import ValidationError
+   from cubicweb.predicates import is_instance
+   from cubicweb.server.hook import Hook
+
+   class PersonAgeRange(Hook):
+        __regid__ = 'person_age_range'
+        __select__ = Hook.__select__ & is_instance('Person')
+        events = ('before_add_entity', 'before_update_entity')
+
+        def __call__(self):
+	    if 'age' in self.entity.cw_edited:
+                if 0 <= self.entity.age <= 120:
+                   return
+		msg = self._cw._('age must be between 0 and 120')
+		raise ValidationError(self.entity.eid, {'age': msg})
+
+In our example the base `__select__` is augmented with an `is_instance` selector
+matching the desired entity type.
+
+The `events` tuple is used specify that our hook should be called before the
+entity is added or updated.
+
+Then in the hook's `__call__` method, we:
+
+* check if the 'age' attribute is edited
+* if so, check the value is in the range
+* if not, raise a validation error properly
+
+Now Let's augment our schema with new `Company` entity type with some relation to
+`Person` (in 'mycube/schema.py').
+
+.. sourcecode:: python
+
+   class Company(EntityType):
+        name = String(required=True)
+        boss = SubjectRelation('Person', cardinality='1*')
+        subsidiary_of = SubjectRelation('Company', cardinality='*?')
+
+
+We would like to constrain the company's bosses to have a minimum (legal)
+age. Let's write an hook for this, which will be fired when the `boss` relation
+is established (still supposing we could not specify that kind of thing in the
+schema).
+
+.. sourcecode:: python
+
+   class CompanyBossLegalAge(Hook):
+        __regid__ = 'company_boss_legal_age'
+        __select__ = Hook.__select__ & match_rtype('boss')
+        events = ('before_add_relation',)
+
+        def __call__(self):
+            boss = self._cw.entity_from_eid(self.eidto)
+            if boss.age < 18:
+                msg = self._cw._('the minimum age for a boss is 18')
+                raise ValidationError(self.eidfrom, {'boss': msg})
+
+.. Note::
+
+    We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the
+    proper relation type.
+
+    The essential difference with respect to an entity hook is that there is no
+    self.entity, but `self.eidfrom` and `self.eidto` hook attributes which
+    represent the subject and object **eid** of the relation.
+
+Suppose we want to check that there is no cycle by the `subsidiary_of`
+relation. This is best achieved in an operation since all relations are likely to
+be set at commit time.
+
+.. sourcecode:: python
+
+    from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype
+
+    def check_cycle(self, session, eid, rtype, role='subject'):
+        parents = set([eid])
+        parent = session.entity_from_eid(eid)
+        while parent.related(rtype, role):
+            parent = parent.related(rtype, role)[0]
+            if parent.eid in parents:
+                msg = session._('detected %s cycle' % rtype)
+                raise ValidationError(eid, {rtype: msg})
+            parents.add(parent.eid)
+
+
+    class CheckSubsidiaryCycleOp(Operation):
+
+        def precommit_event(self):
+            check_cycle(self.session, self.eidto, 'subsidiary_of')
+
+
+    class CheckSubsidiaryCycleHook(Hook):
+        __regid__ = 'check_no_subsidiary_cycle'
+        __select__ = Hook.__select__ & match_rtype('subsidiary_of')
+        events = ('after_add_relation',)
+
+        def __call__(self):
+            CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
+
+
+Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other
+exceptions are usually programming errors.
+
+In the above example, our hook will instantiate an operation each time the hook
+is called, i.e. each time the `subsidiary_of` relation is set. There is an
+alternative method to schedule an operation from a hook, using the
+:func:`get_instance` class method.
+
+.. sourcecode:: python
+
+   from cubicweb.server.hook import set_operation
+
+   class CheckSubsidiaryCycleHook(Hook):
+       __regid__ = 'check_no_subsidiary_cycle'
+       events = ('after_add_relation',)
+       __select__ = Hook.__select__ & match_rtype('subsidiary_of')
+
+       def __call__(self):
+           CheckSubsidiaryCycleOp.get_instance(self._cw).add_data(self.eidto)
+
+   class CheckSubsidiaryCycleOp(DataOperationMixIn, Operation):
+
+       def precommit_event(self):
+           for eid in self.get_data():
+               check_cycle(self.session, eid, self.rtype)
+
+
+Here, we call :func:`set_operation` so that we will simply accumulate eids of
+entities to check at the end in a single `CheckSubsidiaryCycleOp`
+operation. Value are stored in a set associated to the
+'subsidiary_cycle_detection' transaction data key. The set initialization and
+operation creation are handled nicely by :func:`set_operation`.
+
+A more realistic example can be found in the advanced tutorial chapter
+:ref:`adv_tuto_security_propagation`.
+
+
+Inter-instance communication
+----------------------------
+
+If your application consists of several instances, you may need some means to
+communicate between them.  Cubicweb provides a publish/subscribe mechanism
+using ØMQ_.  In order to use it, use
+:meth:`~cubicweb.server.cwzmq.ZMQComm.add_subscription` on the
+`repo.app_instances_bus` object.  The `callback` will get the message (as a
+list).  A message can be sent by calling
+:meth:`~cubicweb.server.cwzmq.ZMQComm.publish` on `repo.app_instances_bus`.
+The first element of the message is the topic which is used for filtering and
+dispatching messages.
+
+.. _ØMQ: http://www.zeromq.org/
+
+.. sourcecode:: python
+
+  class FooHook(hook.Hook):
+      events = ('server_startup',)
+      __regid__ = 'foo_startup'
+
+      def __call__(self):
+          def callback(msg):
+              self.info('received message: %s', ' '.join(msg))
+          self.repo.app_instances_bus.add_subscription('hello', callback)
+
+.. sourcecode:: python
+
+  def do_foo(self):
+      actually_do_foo()
+      self._cw.repo.app_instances_bus.publish(['hello', 'world'])
+
+The `zmq-address-pub` configuration variable contains the address used
+by the instance for sending messages, e.g. `tcp://*:1234`.  The
+`zmq-address-sub` variable contains a comma-separated list of addresses
+to listen on, e.g. `tcp://localhost:1234, tcp://192.168.1.1:2345`.
+
+
+Hooks writing tips
+------------------
+
+Reminder
+~~~~~~~~
+
+You should never use the `entity.foo = 42` notation to update an entity. It will
+not do what you expect (updating the database). Instead, use the
+:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
+:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
+'before_update_entity' event.
+
+
+How to choose between a before and an after event ?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`before_*` hooks give you access to the old attribute (or relation)
+values. You can also intercept and update edited values in the case of
+entity modification before they reach the database.
+
+Else the question is: should I need to do things before or after the actual
+modification ? If the answer is "it doesn't matter", use an 'after' event.
+
+
+Validation Errors
+~~~~~~~~~~~~~~~~~
+
+When a hook which is responsible to maintain the consistency of the
+data model detects an error, it must use a specific exception named
+:exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
+:exc:`~cubicweb.ValidationError` is a programming error. Raising it
+entails aborting the current transaction.
+
+This exception is used to convey enough information up to the user
+interface. Hence its constructor is different from the default Exception
+constructor. It accepts, positionally:
+
+* an entity eid (**not the entity itself**),
+
+* a dict whose keys represent attribute (or relation) names and values
+  an end-user facing message (hence properly translated) relating the
+  problem.
+
+.. sourcecode:: python
+
+  raise ValidationError(earth.eid, {'sea_level': self._cw._('too high'),
+                                    'temperature': self._cw._('too hot')})
+
+
+Checking for object created/deleted in the current transaction
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In hooks, you can use the
+:meth:`~cubicweb.server.session.Session.added_in_transaction` or
+:meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session
+object to check if an eid has been created or deleted during the hook's
+transaction.
+
+This is useful to enable or disable some stuff if some entity is being added or
+deleted.
+
+.. sourcecode:: python
+
+   if self._cw.deleted_in_transaction(self.eidto):
+      return
+
+
+Peculiarities of inlined relations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
+for details) are inserted in the database at the same time as entity attributes.
+
+This may have some side effect, for instance when creating an entity
+and setting an inlined relation in the same rql query, then at
+`before_add_relation` time, the relation will already exist in the
+database (it is otherwise not the case).