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