doc/book/devrepo/repo/hooks.rst
changeset 10491 c67bcee93248
parent 9184 b982e88e4836
child 12352 1a0159426def
equal deleted inserted replaced
10490:76ab3c71aff2 10491:c67bcee93248
       
     1 .. -*- coding: utf-8 -*-
       
     2 .. _hooks:
       
     3 
       
     4 Hooks and Operations
       
     5 ====================
       
     6 
       
     7 .. autodocstring:: cubicweb.server.hook
       
     8 
       
     9 
       
    10 Example using dataflow hooks
       
    11 ----------------------------
       
    12 
       
    13 We will use a very simple example to show hooks usage. Let us start with the
       
    14 following schema.
       
    15 
       
    16 .. sourcecode:: python
       
    17 
       
    18    class Person(EntityType):
       
    19        age = Int(required=True)
       
    20 
       
    21 We would like to add a range constraint over a person's age. Let's write an hook
       
    22 (supposing yams can not handle this nativly, which is wrong). It shall be placed
       
    23 into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
       
    24 `mycube/hooks/... package` containing hooks in various modules.
       
    25 
       
    26 .. sourcecode:: python
       
    27 
       
    28    from cubicweb import ValidationError
       
    29    from cubicweb.predicates import is_instance
       
    30    from cubicweb.server.hook import Hook
       
    31 
       
    32    class PersonAgeRange(Hook):
       
    33         __regid__ = 'person_age_range'
       
    34         __select__ = Hook.__select__ & is_instance('Person')
       
    35         events = ('before_add_entity', 'before_update_entity')
       
    36 
       
    37         def __call__(self):
       
    38 	    if 'age' in self.entity.cw_edited:
       
    39                 if 0 <= self.entity.age <= 120:
       
    40                    return
       
    41 		msg = self._cw._('age must be between 0 and 120')
       
    42 		raise ValidationError(self.entity.eid, {'age': msg})
       
    43 
       
    44 In our example the base `__select__` is augmented with an `is_instance` selector
       
    45 matching the desired entity type.
       
    46 
       
    47 The `events` tuple is used specify that our hook should be called before the
       
    48 entity is added or updated.
       
    49 
       
    50 Then in the hook's `__call__` method, we:
       
    51 
       
    52 * check if the 'age' attribute is edited
       
    53 * if so, check the value is in the range
       
    54 * if not, raise a validation error properly
       
    55 
       
    56 Now Let's augment our schema with new `Company` entity type with some relation to
       
    57 `Person` (in 'mycube/schema.py').
       
    58 
       
    59 .. sourcecode:: python
       
    60 
       
    61    class Company(EntityType):
       
    62         name = String(required=True)
       
    63         boss = SubjectRelation('Person', cardinality='1*')
       
    64         subsidiary_of = SubjectRelation('Company', cardinality='*?')
       
    65 
       
    66 
       
    67 We would like to constrain the company's bosses to have a minimum (legal)
       
    68 age. Let's write an hook for this, which will be fired when the `boss` relation
       
    69 is established (still supposing we could not specify that kind of thing in the
       
    70 schema).
       
    71 
       
    72 .. sourcecode:: python
       
    73 
       
    74    class CompanyBossLegalAge(Hook):
       
    75         __regid__ = 'company_boss_legal_age'
       
    76         __select__ = Hook.__select__ & match_rtype('boss')
       
    77         events = ('before_add_relation',)
       
    78 
       
    79         def __call__(self):
       
    80             boss = self._cw.entity_from_eid(self.eidto)
       
    81             if boss.age < 18:
       
    82                 msg = self._cw._('the minimum age for a boss is 18')
       
    83                 raise ValidationError(self.eidfrom, {'boss': msg})
       
    84 
       
    85 .. Note::
       
    86 
       
    87     We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the
       
    88     proper relation type.
       
    89 
       
    90     The essential difference with respect to an entity hook is that there is no
       
    91     self.entity, but `self.eidfrom` and `self.eidto` hook attributes which
       
    92     represent the subject and object **eid** of the relation.
       
    93 
       
    94 Suppose we want to check that there is no cycle by the `subsidiary_of`
       
    95 relation. This is best achieved in an operation since all relations are likely to
       
    96 be set at commit time.
       
    97 
       
    98 .. sourcecode:: python
       
    99 
       
   100     from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype
       
   101 
       
   102     def check_cycle(self, session, eid, rtype, role='subject'):
       
   103         parents = set([eid])
       
   104         parent = session.entity_from_eid(eid)
       
   105         while parent.related(rtype, role):
       
   106             parent = parent.related(rtype, role)[0]
       
   107             if parent.eid in parents:
       
   108                 msg = session._('detected %s cycle' % rtype)
       
   109                 raise ValidationError(eid, {rtype: msg})
       
   110             parents.add(parent.eid)
       
   111 
       
   112 
       
   113     class CheckSubsidiaryCycleOp(Operation):
       
   114 
       
   115         def precommit_event(self):
       
   116             check_cycle(self.session, self.eidto, 'subsidiary_of')
       
   117 
       
   118 
       
   119     class CheckSubsidiaryCycleHook(Hook):
       
   120         __regid__ = 'check_no_subsidiary_cycle'
       
   121         __select__ = Hook.__select__ & match_rtype('subsidiary_of')
       
   122         events = ('after_add_relation',)
       
   123 
       
   124         def __call__(self):
       
   125             CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
       
   126 
       
   127 
       
   128 Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other
       
   129 exceptions are usually programming errors.
       
   130 
       
   131 In the above example, our hook will instantiate an operation each time the hook
       
   132 is called, i.e. each time the `subsidiary_of` relation is set. There is an
       
   133 alternative method to schedule an operation from a hook, using the
       
   134 :func:`get_instance` class method.
       
   135 
       
   136 .. sourcecode:: python
       
   137 
       
   138    from cubicweb.server.hook import set_operation
       
   139 
       
   140    class CheckSubsidiaryCycleHook(Hook):
       
   141        __regid__ = 'check_no_subsidiary_cycle'
       
   142        events = ('after_add_relation',)
       
   143        __select__ = Hook.__select__ & match_rtype('subsidiary_of')
       
   144 
       
   145        def __call__(self):
       
   146            CheckSubsidiaryCycleOp.get_instance(self._cw).add_data(self.eidto)
       
   147 
       
   148    class CheckSubsidiaryCycleOp(DataOperationMixIn, Operation):
       
   149 
       
   150        def precommit_event(self):
       
   151            for eid in self.get_data():
       
   152                check_cycle(self.session, eid, self.rtype)
       
   153 
       
   154 
       
   155 Here, we call :func:`set_operation` so that we will simply accumulate eids of
       
   156 entities to check at the end in a single `CheckSubsidiaryCycleOp`
       
   157 operation. Value are stored in a set associated to the
       
   158 'subsidiary_cycle_detection' transaction data key. The set initialization and
       
   159 operation creation are handled nicely by :func:`set_operation`.
       
   160 
       
   161 A more realistic example can be found in the advanced tutorial chapter
       
   162 :ref:`adv_tuto_security_propagation`.
       
   163 
       
   164 
       
   165 Inter-instance communication
       
   166 ----------------------------
       
   167 
       
   168 If your application consists of several instances, you may need some means to
       
   169 communicate between them.  Cubicweb provides a publish/subscribe mechanism
       
   170 using ØMQ_.  In order to use it, use
       
   171 :meth:`~cubicweb.server.cwzmq.ZMQComm.add_subscription` on the
       
   172 `repo.app_instances_bus` object.  The `callback` will get the message (as a
       
   173 list).  A message can be sent by calling
       
   174 :meth:`~cubicweb.server.cwzmq.ZMQComm.publish` on `repo.app_instances_bus`.
       
   175 The first element of the message is the topic which is used for filtering and
       
   176 dispatching messages.
       
   177 
       
   178 .. _ØMQ: http://www.zeromq.org/
       
   179 
       
   180 .. sourcecode:: python
       
   181 
       
   182   class FooHook(hook.Hook):
       
   183       events = ('server_startup',)
       
   184       __regid__ = 'foo_startup'
       
   185 
       
   186       def __call__(self):
       
   187           def callback(msg):
       
   188               self.info('received message: %s', ' '.join(msg))
       
   189           self.repo.app_instances_bus.add_subscription('hello', callback)
       
   190 
       
   191 .. sourcecode:: python
       
   192 
       
   193   def do_foo(self):
       
   194       actually_do_foo()
       
   195       self._cw.repo.app_instances_bus.publish(['hello', 'world'])
       
   196 
       
   197 The `zmq-address-pub` configuration variable contains the address used
       
   198 by the instance for sending messages, e.g. `tcp://*:1234`.  The
       
   199 `zmq-address-sub` variable contains a comma-separated list of addresses
       
   200 to listen on, e.g. `tcp://localhost:1234, tcp://192.168.1.1:2345`.
       
   201 
       
   202 
       
   203 Hooks writing tips
       
   204 ------------------
       
   205 
       
   206 Reminder
       
   207 ~~~~~~~~
       
   208 
       
   209 You should never use the `entity.foo = 42` notation to update an entity. It will
       
   210 not do what you expect (updating the database). Instead, use the
       
   211 :meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
       
   212 :attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
       
   213 'before_update_entity' event.
       
   214 
       
   215 
       
   216 How to choose between a before and an after event ?
       
   217 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   218 
       
   219 `before_*` hooks give you access to the old attribute (or relation)
       
   220 values. You can also intercept and update edited values in the case of
       
   221 entity modification before they reach the database.
       
   222 
       
   223 Else the question is: should I need to do things before or after the actual
       
   224 modification ? If the answer is "it doesn't matter", use an 'after' event.
       
   225 
       
   226 
       
   227 Validation Errors
       
   228 ~~~~~~~~~~~~~~~~~
       
   229 
       
   230 When a hook which is responsible to maintain the consistency of the
       
   231 data model detects an error, it must use a specific exception named
       
   232 :exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
       
   233 :exc:`~cubicweb.ValidationError` is a programming error. Raising it
       
   234 entails aborting the current transaction.
       
   235 
       
   236 This exception is used to convey enough information up to the user
       
   237 interface. Hence its constructor is different from the default Exception
       
   238 constructor. It accepts, positionally:
       
   239 
       
   240 * an entity eid (**not the entity itself**),
       
   241 
       
   242 * a dict whose keys represent attribute (or relation) names and values
       
   243   an end-user facing message (hence properly translated) relating the
       
   244   problem.
       
   245 
       
   246 .. sourcecode:: python
       
   247 
       
   248   raise ValidationError(earth.eid, {'sea_level': self._cw._('too high'),
       
   249                                     'temperature': self._cw._('too hot')})
       
   250 
       
   251 
       
   252 Checking for object created/deleted in the current transaction
       
   253 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   254 
       
   255 In hooks, you can use the
       
   256 :meth:`~cubicweb.server.session.Session.added_in_transaction` or
       
   257 :meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session
       
   258 object to check if an eid has been created or deleted during the hook's
       
   259 transaction.
       
   260 
       
   261 This is useful to enable or disable some stuff if some entity is being added or
       
   262 deleted.
       
   263 
       
   264 .. sourcecode:: python
       
   265 
       
   266    if self._cw.deleted_in_transaction(self.eidto):
       
   267       return
       
   268 
       
   269 
       
   270 Peculiarities of inlined relations
       
   271 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   272 
       
   273 Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
       
   274 for details) are inserted in the database at the same time as entity attributes.
       
   275 
       
   276 This may have some side effect, for instance when creating an entity
       
   277 and setting an inlined relation in the same rql query, then at
       
   278 `before_add_relation` time, the relation will already exist in the
       
   279 database (it is otherwise not the case).