|
1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
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/>. |
|
18 """.. _Selectors: |
|
19 |
|
20 Predicates and selectors |
|
21 ------------------------ |
|
22 |
|
23 A predicate is a class testing a particular aspect of a context. A selector is |
|
24 built by combining existant predicates or even selectors. |
|
25 |
|
26 Using and combining existant predicates |
|
27 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
28 |
|
29 You can combine predicates using the `&`, `|` and `~` operators. |
|
30 |
|
31 When two predicates are combined using the `&` operator, it means that |
|
32 both should return a positive score. On success, the sum of scores is |
|
33 returned. |
|
34 |
|
35 When two predicates are combined using the `|` operator, it means that |
|
36 one of them should return a positive score. On success, the first |
|
37 positive score is returned. |
|
38 |
|
39 You can also "negate" a predicate by precedeing it by the `~` unary operator. |
|
40 |
|
41 Of course you can use parenthesis to balance expressions. |
|
42 |
|
43 Example |
|
44 ~~~~~~~ |
|
45 |
|
46 The goal: when on a blog, one wants the RSS link to refer to blog entries, not to |
|
47 the blog entity itself. |
|
48 |
|
49 To do that, one defines a method on entity classes that returns the |
|
50 RSS stream url for a given entity. The default implementation on |
|
51 :class:`~cubicweb.entities.AnyEntity` (the generic entity class used |
|
52 as base for all others) and a specific implementation on `Blog` will |
|
53 do what we want. |
|
54 |
|
55 But when we have a result set containing several `Blog` entities (or |
|
56 different entities), we don't know on which entity to call the |
|
57 aforementioned method. In this case, we keep the generic behaviour. |
|
58 |
|
59 Hence we have two cases here, one for a single-entity rsets, the other for |
|
60 multi-entities rsets. |
|
61 |
|
62 In web/views/boxes.py lies the RSSIconBox class. Look at its selector: |
|
63 |
|
64 .. sourcecode:: python |
|
65 |
|
66 class RSSIconBox(box.Box): |
|
67 ''' just display the RSS icon on uniform result set ''' |
|
68 __select__ = box.Box.__select__ & non_final_entity() |
|
69 |
|
70 It takes into account: |
|
71 |
|
72 * the inherited selection criteria (one has to look them up in the class |
|
73 hierarchy to know the details) |
|
74 |
|
75 * :class:`~cubicweb.predicates.non_final_entity`, which filters on result sets |
|
76 containing non final entities (a 'final entity' being synonym for entity |
|
77 attributes type, eg `String`, `Int`, etc) |
|
78 |
|
79 This matches our second case. Hence we have to provide a specific component for |
|
80 the first case: |
|
81 |
|
82 .. sourcecode:: python |
|
83 |
|
84 class EntityRSSIconBox(RSSIconBox): |
|
85 '''just display the RSS icon on uniform result set for a single entity''' |
|
86 __select__ = RSSIconBox.__select__ & one_line_rset() |
|
87 |
|
88 Here, one adds the :class:`~cubicweb.predicates.one_line_rset` predicate, which |
|
89 filters result sets of size 1. Thus, on a result set containing multiple |
|
90 entities, :class:`one_line_rset` makes the EntityRSSIconBox class non |
|
91 selectable. However for a result set with one entity, the `EntityRSSIconBox` |
|
92 class will have a higher score than `RSSIconBox`, which is what we wanted. |
|
93 |
|
94 Of course, once this is done, you have to: |
|
95 |
|
96 * fill in the call method of `EntityRSSIconBox` |
|
97 |
|
98 * provide the default implementation of the method returning the RSS stream url |
|
99 on :class:`~cubicweb.entities.AnyEntity` |
|
100 |
|
101 * redefine this method on `Blog`. |
|
102 |
|
103 |
|
104 When to use selectors? |
|
105 ~~~~~~~~~~~~~~~~~~~~~~ |
|
106 |
|
107 Selectors are to be used whenever arises the need of dispatching on the shape or |
|
108 content of a result set or whatever else context (value in request form params, |
|
109 authenticated user groups, etc...). That is, almost all the time. |
|
110 |
|
111 Here is a quick example: |
|
112 |
|
113 .. sourcecode:: python |
|
114 |
|
115 class UserLink(component.Component): |
|
116 '''if the user is the anonymous user, build a link to login else a link |
|
117 to the connected user object with a logout link |
|
118 ''' |
|
119 __regid__ = 'loggeduserlink' |
|
120 |
|
121 def call(self): |
|
122 if self._cw.session.anonymous_session: |
|
123 # display login link |
|
124 ... |
|
125 else: |
|
126 # display a link to the connected user object with a loggout link |
|
127 ... |
|
128 |
|
129 The proper way to implement this with |cubicweb| is two have two different |
|
130 classes sharing the same identifier but with different selectors so you'll get |
|
131 the correct one according to the context. |
|
132 |
|
133 .. sourcecode:: python |
|
134 |
|
135 class UserLink(component.Component): |
|
136 '''display a link to the connected user object with a loggout link''' |
|
137 __regid__ = 'loggeduserlink' |
|
138 __select__ = component.Component.__select__ & authenticated_user() |
|
139 |
|
140 def call(self): |
|
141 # display useractions and siteactions |
|
142 ... |
|
143 |
|
144 class AnonUserLink(component.Component): |
|
145 '''build a link to login''' |
|
146 __regid__ = 'loggeduserlink' |
|
147 __select__ = component.Component.__select__ & anonymous_user() |
|
148 |
|
149 def call(self): |
|
150 # display login link |
|
151 ... |
|
152 |
|
153 The big advantage, aside readability once you're familiar with the |
|
154 system, is that your cube becomes much more easily customizable by |
|
155 improving componentization. |
|
156 |
|
157 |
|
158 .. _CustomPredicates: |
|
159 |
|
160 Defining your own predicates |
|
161 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
162 |
|
163 .. autodocstring:: cubicweb.appobject::objectify_predicate |
|
164 |
|
165 In other cases, you can take a look at the following abstract base classes: |
|
166 |
|
167 .. autoclass:: cubicweb.predicates.ExpectedValuePredicate |
|
168 .. autoclass:: cubicweb.predicates.EClassPredicate |
|
169 .. autoclass:: cubicweb.predicates.EntityPredicate |
|
170 |
|
171 .. _DebuggingSelectors: |
|
172 |
|
173 Debugging selection |
|
174 ~~~~~~~~~~~~~~~~~~~ |
|
175 |
|
176 Once in a while, one needs to understand why a view (or any application object) |
|
177 is, or is not selected appropriately. Looking at which predicates fired (or did |
|
178 not) is the way. The :class:`logilab.common.registry.traced_selection` context |
|
179 manager to help with that, *if you're running your instance in debug mode*. |
|
180 |
|
181 .. autoclass:: logilab.common.registry.traced_selection |
|
182 |
|
183 """ |
|
184 |
|
185 __docformat__ = "restructuredtext en" |
|
186 |
|
187 import logging |
|
188 from warnings import warn |
|
189 from operator import eq |
|
190 |
|
191 from logilab.common.compat import all, any |
|
192 from logilab.common.interface import implements as implements_iface |
|
193 from logilab.common.registry import Predicate, objectify_predicate |
|
194 |
|
195 from yams.schema import BASE_TYPES, role_name |
|
196 from rql.nodes import Function |
|
197 |
|
198 from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity, |
|
199 CW_EVENT_MANAGER, role) |
|
200 # even if not used, let yes here so it's importable through this module |
|
201 from cubicweb.uilib import eid_param |
|
202 from cubicweb.schema import split_expression |
|
203 |
|
204 def score_interface(etypesreg, eclass, iface): |
|
205 """Return XXX if the give object (maybe an instance or class) implements |
|
206 the interface. |
|
207 """ |
|
208 if getattr(iface, '__registry__', None) == 'etypes': |
|
209 # adjust score if the interface is an entity class |
|
210 parents, any = etypesreg.parent_classes(eclass.__regid__) |
|
211 if iface is eclass: |
|
212 return len(parents) + 4 |
|
213 if iface is any: # Any |
|
214 return 1 |
|
215 for index, basecls in enumerate(reversed(parents)): |
|
216 if iface is basecls: |
|
217 return index + 3 |
|
218 return 0 |
|
219 # XXX iface in implements deprecated in 3.9 |
|
220 if implements_iface(eclass, iface): |
|
221 # implementing an interface takes precedence other special Any interface |
|
222 return 2 |
|
223 return 0 |
|
224 |
|
225 |
|
226 # abstract predicates / mixin helpers ########################################### |
|
227 |
|
228 class PartialPredicateMixIn(object): |
|
229 """convenience mix-in for predicates that will look into the containing |
|
230 class to find missing information. |
|
231 |
|
232 cf. `cubicweb.web.action.LinkToEntityAction` for instance |
|
233 """ |
|
234 def __call__(self, cls, *args, **kwargs): |
|
235 self.complete(cls) |
|
236 return super(PartialPredicateMixIn, self).__call__(cls, *args, **kwargs) |
|
237 |
|
238 |
|
239 class EClassPredicate(Predicate): |
|
240 """abstract class for predicates working on *entity class(es)* specified |
|
241 explicitly or found of the result set. |
|
242 |
|
243 Here are entity lookup / scoring rules: |
|
244 |
|
245 * if `entity` is specified, return score for this entity's class |
|
246 |
|
247 * elif `rset`, `select` and `filtered_variable` are specified, return score |
|
248 for the possible classes for variable in the given rql :class:`Select` |
|
249 node |
|
250 |
|
251 * elif `rset` and `row` are specified, return score for the class of the |
|
252 entity found in the specified cell, using column specified by `col` or 0 |
|
253 |
|
254 * elif `rset` is specified return score for each entity class found in the |
|
255 column specified specified by the `col` argument or in column 0 if not |
|
256 specified |
|
257 |
|
258 When there are several classes to be evaluated, return the sum of scores for |
|
259 each entity class unless: |
|
260 |
|
261 - `mode` == 'all' (the default) and some entity class is scored |
|
262 to 0, in which case 0 is returned |
|
263 |
|
264 - `mode` == 'any', in which case the first non-zero score is |
|
265 returned |
|
266 |
|
267 - `accept_none` is False and some cell in the column has a None value |
|
268 (this may occurs with outer join) |
|
269 """ |
|
270 def __init__(self, once_is_enough=None, accept_none=True, mode='all'): |
|
271 if once_is_enough is not None: |
|
272 warn("[3.14] once_is_enough is deprecated, use mode='any'", |
|
273 DeprecationWarning, stacklevel=2) |
|
274 if once_is_enough: |
|
275 mode = 'any' |
|
276 assert mode in ('any', 'all'), 'bad mode %s' % mode |
|
277 self.once_is_enough = mode == 'any' |
|
278 self.accept_none = accept_none |
|
279 |
|
280 def __call__(self, cls, req, rset=None, row=None, col=0, entity=None, |
|
281 select=None, filtered_variable=None, |
|
282 accept_none=None, |
|
283 **kwargs): |
|
284 if entity is not None: |
|
285 return self.score_class(entity.__class__, req) |
|
286 if not rset: |
|
287 return 0 |
|
288 if select is not None and filtered_variable is not None: |
|
289 etypes = set(sol[filtered_variable.name] for sol in select.solutions) |
|
290 elif row is None: |
|
291 if accept_none is None: |
|
292 accept_none = self.accept_none |
|
293 if not accept_none and \ |
|
294 any(rset[i][col] is None for i in xrange(len(rset))): |
|
295 return 0 |
|
296 etypes = rset.column_types(col) |
|
297 else: |
|
298 etype = rset.description[row][col] |
|
299 # may have None in rset.description on outer join |
|
300 if etype is None or rset.rows[row][col] is None: |
|
301 return 0 |
|
302 etypes = (etype,) |
|
303 score = 0 |
|
304 for etype in etypes: |
|
305 escore = self.score(cls, req, etype) |
|
306 if not escore and not self.once_is_enough: |
|
307 return 0 |
|
308 elif self.once_is_enough: |
|
309 return escore |
|
310 score += escore |
|
311 return score |
|
312 |
|
313 def score(self, cls, req, etype): |
|
314 if etype in BASE_TYPES: |
|
315 return 0 |
|
316 return self.score_class(req.vreg['etypes'].etype_class(etype), req) |
|
317 |
|
318 def score_class(self, eclass, req): |
|
319 raise NotImplementedError() |
|
320 |
|
321 |
|
322 class EntityPredicate(EClassPredicate): |
|
323 """abstract class for predicates working on *entity instance(s)* specified |
|
324 explicitly or found of the result set. |
|
325 |
|
326 Here are entity lookup / scoring rules: |
|
327 |
|
328 * if `entity` is specified, return score for this entity |
|
329 |
|
330 * elif `row` is specified, return score for the entity found in the |
|
331 specified cell, using column specified by `col` or 0 |
|
332 |
|
333 * else return the sum of scores for each entity found in the column |
|
334 specified specified by the `col` argument or in column 0 if not specified, |
|
335 unless: |
|
336 |
|
337 - `mode` == 'all' (the default) and some entity class is scored |
|
338 to 0, in which case 0 is returned |
|
339 |
|
340 - `mode` == 'any', in which case the first non-zero score is |
|
341 returned |
|
342 |
|
343 - `accept_none` is False and some cell in the column has a None value |
|
344 (this may occurs with outer join) |
|
345 |
|
346 .. Note:: |
|
347 using :class:`EntityPredicate` or :class:`EClassPredicate` as base predicate |
|
348 class impacts performance, since when no entity or row is specified the |
|
349 later works on every different *entity class* found in the result set, |
|
350 while the former works on each *entity* (eg each row of the result set), |
|
351 which may be much more costly. |
|
352 """ |
|
353 |
|
354 def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None, |
|
355 **kwargs): |
|
356 if not rset and not kwargs.get('entity'): |
|
357 return 0 |
|
358 score = 0 |
|
359 if kwargs.get('entity'): |
|
360 score = self.score_entity(kwargs['entity']) |
|
361 elif row is None: |
|
362 col = col or 0 |
|
363 if accept_none is None: |
|
364 accept_none = self.accept_none |
|
365 for row, rowvalue in enumerate(rset.rows): |
|
366 if rowvalue[col] is None: # outer join |
|
367 if not accept_none: |
|
368 return 0 |
|
369 continue |
|
370 escore = self.score(req, rset, row, col) |
|
371 if not escore and not self.once_is_enough: |
|
372 return 0 |
|
373 elif self.once_is_enough: |
|
374 return escore |
|
375 score += escore |
|
376 else: |
|
377 col = col or 0 |
|
378 etype = rset.description[row][col] |
|
379 if etype is not None: # outer join |
|
380 score = self.score(req, rset, row, col) |
|
381 return score |
|
382 |
|
383 def score(self, req, rset, row, col): |
|
384 try: |
|
385 return self.score_entity(rset.get_entity(row, col)) |
|
386 except NotAnEntity: |
|
387 return 0 |
|
388 |
|
389 def score_entity(self, entity): |
|
390 raise NotImplementedError() |
|
391 |
|
392 |
|
393 class ExpectedValuePredicate(Predicate): |
|
394 """Take a list of expected values as initializer argument and store them |
|
395 into the :attr:`expected` set attribute. You may also give a set as single |
|
396 argument, which will then be referenced as set of expected values, |
|
397 allowing modifications to the given set to be considered. |
|
398 |
|
399 You should implement one of :meth:`_values_set(cls, req, **kwargs)` or |
|
400 :meth:`_get_value(cls, req, **kwargs)` method which should respectively |
|
401 return the set of values or the unique possible value for the given context. |
|
402 |
|
403 You may also specify a `mode` behaviour as argument, as explained below. |
|
404 |
|
405 Returned score is: |
|
406 |
|
407 - 0 if `mode` == 'all' (the default) and at least one expected |
|
408 values isn't found |
|
409 |
|
410 - 0 if `mode` == 'any' and no expected values isn't found at all |
|
411 |
|
412 - else the number of matching values |
|
413 |
|
414 Notice `mode` = 'any' with a single expected value has no effect at all. |
|
415 """ |
|
416 def __init__(self, *expected, **kwargs): |
|
417 assert expected, self |
|
418 if len(expected) == 1 and isinstance(expected[0], set): |
|
419 self.expected = expected[0] |
|
420 else: |
|
421 self.expected = frozenset(expected) |
|
422 mode = kwargs.pop('mode', 'all') |
|
423 assert mode in ('any', 'all'), 'bad mode %s' % mode |
|
424 self.once_is_enough = mode == 'any' |
|
425 assert not kwargs, 'unexpected arguments %s' % kwargs |
|
426 |
|
427 def __str__(self): |
|
428 return '%s(%s)' % (self.__class__.__name__, |
|
429 ','.join(sorted(str(s) for s in self.expected))) |
|
430 |
|
431 def __call__(self, cls, req, **kwargs): |
|
432 values = self._values_set(cls, req, **kwargs) |
|
433 matching = len(values & self.expected) |
|
434 if self.once_is_enough: |
|
435 return matching |
|
436 if matching == len(self.expected): |
|
437 return matching |
|
438 return 0 |
|
439 |
|
440 def _values_set(self, cls, req, **kwargs): |
|
441 return frozenset( (self._get_value(cls, req, **kwargs),) ) |
|
442 |
|
443 def _get_value(self, cls, req, **kwargs): |
|
444 raise NotImplementedError() |
|
445 |
|
446 |
|
447 # bare predicates ############################################################## |
|
448 |
|
449 class match_kwargs(ExpectedValuePredicate): |
|
450 """Return non-zero score if parameter names specified as initializer |
|
451 arguments are specified in the input context. |
|
452 |
|
453 |
|
454 Return a score corresponding to the number of expected parameters. |
|
455 |
|
456 When multiple parameters are expected, all of them should be found in |
|
457 the input context unless `mode` keyword argument is given to 'any', |
|
458 in which case a single matching parameter is enough. |
|
459 """ |
|
460 |
|
461 def _values_set(self, cls, req, **kwargs): |
|
462 return frozenset(kwargs) |
|
463 |
|
464 |
|
465 class appobject_selectable(Predicate): |
|
466 """Return 1 if another appobject is selectable using the same input context. |
|
467 |
|
468 Initializer arguments: |
|
469 |
|
470 * `registry`, a registry name |
|
471 |
|
472 * `regids`, object identifiers in this registry, one of them should be |
|
473 selectable. |
|
474 """ |
|
475 selectable_score = 1 |
|
476 def __init__(self, registry, *regids): |
|
477 self.registry = registry |
|
478 self.regids = regids |
|
479 |
|
480 def __call__(self, cls, req, **kwargs): |
|
481 for regid in self.regids: |
|
482 try: |
|
483 req.vreg[self.registry].select(regid, req, **kwargs) |
|
484 return self.selectable_score |
|
485 except NoSelectableObject: |
|
486 continue |
|
487 return 0 |
|
488 |
|
489 |
|
490 class adaptable(appobject_selectable): |
|
491 """Return 1 if another appobject is selectable using the same input context. |
|
492 |
|
493 Initializer arguments: |
|
494 |
|
495 * `regids`, adapter identifiers (e.g. interface names) to which the context |
|
496 (usually entities) should be adaptable. One of them should be selectable |
|
497 when multiple identifiers are given. |
|
498 """ |
|
499 def __init__(self, *regids): |
|
500 super(adaptable, self).__init__('adapters', *regids) |
|
501 |
|
502 def __call__(self, cls, req, **kwargs): |
|
503 kwargs.setdefault('accept_none', False) |
|
504 # being adaptable to an interface should takes precedence other is_instance('Any'), |
|
505 # but not other explicit is_instance('SomeEntityType'), and: |
|
506 # * is_instance('Any') score is 1 |
|
507 # * is_instance('SomeEntityType') score is at least 2 |
|
508 score = super(adaptable, self).__call__(cls, req, **kwargs) |
|
509 if score >= 2: |
|
510 return score - 0.5 |
|
511 if score == 1: |
|
512 return score + 0.5 |
|
513 return score |
|
514 |
|
515 |
|
516 class configuration_values(Predicate): |
|
517 """Return 1 if the instance has an option set to a given value(s) in its |
|
518 configuration file. |
|
519 """ |
|
520 # XXX this predicate could be evaluated on startup |
|
521 def __init__(self, key, values): |
|
522 self._key = key |
|
523 if not isinstance(values, (tuple, list)): |
|
524 values = (values,) |
|
525 self._values = frozenset(values) |
|
526 |
|
527 def __call__(self, cls, req, **kwargs): |
|
528 try: |
|
529 return self._score |
|
530 except AttributeError: |
|
531 if req is None: |
|
532 config = kwargs['repo'].config |
|
533 else: |
|
534 config = req.vreg.config |
|
535 self._score = config[self._key] in self._values |
|
536 return self._score |
|
537 |
|
538 |
|
539 # rset predicates ############################################################## |
|
540 |
|
541 @objectify_predicate |
|
542 def none_rset(cls, req, rset=None, **kwargs): |
|
543 """Return 1 if the result set is None (eg usually not specified).""" |
|
544 if rset is None: |
|
545 return 1 |
|
546 return 0 |
|
547 |
|
548 |
|
549 # XXX == ~ none_rset |
|
550 @objectify_predicate |
|
551 def any_rset(cls, req, rset=None, **kwargs): |
|
552 """Return 1 for any result set, whatever the number of rows in it, even 0.""" |
|
553 if rset is not None: |
|
554 return 1 |
|
555 return 0 |
|
556 |
|
557 |
|
558 @objectify_predicate |
|
559 def nonempty_rset(cls, req, rset=None, **kwargs): |
|
560 """Return 1 for result set containing one ore more rows.""" |
|
561 if rset is not None and rset.rowcount: |
|
562 return 1 |
|
563 return 0 |
|
564 |
|
565 |
|
566 # XXX == ~ nonempty_rset |
|
567 @objectify_predicate |
|
568 def empty_rset(cls, req, rset=None, **kwargs): |
|
569 """Return 1 for result set which doesn't contain any row.""" |
|
570 if rset is not None and rset.rowcount == 0: |
|
571 return 1 |
|
572 return 0 |
|
573 |
|
574 |
|
575 # XXX == multi_lines_rset(1) |
|
576 @objectify_predicate |
|
577 def one_line_rset(cls, req, rset=None, row=None, **kwargs): |
|
578 """Return 1 if the result set is of size 1, or greater but a specific row in |
|
579 the result set is specified ('row' argument). |
|
580 """ |
|
581 if rset is None and 'entity' in kwargs: |
|
582 return 1 |
|
583 if rset is not None and (row is not None or rset.rowcount == 1): |
|
584 return 1 |
|
585 return 0 |
|
586 |
|
587 |
|
588 class multi_lines_rset(Predicate): |
|
589 """Return 1 if the operator expression matches between `num` elements |
|
590 in the result set and the `expected` value if defined. |
|
591 |
|
592 By default, multi_lines_rset(expected) matches equality expression: |
|
593 `nb` row(s) in result set equals to expected value |
|
594 But, you can perform richer comparisons by overriding default operator: |
|
595 multi_lines_rset(expected, operator.gt) |
|
596 |
|
597 If `expected` is None, return 1 if the result set contains *at least* |
|
598 two rows. |
|
599 If rset is None, return 0. |
|
600 """ |
|
601 def __init__(self, expected=None, operator=eq): |
|
602 self.expected = expected |
|
603 self.operator = operator |
|
604 |
|
605 def match_expected(self, num): |
|
606 if self.expected is None: |
|
607 return num > 1 |
|
608 return self.operator(num, self.expected) |
|
609 |
|
610 def __call__(self, cls, req, rset=None, **kwargs): |
|
611 return int(rset is not None and self.match_expected(rset.rowcount)) |
|
612 |
|
613 |
|
614 class multi_columns_rset(multi_lines_rset): |
|
615 """If `nb` is specified, return 1 if the result set has exactly `nb` column |
|
616 per row. Else (`nb` is None), return 1 if the result set contains *at least* |
|
617 two columns per row. Return 0 for empty result set. |
|
618 """ |
|
619 |
|
620 def __call__(self, cls, req, rset=None, **kwargs): |
|
621 # 'or 0' since we *must not* return None |
|
622 return rset and self.match_expected(len(rset.rows[0])) or 0 |
|
623 |
|
624 |
|
625 class paginated_rset(Predicate): |
|
626 """Return 1 or more for result set with more rows than one or more page |
|
627 size. You can specify expected number of pages to the initializer (default |
|
628 to one), and you'll get that number of pages as score if the result set is |
|
629 big enough. |
|
630 |
|
631 Page size is searched in (respecting order): |
|
632 * a `page_size` argument |
|
633 * a `page_size` form parameters |
|
634 * the `navigation.page-size` property (see :ref:`PersistentProperties`) |
|
635 """ |
|
636 def __init__(self, nbpages=1): |
|
637 assert nbpages > 0 |
|
638 self.nbpages = nbpages |
|
639 |
|
640 def __call__(self, cls, req, rset=None, **kwargs): |
|
641 if rset is None: |
|
642 return 0 |
|
643 page_size = kwargs.get('page_size') |
|
644 if page_size is None: |
|
645 page_size = req.form.get('page_size') |
|
646 if page_size is None: |
|
647 page_size = req.property_value('navigation.page-size') |
|
648 else: |
|
649 page_size = int(page_size) |
|
650 if rset.rowcount <= (page_size*self.nbpages): |
|
651 return 0 |
|
652 return self.nbpages |
|
653 |
|
654 |
|
655 @objectify_predicate |
|
656 def sorted_rset(cls, req, rset=None, **kwargs): |
|
657 """Return 1 for sorted result set (e.g. from an RQL query containing an |
|
658 ORDERBY clause), with exception that it will return 0 if the rset is |
|
659 'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index). |
|
660 """ |
|
661 if rset is None: |
|
662 return 0 |
|
663 selects = rset.syntax_tree().children |
|
664 if (len(selects) > 1 or |
|
665 not selects[0].orderby or |
|
666 (isinstance(selects[0].orderby[0].term, Function) and |
|
667 selects[0].orderby[0].term.name == 'FTIRANK') |
|
668 ): |
|
669 return 0 |
|
670 return 2 |
|
671 |
|
672 |
|
673 # XXX == multi_etypes_rset(1) |
|
674 @objectify_predicate |
|
675 def one_etype_rset(cls, req, rset=None, col=0, **kwargs): |
|
676 """Return 1 if the result set contains entities which are all of the same |
|
677 type in the column specified by the `col` argument of the input context, or |
|
678 in column 0. |
|
679 """ |
|
680 if rset is None: |
|
681 return 0 |
|
682 if len(rset.column_types(col)) != 1: |
|
683 return 0 |
|
684 return 1 |
|
685 |
|
686 |
|
687 class multi_etypes_rset(multi_lines_rset): |
|
688 """If `nb` is specified, return 1 if the result set contains `nb` different |
|
689 types of entities in the column specified by the `col` argument of the input |
|
690 context, or in column 0. If `nb` is None, return 1 if the result set contains |
|
691 *at least* two different types of entities. |
|
692 """ |
|
693 |
|
694 def __call__(self, cls, req, rset=None, col=0, **kwargs): |
|
695 # 'or 0' since we *must not* return None |
|
696 return rset and self.match_expected(len(rset.column_types(col))) or 0 |
|
697 |
|
698 |
|
699 @objectify_predicate |
|
700 def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs): |
|
701 """Return positive score if the result set at the specified row / col |
|
702 contains the eid of the logged user. |
|
703 """ |
|
704 if rset is None: |
|
705 return 0 |
|
706 return req.user.eid == rset[row or 0][col] |
|
707 |
|
708 |
|
709 # entity predicates ############################################################# |
|
710 |
|
711 class non_final_entity(EClassPredicate): |
|
712 """Return 1 for entity of a non final entity type(s). Remember, "final" |
|
713 entity types are String, Int, etc... This is equivalent to |
|
714 `is_instance('Any')` but more optimized. |
|
715 |
|
716 See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity |
|
717 class lookup / score rules according to the input context. |
|
718 """ |
|
719 def score(self, cls, req, etype): |
|
720 if etype in BASE_TYPES: |
|
721 return 0 |
|
722 return 1 |
|
723 |
|
724 def score_class(self, eclass, req): |
|
725 return 1 # necessarily true if we're there |
|
726 |
|
727 |
|
728 class implements(EClassPredicate): |
|
729 """Return non-zero score for entity that are of the given type(s) or |
|
730 implements at least one of the given interface(s). If multiple arguments are |
|
731 given, matching one of them is enough. |
|
732 |
|
733 Entity types should be given as string, the corresponding class will be |
|
734 fetched from the entity types registry at selection time. |
|
735 |
|
736 See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity |
|
737 class lookup / score rules according to the input context. |
|
738 |
|
739 .. note:: when interface is an entity class, the score will reflect class |
|
740 proximity so the most specific object will be selected. |
|
741 |
|
742 .. note:: deprecated in cubicweb >= 3.9, use either |
|
743 :class:`~cubicweb.predicates.is_instance` or |
|
744 :class:`~cubicweb.predicates.adaptable`. |
|
745 """ |
|
746 |
|
747 def __init__(self, *expected_ifaces, **kwargs): |
|
748 emit_warn = kwargs.pop('warn', True) |
|
749 super(implements, self).__init__(**kwargs) |
|
750 self.expected_ifaces = expected_ifaces |
|
751 if emit_warn: |
|
752 warn('[3.9] implements predicate is deprecated, use either ' |
|
753 'is_instance or adaptable', DeprecationWarning, stacklevel=2) |
|
754 |
|
755 def __str__(self): |
|
756 return '%s(%s)' % (self.__class__.__name__, |
|
757 ','.join(str(s) for s in self.expected_ifaces)) |
|
758 |
|
759 def score_class(self, eclass, req): |
|
760 score = 0 |
|
761 etypesreg = req.vreg['etypes'] |
|
762 for iface in self.expected_ifaces: |
|
763 if isinstance(iface, basestring): |
|
764 # entity type |
|
765 try: |
|
766 iface = etypesreg.etype_class(iface) |
|
767 except KeyError: |
|
768 continue # entity type not in the schema |
|
769 score += score_interface(etypesreg, eclass, iface) |
|
770 return score |
|
771 |
|
772 def _reset_is_instance_cache(vreg): |
|
773 vreg._is_instance_predicate_cache = {} |
|
774 |
|
775 CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache) |
|
776 |
|
777 class is_instance(EClassPredicate): |
|
778 """Return non-zero score for entity that is an instance of the one of given |
|
779 type(s). If multiple arguments are given, matching one of them is enough. |
|
780 |
|
781 Entity types should be given as string, the corresponding class will be |
|
782 fetched from the registry at selection time. |
|
783 |
|
784 See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity |
|
785 class lookup / score rules according to the input context. |
|
786 |
|
787 .. note:: the score will reflect class proximity so the most specific object |
|
788 will be selected. |
|
789 """ |
|
790 |
|
791 def __init__(self, *expected_etypes, **kwargs): |
|
792 super(is_instance, self).__init__(**kwargs) |
|
793 self.expected_etypes = expected_etypes |
|
794 for etype in self.expected_etypes: |
|
795 assert isinstance(etype, basestring), etype |
|
796 |
|
797 def __str__(self): |
|
798 return '%s(%s)' % (self.__class__.__name__, |
|
799 ','.join(str(s) for s in self.expected_etypes)) |
|
800 |
|
801 def score_class(self, eclass, req): |
|
802 # cache on vreg to avoid reloading issues |
|
803 try: |
|
804 cache = req.vreg._is_instance_predicate_cache |
|
805 except AttributeError: |
|
806 # XXX 'before-registry-reset' not called for db-api connections |
|
807 cache = req.vreg._is_instance_predicate_cache = {} |
|
808 try: |
|
809 expected_eclasses = cache[self] |
|
810 except KeyError: |
|
811 # turn list of entity types as string into a list of |
|
812 # (entity class, parent classes) |
|
813 etypesreg = req.vreg['etypes'] |
|
814 expected_eclasses = cache[self] = [] |
|
815 for etype in self.expected_etypes: |
|
816 try: |
|
817 expected_eclasses.append(etypesreg.etype_class(etype)) |
|
818 except KeyError: |
|
819 continue # entity type not in the schema |
|
820 parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__) |
|
821 score = 0 |
|
822 for expectedcls in expected_eclasses: |
|
823 # adjust score according to class proximity |
|
824 if expectedcls is eclass: |
|
825 score += len(parents) + 4 |
|
826 elif expectedcls is any: # Any |
|
827 score += 1 |
|
828 else: |
|
829 for index, basecls in enumerate(reversed(parents)): |
|
830 if expectedcls is basecls: |
|
831 score += index + 3 |
|
832 break |
|
833 return score |
|
834 |
|
835 |
|
836 class score_entity(EntityPredicate): |
|
837 """Return score according to an arbitrary function given as argument which |
|
838 will be called with input content entity as argument. |
|
839 |
|
840 This is a very useful predicate that will usually interest you since it |
|
841 allows a lot of things without having to write a specific predicate. |
|
842 |
|
843 The function can return arbitrary value which will be casted to an integer |
|
844 value at the end. |
|
845 |
|
846 See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity |
|
847 lookup / score rules according to the input context. |
|
848 """ |
|
849 def __init__(self, scorefunc, once_is_enough=None, mode='all'): |
|
850 super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough) |
|
851 def intscore(*args, **kwargs): |
|
852 score = scorefunc(*args, **kwargs) |
|
853 if not score: |
|
854 return 0 |
|
855 if isinstance(score, (int, long)): |
|
856 return score |
|
857 return 1 |
|
858 self.score_entity = intscore |
|
859 |
|
860 |
|
861 class has_mimetype(EntityPredicate): |
|
862 """Return 1 if the entity adapt to IDownloadable and has the given MIME type. |
|
863 |
|
864 You can give 'image/' to match any image for instance, or 'image/png' to match |
|
865 only PNG images. |
|
866 """ |
|
867 def __init__(self, mimetype, once_is_enough=None, mode='all'): |
|
868 super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough) |
|
869 self.mimetype = mimetype |
|
870 |
|
871 def score_entity(self, entity): |
|
872 idownloadable = entity.cw_adapt_to('IDownloadable') |
|
873 if idownloadable is None: |
|
874 return 0 |
|
875 mt = idownloadable.download_content_type() |
|
876 if not (mt and mt.startswith(self.mimetype)): |
|
877 return 0 |
|
878 return 1 |
|
879 |
|
880 |
|
881 class relation_possible(EntityPredicate): |
|
882 """Return 1 for entity that supports the relation, provided that the |
|
883 request's user may do some `action` on it (see below). |
|
884 |
|
885 The relation is specified by the following initializer arguments: |
|
886 |
|
887 * `rtype`, the name of the relation |
|
888 |
|
889 * `role`, the role of the entity in the relation, either 'subject' or |
|
890 'object', default to 'subject' |
|
891 |
|
892 * `target_etype`, optional name of an entity type that should be supported |
|
893 at the other end of the relation |
|
894 |
|
895 * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete', |
|
896 default to 'read') which must be granted to the user, else a 0 score will |
|
897 be returned. Give None if you don't want any permission checking. |
|
898 |
|
899 * `strict`, boolean (default to False) telling what to do when the user has |
|
900 not globally the permission for the action (eg the action is not granted |
|
901 to one of the user's groups) |
|
902 |
|
903 - when strict is False, if there are some local role defined for this |
|
904 action (e.g. using rql expressions), then the permission will be |
|
905 considered as granted |
|
906 |
|
907 - when strict is True, then the permission will be actually checked for |
|
908 each entity |
|
909 |
|
910 Setting `strict` to True impacts performance for large result set since |
|
911 you'll then get the :class:`~cubicweb.predicates.EntityPredicate` behaviour |
|
912 while otherwise you get the :class:`~cubicweb.predicates.EClassPredicate`'s |
|
913 one. See those classes documentation for entity lookup / score rules |
|
914 according to the input context. |
|
915 """ |
|
916 |
|
917 def __init__(self, rtype, role='subject', target_etype=None, |
|
918 action='read', strict=False, **kwargs): |
|
919 super(relation_possible, self).__init__(**kwargs) |
|
920 self.rtype = rtype |
|
921 self.role = role |
|
922 self.target_etype = target_etype |
|
923 self.action = action |
|
924 self.strict = strict |
|
925 |
|
926 # hack hack hack |
|
927 def __call__(self, cls, req, **kwargs): |
|
928 # hack hack hack |
|
929 if self.strict: |
|
930 return EntityPredicate.__call__(self, cls, req, **kwargs) |
|
931 return EClassPredicate.__call__(self, cls, req, **kwargs) |
|
932 |
|
933 def score(self, *args): |
|
934 if self.strict: |
|
935 return EntityPredicate.score(self, *args) |
|
936 return EClassPredicate.score(self, *args) |
|
937 |
|
938 def _get_rschema(self, eclass): |
|
939 eschema = eclass.e_schema |
|
940 try: |
|
941 if self.role == 'object': |
|
942 return eschema.objrels[self.rtype] |
|
943 else: |
|
944 return eschema.subjrels[self.rtype] |
|
945 except KeyError: |
|
946 return None |
|
947 |
|
948 def score_class(self, eclass, req): |
|
949 rschema = self._get_rschema(eclass) |
|
950 if rschema is None: |
|
951 return 0 # relation not supported |
|
952 eschema = eclass.e_schema |
|
953 if self.target_etype is not None: |
|
954 try: |
|
955 rdef = rschema.role_rdef(eschema, self.target_etype, self.role) |
|
956 except KeyError: |
|
957 return 0 |
|
958 if self.action and not rdef.may_have_permission(self.action, req): |
|
959 return 0 |
|
960 teschema = req.vreg.schema.eschema(self.target_etype) |
|
961 if not teschema.may_have_permission('read', req): |
|
962 return 0 |
|
963 elif self.action: |
|
964 return rschema.may_have_permission(self.action, req, eschema, self.role) |
|
965 return 1 |
|
966 |
|
967 def score_entity(self, entity): |
|
968 rschema = self._get_rschema(entity) |
|
969 if rschema is None: |
|
970 return 0 # relation not supported |
|
971 if self.action: |
|
972 if self.target_etype is not None: |
|
973 rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role) |
|
974 if self.role == 'subject': |
|
975 if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid): |
|
976 return 0 |
|
977 elif not rschema.has_perm(entity._cw, self.action, toeid=entity.eid): |
|
978 return 0 |
|
979 if self.target_etype is not None: |
|
980 req = entity._cw |
|
981 teschema = req.vreg.schema.eschema(self.target_etype) |
|
982 if not teschema.may_have_permission('read', req): |
|
983 return 0 |
|
984 return 1 |
|
985 |
|
986 |
|
987 class partial_relation_possible(PartialPredicateMixIn, relation_possible): |
|
988 """Same as :class:~`cubicweb.predicates.relation_possible`, but will look for |
|
989 attributes of the selected class to get information which is otherwise |
|
990 expected by the initializer, except for `action` and `strict` which are kept |
|
991 as initializer arguments. |
|
992 |
|
993 This is useful to predefine predicate of an abstract class designed to be |
|
994 customized. |
|
995 """ |
|
996 def __init__(self, action='read', **kwargs): |
|
997 super(partial_relation_possible, self).__init__(None, None, None, |
|
998 action, **kwargs) |
|
999 |
|
1000 def complete(self, cls): |
|
1001 self.rtype = cls.rtype |
|
1002 self.role = role(cls) |
|
1003 self.target_etype = getattr(cls, 'target_etype', None) |
|
1004 |
|
1005 |
|
1006 class has_related_entities(EntityPredicate): |
|
1007 """Return 1 if entity support the specified relation and has some linked |
|
1008 entities by this relation , optionaly filtered according to the specified |
|
1009 target type. |
|
1010 |
|
1011 The relation is specified by the following initializer arguments: |
|
1012 |
|
1013 * `rtype`, the name of the relation |
|
1014 |
|
1015 * `role`, the role of the entity in the relation, either 'subject' or |
|
1016 'object', default to 'subject'. |
|
1017 |
|
1018 * `target_etype`, optional name of an entity type that should be found |
|
1019 at the other end of the relation |
|
1020 |
|
1021 See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity |
|
1022 lookup / score rules according to the input context. |
|
1023 """ |
|
1024 def __init__(self, rtype, role='subject', target_etype=None, **kwargs): |
|
1025 super(has_related_entities, self).__init__(**kwargs) |
|
1026 self.rtype = rtype |
|
1027 self.role = role |
|
1028 self.target_etype = target_etype |
|
1029 |
|
1030 def score_entity(self, entity): |
|
1031 relpossel = relation_possible(self.rtype, self.role, self.target_etype) |
|
1032 if not relpossel.score_class(entity.__class__, entity._cw): |
|
1033 return 0 |
|
1034 rset = entity.related(self.rtype, self.role) |
|
1035 if self.target_etype: |
|
1036 return any(r for r in rset.description if r[0] == self.target_etype) |
|
1037 return rset and 1 or 0 |
|
1038 |
|
1039 |
|
1040 class partial_has_related_entities(PartialPredicateMixIn, has_related_entities): |
|
1041 """Same as :class:~`cubicweb.predicates.has_related_entity`, but will look |
|
1042 for attributes of the selected class to get information which is otherwise |
|
1043 expected by the initializer. |
|
1044 |
|
1045 This is useful to predefine predicate of an abstract class designed to be |
|
1046 customized. |
|
1047 """ |
|
1048 def __init__(self, **kwargs): |
|
1049 super(partial_has_related_entities, self).__init__(None, None, None, |
|
1050 **kwargs) |
|
1051 |
|
1052 def complete(self, cls): |
|
1053 self.rtype = cls.rtype |
|
1054 self.role = role(cls) |
|
1055 self.target_etype = getattr(cls, 'target_etype', None) |
|
1056 |
|
1057 |
|
1058 class has_permission(EntityPredicate): |
|
1059 """Return non-zero score if request's user has the permission to do the |
|
1060 requested action on the entity. `action` is an entity schema action (eg one |
|
1061 of 'read', 'add', 'delete', 'update'). |
|
1062 |
|
1063 Here are entity lookup / scoring rules: |
|
1064 |
|
1065 * if `entity` is specified, check permission is granted for this entity |
|
1066 |
|
1067 * elif `row` is specified, check permission is granted for the entity found |
|
1068 in the specified cell |
|
1069 |
|
1070 * else check permission is granted for each entity found in the column |
|
1071 specified specified by the `col` argument or in column 0 |
|
1072 """ |
|
1073 def __init__(self, action): |
|
1074 self.action = action |
|
1075 |
|
1076 # don't use EntityPredicate.__call__ but this optimized implementation to |
|
1077 # avoid considering each entity when it's not necessary |
|
1078 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
|
1079 if kwargs.get('entity'): |
|
1080 return self.score_entity(kwargs['entity']) |
|
1081 if rset is None: |
|
1082 return 0 |
|
1083 if row is None: |
|
1084 score = 0 |
|
1085 need_local_check = [] |
|
1086 geteschema = req.vreg.schema.eschema |
|
1087 user = req.user |
|
1088 action = self.action |
|
1089 for etype in rset.column_types(0): |
|
1090 if etype in BASE_TYPES: |
|
1091 return 0 |
|
1092 eschema = geteschema(etype) |
|
1093 if not user.matching_groups(eschema.get_groups(action)): |
|
1094 if eschema.has_local_role(action): |
|
1095 # have to ckeck local roles |
|
1096 need_local_check.append(eschema) |
|
1097 continue |
|
1098 else: |
|
1099 # even a local role won't be enough |
|
1100 return 0 |
|
1101 score += 1 |
|
1102 if need_local_check: |
|
1103 # check local role for entities of necessary types |
|
1104 for i, row in enumerate(rset): |
|
1105 if not rset.description[i][col] in need_local_check: |
|
1106 continue |
|
1107 # micro-optimisation instead of calling self.score(req, |
|
1108 # rset, i, col): rset may be large |
|
1109 if not rset.get_entity(i, col).cw_has_perm(action): |
|
1110 return 0 |
|
1111 score += 1 |
|
1112 return score |
|
1113 return self.score(req, rset, row, col) |
|
1114 |
|
1115 def score_entity(self, entity): |
|
1116 if entity.cw_has_perm(self.action): |
|
1117 return 1 |
|
1118 return 0 |
|
1119 |
|
1120 |
|
1121 class has_add_permission(EClassPredicate): |
|
1122 """Return 1 if request's user has the add permission on entity type |
|
1123 specified in the `etype` initializer argument, or according to entity found |
|
1124 in the input content if not specified. |
|
1125 |
|
1126 It also check that then entity type is not a strict subobject (e.g. may only |
|
1127 be used as a composed of another entity). |
|
1128 |
|
1129 See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity |
|
1130 class lookup / score rules according to the input context when `etype` is |
|
1131 not specified. |
|
1132 """ |
|
1133 def __init__(self, etype=None, **kwargs): |
|
1134 super(has_add_permission, self).__init__(**kwargs) |
|
1135 self.etype = etype |
|
1136 |
|
1137 def __call__(self, cls, req, **kwargs): |
|
1138 if self.etype is None: |
|
1139 return super(has_add_permission, self).__call__(cls, req, **kwargs) |
|
1140 return self.score(cls, req, self.etype) |
|
1141 |
|
1142 def score_class(self, eclass, req): |
|
1143 eschema = eclass.e_schema |
|
1144 if eschema.final or eschema.is_subobject(strict=True) \ |
|
1145 or not eschema.has_perm(req, 'add'): |
|
1146 return 0 |
|
1147 return 1 |
|
1148 |
|
1149 |
|
1150 class rql_condition(EntityPredicate): |
|
1151 """Return non-zero score if arbitrary rql specified in `expression` |
|
1152 initializer argument return some results for entity found in the input |
|
1153 context. Returned score is the number of items returned by the rql |
|
1154 condition. |
|
1155 |
|
1156 `expression` is expected to be a string containing an rql expression, which |
|
1157 must use 'X' variable to represent the context entity and may use 'U' to |
|
1158 represent the request's user. |
|
1159 |
|
1160 .. warning:: |
|
1161 If simply testing value of some attribute/relation of context entity (X), |
|
1162 you should rather use the :class:`score_entity` predicate which will |
|
1163 benefit from the ORM's request entities cache. |
|
1164 |
|
1165 See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity |
|
1166 lookup / score rules according to the input context. |
|
1167 """ |
|
1168 def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False): |
|
1169 super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough) |
|
1170 self.user_condition = user_condition |
|
1171 if user_condition: |
|
1172 rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression |
|
1173 elif 'U' in frozenset(split_expression(expression)): |
|
1174 rql = 'Any COUNT(X) WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression |
|
1175 else: |
|
1176 rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression |
|
1177 self.rql = rql |
|
1178 |
|
1179 def __str__(self): |
|
1180 return '%s(%r)' % (self.__class__.__name__, self.rql) |
|
1181 |
|
1182 def __call__(self, cls, req, **kwargs): |
|
1183 if self.user_condition: |
|
1184 try: |
|
1185 return req.execute(self.rql, {'u': req.user.eid})[0][0] |
|
1186 except Unauthorized: |
|
1187 return 0 |
|
1188 else: |
|
1189 return super(rql_condition, self).__call__(cls, req, **kwargs) |
|
1190 |
|
1191 def _score(self, req, eid): |
|
1192 try: |
|
1193 return req.execute(self.rql, {'x': eid, 'u': req.user.eid})[0][0] |
|
1194 except Unauthorized: |
|
1195 return 0 |
|
1196 |
|
1197 def score(self, req, rset, row, col): |
|
1198 return self._score(req, rset[row][col]) |
|
1199 |
|
1200 def score_entity(self, entity): |
|
1201 return self._score(entity._cw, entity.eid) |
|
1202 |
|
1203 |
|
1204 # workflow predicates ########################################################### |
|
1205 |
|
1206 class is_in_state(score_entity): |
|
1207 """Return 1 if entity is in one of the states given as argument list |
|
1208 |
|
1209 You should use this instead of your own :class:`score_entity` predicate to |
|
1210 avoid some gotchas: |
|
1211 |
|
1212 * possible views gives a fake entity with no state |
|
1213 * you must use the latest tr info thru the workflow adapter for repository |
|
1214 side checking of the current state |
|
1215 |
|
1216 In debug mode, this predicate can raise :exc:`ValueError` for unknown states names |
|
1217 (only checked on entities without a custom workflow) |
|
1218 |
|
1219 :rtype: int |
|
1220 """ |
|
1221 def __init__(self, *expected): |
|
1222 assert expected, self |
|
1223 self.expected = frozenset(expected) |
|
1224 def score(entity, expected=self.expected): |
|
1225 adapted = entity.cw_adapt_to('IWorkflowable') |
|
1226 # in debug mode only (time consuming) |
|
1227 if entity._cw.vreg.config.debugmode: |
|
1228 # validation can only be done for generic etype workflow because |
|
1229 # expected transition list could have been changed for a custom |
|
1230 # workflow (for the current entity) |
|
1231 if not entity.custom_workflow: |
|
1232 self._validate(adapted) |
|
1233 return self._score(adapted) |
|
1234 super(is_in_state, self).__init__(score) |
|
1235 |
|
1236 def _score(self, adapted): |
|
1237 trinfo = adapted.latest_trinfo() |
|
1238 if trinfo is None: # entity is probably in it's initial state |
|
1239 statename = adapted.state |
|
1240 else: |
|
1241 statename = trinfo.new_state.name |
|
1242 return statename in self.expected |
|
1243 |
|
1244 def _validate(self, adapted): |
|
1245 wf = adapted.current_workflow |
|
1246 valid = [n.name for n in wf.reverse_state_of] |
|
1247 unknown = sorted(self.expected.difference(valid)) |
|
1248 if unknown: |
|
1249 raise ValueError("%s: unknown state(s): %s" |
|
1250 % (wf.name, ",".join(unknown))) |
|
1251 |
|
1252 def __str__(self): |
|
1253 return '%s(%s)' % (self.__class__.__name__, |
|
1254 ','.join(str(s) for s in self.expected)) |
|
1255 |
|
1256 |
|
1257 def on_fire_transition(etype, tr_name, from_state_name=None): |
|
1258 """Return 1 when entity of the type `etype` is going through transition of |
|
1259 the name `tr_name`. |
|
1260 |
|
1261 If `from_state_name` is specified, this predicate will also check the |
|
1262 incoming state. |
|
1263 |
|
1264 You should use this predicate on 'after_add_entity' hook, since it's actually |
|
1265 looking for addition of `TrInfo` entities. Hence in the hook, `self.entity` |
|
1266 will reference the matching `TrInfo` entity, allowing to get all the |
|
1267 transition details (including the entity to which is applied the transition |
|
1268 but also its original state, transition, destination state, user...). |
|
1269 |
|
1270 See :class:`cubicweb.entities.wfobjs.TrInfo` for more information. |
|
1271 """ |
|
1272 def match_etype_and_transition(trinfo): |
|
1273 # take care trinfo.transition is None when calling change_state |
|
1274 return (trinfo.transition and trinfo.transition.name == tr_name |
|
1275 # is_instance() first two arguments are 'cls' (unused, so giving |
|
1276 # None is fine) and the request/session |
|
1277 and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity)) |
|
1278 |
|
1279 return is_instance('TrInfo') & score_entity(match_etype_and_transition) |
|
1280 |
|
1281 |
|
1282 class match_transition(ExpectedValuePredicate): |
|
1283 """Return 1 if `transition` argument is found in the input context which has |
|
1284 a `.name` attribute matching one of the expected names given to the |
|
1285 initializer. |
|
1286 |
|
1287 This predicate is expected to be used to customise the status change form in |
|
1288 the web ui. |
|
1289 """ |
|
1290 def __call__(self, cls, req, transition=None, **kwargs): |
|
1291 # XXX check this is a transition that apply to the object? |
|
1292 if transition is None: |
|
1293 treid = req.form.get('treid', None) |
|
1294 if treid: |
|
1295 transition = req.entity_from_eid(treid) |
|
1296 if transition is not None and getattr(transition, 'name', None) in self.expected: |
|
1297 return 1 |
|
1298 return 0 |
|
1299 |
|
1300 |
|
1301 # logged user predicates ######################################################## |
|
1302 |
|
1303 @objectify_predicate |
|
1304 def no_cnx(cls, req, **kwargs): |
|
1305 """Return 1 if the web session has no connection set. This occurs when |
|
1306 anonymous access is not allowed and user isn't authenticated. |
|
1307 |
|
1308 May only be used on the web side, not on the data repository side. |
|
1309 """ |
|
1310 if not req.cnx: |
|
1311 return 1 |
|
1312 return 0 |
|
1313 |
|
1314 @objectify_predicate |
|
1315 def authenticated_user(cls, req, **kwargs): |
|
1316 """Return 1 if the user is authenticated (e.g. not the anonymous user). |
|
1317 |
|
1318 May only be used on the web side, not on the data repository side. |
|
1319 """ |
|
1320 if req.session.anonymous_session: |
|
1321 return 0 |
|
1322 return 1 |
|
1323 |
|
1324 |
|
1325 # XXX == ~ authenticated_user() |
|
1326 def anonymous_user(): |
|
1327 """Return 1 if the user is not authenticated (e.g. is the anonymous user). |
|
1328 |
|
1329 May only be used on the web side, not on the data repository side. |
|
1330 """ |
|
1331 return ~ authenticated_user() |
|
1332 |
|
1333 class match_user_groups(ExpectedValuePredicate): |
|
1334 """Return a non-zero score if request's user is in at least one of the |
|
1335 groups given as initializer argument. Returned score is the number of groups |
|
1336 in which the user is. |
|
1337 |
|
1338 If the special 'owners' group is given and `rset` is specified in the input |
|
1339 context: |
|
1340 |
|
1341 * if `row` is specified check the entity at the given `row`/`col` (default |
|
1342 to 0) is owned by the user |
|
1343 |
|
1344 * else check all entities in `col` (default to 0) are owned by the user |
|
1345 """ |
|
1346 |
|
1347 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
|
1348 if not getattr(req, 'cnx', True): # default to True for repo session instances |
|
1349 return 0 |
|
1350 user = req.user |
|
1351 if user is None: |
|
1352 return int('guests' in self.expected) |
|
1353 score = user.matching_groups(self.expected) |
|
1354 if not score and 'owners' in self.expected and rset: |
|
1355 if row is not None: |
|
1356 if not user.owns(rset[row][col]): |
|
1357 return 0 |
|
1358 score = 1 |
|
1359 else: |
|
1360 score = all(user.owns(r[col]) for r in rset) |
|
1361 return score |
|
1362 |
|
1363 # Web request predicates ######################################################## |
|
1364 |
|
1365 # XXX deprecate |
|
1366 @objectify_predicate |
|
1367 def primary_view(cls, req, view=None, **kwargs): |
|
1368 """Return 1 if: |
|
1369 |
|
1370 * *no view is specified* in the input context |
|
1371 |
|
1372 * a view is specified and its `.is_primary()` method return True |
|
1373 |
|
1374 This predicate is usually used by contextual components that only want to |
|
1375 appears for the primary view of an entity. |
|
1376 """ |
|
1377 if view is not None and not view.is_primary(): |
|
1378 return 0 |
|
1379 return 1 |
|
1380 |
|
1381 |
|
1382 @objectify_predicate |
|
1383 def contextual(cls, req, view=None, **kwargs): |
|
1384 """Return 1 if view's contextual property is true""" |
|
1385 if view is not None and view.contextual: |
|
1386 return 1 |
|
1387 return 0 |
|
1388 |
|
1389 |
|
1390 class match_view(ExpectedValuePredicate): |
|
1391 """Return 1 if a view is specified an as its registry id is in one of the |
|
1392 expected view id given to the initializer. |
|
1393 """ |
|
1394 def __call__(self, cls, req, view=None, **kwargs): |
|
1395 if view is None or not view.__regid__ in self.expected: |
|
1396 return 0 |
|
1397 return 1 |
|
1398 |
|
1399 |
|
1400 class match_context(ExpectedValuePredicate): |
|
1401 |
|
1402 def __call__(self, cls, req, context=None, **kwargs): |
|
1403 if not context in self.expected: |
|
1404 return 0 |
|
1405 return 1 |
|
1406 |
|
1407 |
|
1408 # XXX deprecate |
|
1409 @objectify_predicate |
|
1410 def match_context_prop(cls, req, context=None, **kwargs): |
|
1411 """Return 1 if: |
|
1412 |
|
1413 * no `context` is specified in input context (take care to confusion, here |
|
1414 `context` refers to a string given as an argument to the input context...) |
|
1415 |
|
1416 * specified `context` is matching the context property value for the |
|
1417 appobject using this predicate |
|
1418 |
|
1419 * the appobject's context property value is None |
|
1420 |
|
1421 This predicate is usually used by contextual components that want to appears |
|
1422 in a configurable place. |
|
1423 """ |
|
1424 if context is None: |
|
1425 return 1 |
|
1426 propval = req.property_value('%s.%s.context' % (cls.__registry__, |
|
1427 cls.__regid__)) |
|
1428 if propval and context != propval: |
|
1429 return 0 |
|
1430 return 1 |
|
1431 |
|
1432 |
|
1433 class match_search_state(ExpectedValuePredicate): |
|
1434 """Return 1 if the current request search state is in one of the expected |
|
1435 states given to the initializer. |
|
1436 |
|
1437 Known search states are either 'normal' or 'linksearch' (eg searching for an |
|
1438 object to create a relation with another). |
|
1439 |
|
1440 This predicate is usually used by action that want to appears or not according |
|
1441 to the ui search state. |
|
1442 """ |
|
1443 |
|
1444 def __call__(self, cls, req, **kwargs): |
|
1445 try: |
|
1446 if not req.search_state[0] in self.expected: |
|
1447 return 0 |
|
1448 except AttributeError: |
|
1449 return 1 # class doesn't care about search state, accept it |
|
1450 return 1 |
|
1451 |
|
1452 |
|
1453 class match_form_params(ExpectedValuePredicate): |
|
1454 """Return non-zero score if parameter names specified as initializer |
|
1455 arguments are specified in request's form parameters. |
|
1456 |
|
1457 Return a score corresponding to the number of expected parameters. |
|
1458 |
|
1459 When multiple parameters are expected, all of them should be found in |
|
1460 the input context unless `mode` keyword argument is given to 'any', |
|
1461 in which case a single matching parameter is enough. |
|
1462 """ |
|
1463 |
|
1464 def _values_set(self, cls, req, **kwargs): |
|
1465 return frozenset(req.form) |
|
1466 |
|
1467 |
|
1468 class match_edited_type(ExpectedValuePredicate): |
|
1469 """return non-zero if main edited entity type is the one specified as |
|
1470 initializer argument, or is among initializer arguments if `mode` == 'any'. |
|
1471 """ |
|
1472 |
|
1473 def _values_set(self, cls, req, **kwargs): |
|
1474 try: |
|
1475 return frozenset((req.form['__type:%s' % req.form['__maineid']],)) |
|
1476 except KeyError: |
|
1477 return frozenset() |
|
1478 |
|
1479 |
|
1480 class match_form_id(ExpectedValuePredicate): |
|
1481 """return non-zero if request form identifier is the one specified as |
|
1482 initializer argument, or is among initializer arguments if `mode` == 'any'. |
|
1483 """ |
|
1484 |
|
1485 def _values_set(self, cls, req, **kwargs): |
|
1486 try: |
|
1487 return frozenset((req.form['__form_id'],)) |
|
1488 except KeyError: |
|
1489 return frozenset() |
|
1490 |
|
1491 |
|
1492 class specified_etype_implements(is_instance): |
|
1493 """Return non-zero score if the entity type specified by an 'etype' key |
|
1494 searched in (by priority) input context kwargs and request form parameters |
|
1495 match a known entity type (case insensitivly), and it's associated entity |
|
1496 class is of one of the type(s) given to the initializer. If multiple |
|
1497 arguments are given, matching one of them is enough. |
|
1498 |
|
1499 .. note:: as with :class:`~cubicweb.predicates.is_instance`, entity types |
|
1500 should be given as string and the score will reflect class |
|
1501 proximity so the most specific object will be selected. |
|
1502 |
|
1503 This predicate is usually used by views holding entity creation forms (since |
|
1504 we've no result set to work on). |
|
1505 """ |
|
1506 |
|
1507 def __call__(self, cls, req, **kwargs): |
|
1508 try: |
|
1509 etype = kwargs['etype'] |
|
1510 except KeyError: |
|
1511 try: |
|
1512 etype = req.form['etype'] |
|
1513 except KeyError: |
|
1514 return 0 |
|
1515 else: |
|
1516 # only check this is a known type if etype comes from req.form, |
|
1517 # else we want the error to propagate |
|
1518 try: |
|
1519 etype = req.vreg.case_insensitive_etypes[etype.lower()] |
|
1520 req.form['etype'] = etype |
|
1521 except KeyError: |
|
1522 return 0 |
|
1523 score = self.score_class(req.vreg['etypes'].etype_class(etype), req) |
|
1524 if score: |
|
1525 eschema = req.vreg.schema.eschema(etype) |
|
1526 if eschema.has_local_role('add') or eschema.has_perm(req, 'add'): |
|
1527 return score |
|
1528 return 0 |
|
1529 |
|
1530 |
|
1531 class attribute_edited(EntityPredicate): |
|
1532 """Scores if the specified attribute has been edited This is useful for |
|
1533 selection of forms by the edit controller. |
|
1534 |
|
1535 The initial use case is on a form, in conjunction with match_transition, |
|
1536 which will not score at edit time:: |
|
1537 |
|
1538 is_instance('Version') & (match_transition('ready') | |
|
1539 attribute_edited('publication_date')) |
|
1540 """ |
|
1541 def __init__(self, attribute, once_is_enough=None, mode='all'): |
|
1542 super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough) |
|
1543 self._attribute = attribute |
|
1544 |
|
1545 def score_entity(self, entity): |
|
1546 return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form |
|
1547 |
|
1548 |
|
1549 # Other predicates ############################################################## |
|
1550 |
|
1551 class match_exception(ExpectedValuePredicate): |
|
1552 """Return 1 if exception given as `exc` in the input context is an instance |
|
1553 of one of the class given on instanciation of this predicate. |
|
1554 """ |
|
1555 def __init__(self, *expected): |
|
1556 assert expected, self |
|
1557 # we want a tuple, not a set as done in the parent class |
|
1558 self.expected = expected |
|
1559 |
|
1560 def __call__(self, cls, req, exc=None, **kwargs): |
|
1561 if exc is not None and isinstance(exc, self.expected): |
|
1562 return 1 |
|
1563 return 0 |
|
1564 |
|
1565 |
|
1566 @objectify_predicate |
|
1567 def debug_mode(cls, req, rset=None, **kwargs): |
|
1568 """Return 1 if running in debug mode.""" |
|
1569 return req.vreg.config.debugmode and 1 or 0 |