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). |
|