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