|
1 """This file contains some basic selectors required by application objects. |
|
2 |
|
3 A selector is responsible to score how well an object may be used with a |
|
4 given context by returning a score. |
|
5 |
|
6 In CubicWeb Usually the context consists for a request object, a result set |
|
7 or None, a specific row/col in the result set, etc... |
|
8 |
|
9 |
|
10 If you have trouble with selectors, especially if the objet (typically |
|
11 a view or a component) you want to use is not selected and you want to |
|
12 know which one(s) of its selectors fail (e.g. returns 0), you can use |
|
13 `traced_selection` or even direclty `TRACED_OIDS`. |
|
14 |
|
15 `TRACED_OIDS` is a tuple of traced object ids. The special value |
|
16 'all' may be used to log selectors for all objects. |
|
17 |
|
18 For instance, say that the following code yields a `NoSelectableObject` |
|
19 exception:: |
|
20 |
|
21 self.view('calendar', myrset) |
|
22 |
|
23 You can log the selectors involved for *calendar* by replacing the line |
|
24 above by:: |
|
25 |
|
26 # in Python2.5 |
|
27 from cubicweb.selectors import traced_selection |
|
28 with traced_selection(): |
|
29 self.view('calendar', myrset) |
|
30 |
|
31 # in Python2.4 |
|
32 from cubicweb import selectors |
|
33 selectors.TRACED_OIDS = ('calendar',) |
|
34 self.view('calendar', myrset) |
|
35 selectors.TRACED_OIDS = () |
|
36 |
|
37 |
|
38 :organization: Logilab |
|
39 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
40 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
41 """ |
|
42 |
|
43 __docformat__ = "restructuredtext en" |
|
44 |
|
45 import logging |
|
46 from warnings import warn |
|
47 |
|
48 from logilab.common.compat import all |
|
49 from logilab.common.deprecation import deprecated_function |
|
50 from logilab.common.interface import implements as implements_iface |
|
51 |
|
52 from yams import BASE_TYPES |
|
53 |
|
54 from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity, |
|
55 role, typed_eid) |
|
56 from cubicweb.vregistry import (NoSelectableObject, Selector, |
|
57 chainall, objectify_selector) |
|
58 from cubicweb.cwconfig import CubicWebConfiguration |
|
59 from cubicweb.schema import split_expression |
|
60 |
|
61 # helpers for debugging selectors |
|
62 SELECTOR_LOGGER = logging.getLogger('cubicweb.selectors') |
|
63 TRACED_OIDS = () |
|
64 |
|
65 def lltrace(selector): |
|
66 # don't wrap selectors if not in development mode |
|
67 if CubicWebConfiguration.mode == 'installed': |
|
68 return selector |
|
69 def traced(cls, *args, **kwargs): |
|
70 # /!\ lltrace decorates pure function or __call__ method, this |
|
71 # means argument order may be different |
|
72 if isinstance(cls, Selector): |
|
73 selname = str(cls) |
|
74 vobj = args[0] |
|
75 else: |
|
76 selname = selector.__name__ |
|
77 vobj = cls |
|
78 oid = vobj.id |
|
79 ret = selector(cls, *args, **kwargs) |
|
80 if TRACED_OIDS == 'all' or oid in TRACED_OIDS: |
|
81 #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls) |
|
82 print 'selector %s returned %s for %s' % (selname, ret, vobj) |
|
83 return ret |
|
84 traced.__name__ = selector.__name__ |
|
85 return traced |
|
86 |
|
87 class traced_selection(object): |
|
88 """selector debugging helper. |
|
89 |
|
90 Typical usage is : |
|
91 |
|
92 >>> with traced_selection(): |
|
93 ... # some code in which you want to debug selectors |
|
94 ... # for all objects |
|
95 |
|
96 or |
|
97 |
|
98 >>> with traced_selection( ('oid1', 'oid2') ): |
|
99 ... # some code in which you want to debug selectors |
|
100 ... # for objects with id 'oid1' and 'oid2' |
|
101 |
|
102 """ |
|
103 def __init__(self, traced='all'): |
|
104 self.traced = traced |
|
105 |
|
106 def __enter__(self): |
|
107 global TRACED_OIDS |
|
108 TRACED_OIDS = self.traced |
|
109 |
|
110 def __exit__(self, exctype, exc, traceback): |
|
111 global TRACED_OIDS |
|
112 TRACED_OIDS = () |
|
113 return traceback is None |
|
114 |
|
115 |
|
116 def score_interface(cls_or_inst, cls, iface): |
|
117 """Return true if the give object (maybe an instance or class) implements |
|
118 the interface. |
|
119 """ |
|
120 if getattr(iface, '__registry__', None) == 'etypes': |
|
121 # adjust score if the interface is an entity class |
|
122 parents = cls_or_inst.parent_classes() |
|
123 if iface is cls: |
|
124 return len(parents) + 4 |
|
125 if iface is parents[-1]: # Any |
|
126 return 1 |
|
127 for index, basecls in enumerate(reversed(parents[:-1])): |
|
128 if iface is basecls: |
|
129 return index + 3 |
|
130 return 0 |
|
131 if implements_iface(cls_or_inst, iface): |
|
132 # implenting an interface takes precedence other special Any interface |
|
133 return 2 |
|
134 return 0 |
|
135 |
|
136 |
|
137 # abstract selectors ########################################################## |
|
138 |
|
139 class PartialSelectorMixIn(object): |
|
140 """convenience mix-in for selectors that will look into the containing |
|
141 class to find missing information. |
|
142 |
|
143 cf. `cubicweb.web.action.LinkToEntityAction` for instance |
|
144 """ |
|
145 def __call__(self, cls, *args, **kwargs): |
|
146 self.complete(cls) |
|
147 return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs) |
|
148 |
|
149 |
|
150 class ImplementsMixIn(object): |
|
151 """mix-in class for selectors checking implemented interfaces of something |
|
152 """ |
|
153 def __init__(self, *expected_ifaces): |
|
154 super(ImplementsMixIn, self).__init__() |
|
155 self.expected_ifaces = expected_ifaces |
|
156 |
|
157 def __str__(self): |
|
158 return '%s(%s)' % (self.__class__.__name__, |
|
159 ','.join(str(s) for s in self.expected_ifaces)) |
|
160 |
|
161 def score_interfaces(self, cls_or_inst, cls): |
|
162 score = 0 |
|
163 vreg, eschema = cls_or_inst.vreg, cls_or_inst.e_schema |
|
164 for iface in self.expected_ifaces: |
|
165 if isinstance(iface, basestring): |
|
166 # entity type |
|
167 try: |
|
168 iface = vreg.etype_class(iface) |
|
169 except KeyError: |
|
170 continue # entity type not in the schema |
|
171 score += score_interface(cls_or_inst, cls, iface) |
|
172 return score |
|
173 |
|
174 |
|
175 class EClassSelector(Selector): |
|
176 """abstract class for selectors working on the entity classes of the result |
|
177 set. Its __call__ method has the following behaviour: |
|
178 |
|
179 * if row is specified, return the score returned by the score_class method |
|
180 called with the entity class found in the specified cell |
|
181 * else return the sum of score returned by the score_class method for each |
|
182 entity type found in the specified column, unless: |
|
183 - `once_is_enough` is True, in which case the first non-zero score is |
|
184 returned |
|
185 - `once_is_enough` is False, in which case if score_class return 0, 0 is |
|
186 returned |
|
187 """ |
|
188 def __init__(self, once_is_enough=False): |
|
189 self.once_is_enough = once_is_enough |
|
190 |
|
191 @lltrace |
|
192 def __call__(self, cls, req, rset, row=None, col=0, **kwargs): |
|
193 if not rset: |
|
194 return 0 |
|
195 score = 0 |
|
196 if row is None: |
|
197 for etype in rset.column_types(col): |
|
198 if etype is None: # outer join |
|
199 continue |
|
200 escore = self.score(cls, req, etype) |
|
201 if not escore and not self.once_is_enough: |
|
202 return 0 |
|
203 elif self.once_is_enough: |
|
204 return escore |
|
205 score += escore |
|
206 else: |
|
207 etype = rset.description[row][col] |
|
208 if etype is not None: |
|
209 score = self.score(cls, req, etype) |
|
210 return score |
|
211 |
|
212 def score(self, cls, req, etype): |
|
213 if etype in BASE_TYPES: |
|
214 return 0 |
|
215 return self.score_class(cls.vreg.etype_class(etype), req) |
|
216 |
|
217 def score_class(self, eclass, req): |
|
218 raise NotImplementedError() |
|
219 |
|
220 |
|
221 class EntitySelector(EClassSelector): |
|
222 """abstract class for selectors working on the entity instances of the |
|
223 result set. Its __call__ method has the following behaviour: |
|
224 |
|
225 * if 'entity' find in kwargs, return the score returned by the score_entity |
|
226 method for this entity |
|
227 * if row is specified, return the score returned by the score_entity method |
|
228 called with the entity instance found in the specified cell |
|
229 * else return the sum of score returned by the score_entity method for each |
|
230 entity found in the specified column, unless: |
|
231 - `once_is_enough` is True, in which case the first non-zero score is |
|
232 returned |
|
233 - `once_is_enough` is False, in which case if score_class return 0, 0 is |
|
234 returned |
|
235 |
|
236 note: None values (resulting from some outer join in the query) are not |
|
237 considered. |
|
238 """ |
|
239 |
|
240 @lltrace |
|
241 def __call__(self, cls, req, rset, row=None, col=0, **kwargs): |
|
242 if not rset and not kwargs.get('entity'): |
|
243 return 0 |
|
244 score = 0 |
|
245 if kwargs.get('entity'): |
|
246 score = self.score_entity(kwargs['entity']) |
|
247 elif row is None: |
|
248 for row, rowvalue in enumerate(rset.rows): |
|
249 if rowvalue[col] is None: # outer join |
|
250 continue |
|
251 escore = self.score(req, rset, row, col) |
|
252 if not escore and not self.once_is_enough: |
|
253 return 0 |
|
254 elif self.once_is_enough: |
|
255 return escore |
|
256 score += escore |
|
257 else: |
|
258 etype = rset.description[row][col] |
|
259 if etype is not None: # outer join |
|
260 score = self.score(req, rset, row, col) |
|
261 return score |
|
262 |
|
263 def score(self, req, rset, row, col): |
|
264 try: |
|
265 return self.score_entity(rset.get_entity(row, col)) |
|
266 except NotAnEntity: |
|
267 return 0 |
|
268 |
|
269 def score_entity(self, entity): |
|
270 raise NotImplementedError() |
|
271 |
|
272 |
|
273 # very basic selectors ######################################################## |
|
274 |
|
275 class yes(Selector): |
|
276 """return arbitrary score""" |
|
277 def __init__(self, score=1): |
|
278 self.score = score |
|
279 def __call__(self, *args, **kwargs): |
|
280 return self.score |
|
281 |
|
282 @objectify_selector |
|
283 @lltrace |
|
284 def none_rset(cls, req, rset, *args, **kwargs): |
|
285 """accept no result set (e.g. given rset is None)""" |
|
286 if rset is None: |
|
287 return 1 |
|
288 return 0 |
|
289 |
|
290 @objectify_selector |
|
291 @lltrace |
|
292 def any_rset(cls, req, rset, *args, **kwargs): |
|
293 """accept result set, whatever the number of result it contains""" |
|
294 if rset is not None: |
|
295 return 1 |
|
296 return 0 |
|
297 |
|
298 @objectify_selector |
|
299 @lltrace |
|
300 def nonempty_rset(cls, req, rset, *args, **kwargs): |
|
301 """accept any non empty result set""" |
|
302 if rset is not None and rset.rowcount: |
|
303 return 1 |
|
304 return 0 |
|
305 |
|
306 @objectify_selector |
|
307 @lltrace |
|
308 def empty_rset(cls, req, rset, *args, **kwargs): |
|
309 """accept empty result set""" |
|
310 if rset is not None and rset.rowcount == 0: |
|
311 return 1 |
|
312 return 0 |
|
313 |
|
314 @objectify_selector |
|
315 @lltrace |
|
316 def one_line_rset(cls, req, rset, row=None, *args, **kwargs): |
|
317 """if row is specified, accept result set with a single line of result, |
|
318 else accepts anyway |
|
319 """ |
|
320 if rset is not None and (row is not None or rset.rowcount == 1): |
|
321 return 1 |
|
322 return 0 |
|
323 |
|
324 @objectify_selector |
|
325 @lltrace |
|
326 def two_lines_rset(cls, req, rset, *args, **kwargs): |
|
327 """accept result set with *at least* two lines of result""" |
|
328 if rset is not None and rset.rowcount > 1: |
|
329 return 1 |
|
330 return 0 |
|
331 |
|
332 @objectify_selector |
|
333 @lltrace |
|
334 def two_cols_rset(cls, req, rset, *args, **kwargs): |
|
335 """accept result set with at least one line and two columns of result""" |
|
336 if rset is not None and rset.rowcount and len(rset.rows[0]) > 1: |
|
337 return 1 |
|
338 return 0 |
|
339 |
|
340 @objectify_selector |
|
341 @lltrace |
|
342 def paginated_rset(cls, req, rset, *args, **kwargs): |
|
343 """accept result set with more lines than the page size. |
|
344 |
|
345 Page size is searched in (respecting order): |
|
346 * a page_size argument |
|
347 * a page_size form parameters |
|
348 * the navigation.page-size property |
|
349 """ |
|
350 page_size = kwargs.get('page_size') |
|
351 if page_size is None: |
|
352 page_size = req.form.get('page_size') |
|
353 if page_size is None: |
|
354 page_size = req.property_value('navigation.page-size') |
|
355 else: |
|
356 page_size = int(page_size) |
|
357 if rset is None or rset.rowcount <= page_size: |
|
358 return 0 |
|
359 return 1 |
|
360 |
|
361 @objectify_selector |
|
362 @lltrace |
|
363 def sorted_rset(cls, req, rset, row=None, col=0, **kwargs): |
|
364 """accept sorted result set""" |
|
365 rqlst = rset.syntax_tree() |
|
366 if len(rqlst.children) > 1 or not rqlst.children[0].orderby: |
|
367 return 0 |
|
368 return 2 |
|
369 |
|
370 @objectify_selector |
|
371 @lltrace |
|
372 def one_etype_rset(cls, req, rset, row=None, col=0, *args, **kwargs): |
|
373 """accept result set where entities in the specified column (or 0) are all |
|
374 of the same type |
|
375 """ |
|
376 if rset is None: |
|
377 return 0 |
|
378 if len(rset.column_types(col)) != 1: |
|
379 return 0 |
|
380 return 1 |
|
381 |
|
382 @objectify_selector |
|
383 @lltrace |
|
384 def two_etypes_rset(cls, req, rset, row=None, col=0, **kwargs): |
|
385 """accept result set where entities in the specified column (or 0) are not |
|
386 of the same type |
|
387 """ |
|
388 if rset: |
|
389 etypes = rset.column_types(col) |
|
390 if len(etypes) > 1: |
|
391 return 1 |
|
392 return 0 |
|
393 |
|
394 class non_final_entity(EClassSelector): |
|
395 """accept if entity type found in the result set is non final. |
|
396 |
|
397 See `EClassSelector` documentation for behaviour when row is not specified. |
|
398 """ |
|
399 def score(self, cls, req, etype): |
|
400 if etype in BASE_TYPES: |
|
401 return 0 |
|
402 return 1 |
|
403 |
|
404 @objectify_selector |
|
405 @lltrace |
|
406 def authenticated_user(cls, req, *args, **kwargs): |
|
407 """accept if user is authenticated""" |
|
408 if req.cnx.anonymous_connection: |
|
409 return 0 |
|
410 return 1 |
|
411 |
|
412 def anonymous_user(): |
|
413 return ~ authenticated_user() |
|
414 |
|
415 @objectify_selector |
|
416 @lltrace |
|
417 def primary_view(cls, req, rset, row=None, col=0, view=None, **kwargs): |
|
418 """accept if view given as named argument is a primary view, or if no view |
|
419 is given |
|
420 """ |
|
421 if view is not None and not view.is_primary(): |
|
422 return 0 |
|
423 return 1 |
|
424 |
|
425 @objectify_selector |
|
426 @lltrace |
|
427 def match_context_prop(cls, req, rset, row=None, col=0, context=None, |
|
428 **kwargs): |
|
429 """accept if: |
|
430 * no context given |
|
431 * context (`basestring`) is matching the context property value for the |
|
432 given cls |
|
433 """ |
|
434 propval = req.property_value('%s.%s.context' % (cls.__registry__, cls.id)) |
|
435 if not propval: |
|
436 propval = cls.context |
|
437 if context is not None and propval and context != propval: |
|
438 return 0 |
|
439 return 1 |
|
440 |
|
441 |
|
442 class match_search_state(Selector): |
|
443 """accept if the current request search state is in one of the expected |
|
444 states given to the initializer |
|
445 |
|
446 :param expected: either 'normal' or 'linksearch' (eg searching for an |
|
447 object to create a relation with another) |
|
448 """ |
|
449 def __init__(self, *expected): |
|
450 assert expected, self |
|
451 self.expected = frozenset(expected) |
|
452 |
|
453 def __str__(self): |
|
454 return '%s(%s)' % (self.__class__.__name__, |
|
455 ','.join(sorted(str(s) for s in self.expected))) |
|
456 |
|
457 @lltrace |
|
458 def __call__(self, cls, req, rset, row=None, col=0, **kwargs): |
|
459 try: |
|
460 if not req.search_state[0] in self.expected: |
|
461 return 0 |
|
462 except AttributeError: |
|
463 return 1 # class doesn't care about search state, accept it |
|
464 return 1 |
|
465 |
|
466 |
|
467 class match_form_params(match_search_state): |
|
468 """accept if parameters specified as initializer arguments are specified |
|
469 in request's form parameters |
|
470 |
|
471 :param *expected: parameters (eg `basestring`) which are expected to be |
|
472 found in request's form parameters |
|
473 """ |
|
474 |
|
475 @lltrace |
|
476 def __call__(self, cls, req, *args, **kwargs): |
|
477 score = 0 |
|
478 for param in self.expected: |
|
479 val = req.form.get(param) |
|
480 if not val: |
|
481 return 0 |
|
482 score += 1 |
|
483 return len(self.expected) |
|
484 |
|
485 |
|
486 class match_kwargs(match_search_state): |
|
487 """accept if parameters specified as initializer arguments are specified |
|
488 in named arguments given to the selector |
|
489 |
|
490 :param *expected: parameters (eg `basestring`) which are expected to be |
|
491 found in named arguments (kwargs) |
|
492 """ |
|
493 |
|
494 @lltrace |
|
495 def __call__(self, cls, req, *args, **kwargs): |
|
496 for arg in self.expected: |
|
497 if not arg in kwargs: |
|
498 return 0 |
|
499 return len(self.expected) |
|
500 |
|
501 |
|
502 class match_user_groups(match_search_state): |
|
503 """accept if logged users is in at least one of the given groups. Returned |
|
504 score is the number of groups in which the user is. |
|
505 |
|
506 If the special 'owners' group is given: |
|
507 * if row is specified check the entity at the given row/col is owned by the |
|
508 logged user |
|
509 * if row is not specified check all entities in col are owned by the logged |
|
510 user |
|
511 |
|
512 :param *required_groups: name of groups (`basestring`) in which the logged |
|
513 user should be |
|
514 """ |
|
515 |
|
516 @lltrace |
|
517 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
|
518 user = req.user |
|
519 if user is None: |
|
520 return int('guests' in self.expected) |
|
521 score = user.matching_groups(self.expected) |
|
522 if not score and 'owners' in self.expected and rset: |
|
523 if row is not None: |
|
524 if not user.owns(rset[row][col]): |
|
525 return 0 |
|
526 score = 1 |
|
527 else: |
|
528 score = all(user.owns(r[col]) for r in rset) |
|
529 return score |
|
530 |
|
531 |
|
532 class match_transition(match_search_state): |
|
533 @lltrace |
|
534 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
|
535 try: |
|
536 trname = req.execute('Any XN WHERE X is Transition, X eid %(x)s, X name XN', |
|
537 {'x': typed_eid(req.form['treid'])})[0][0] |
|
538 except (KeyError, IndexError): |
|
539 return 0 |
|
540 # XXX check this is a transition that apply to the object? |
|
541 if not trname in self.expected: |
|
542 return 0 |
|
543 return 1 |
|
544 |
|
545 |
|
546 class match_view(match_search_state): |
|
547 """accept if the current view is in one of the expected vid given to the |
|
548 initializer |
|
549 """ |
|
550 @lltrace |
|
551 def __call__(self, cls, req, rset, row=None, col=0, view=None, **kwargs): |
|
552 if view is None or not view.id in self.expected: |
|
553 return 0 |
|
554 return 1 |
|
555 |
|
556 |
|
557 class appobject_selectable(Selector): |
|
558 """accept with another appobject is selectable using selector's input |
|
559 context. |
|
560 |
|
561 :param registry: a registry name (`basestring`) |
|
562 :param oid: an object identifier (`basestring`) |
|
563 """ |
|
564 def __init__(self, registry, oid): |
|
565 self.registry = registry |
|
566 self.oid = oid |
|
567 |
|
568 def __call__(self, cls, req, rset, *args, **kwargs): |
|
569 try: |
|
570 cls.vreg.select_object(self.registry, self.oid, req, rset, *args, **kwargs) |
|
571 return 1 |
|
572 except NoSelectableObject: |
|
573 return 0 |
|
574 |
|
575 |
|
576 # not so basic selectors ###################################################### |
|
577 |
|
578 class implements(ImplementsMixIn, EClassSelector): |
|
579 """accept if entity classes found in the result set implements at least one |
|
580 of the interfaces given as argument. Returned score is the number of |
|
581 implemented interfaces. |
|
582 |
|
583 See `EClassSelector` documentation for behaviour when row is not specified. |
|
584 |
|
585 :param *expected_ifaces: expected interfaces. An interface may be a class |
|
586 or an entity type (e.g. `basestring`) in which case |
|
587 the associated class will be searched in the |
|
588 registry (at selection time) |
|
589 |
|
590 note: when interface is an entity class, the score will reflect class |
|
591 proximity so the most specific object'll be selected |
|
592 """ |
|
593 def score_class(self, eclass, req): |
|
594 return self.score_interfaces(eclass, eclass) |
|
595 |
|
596 |
|
597 class specified_etype_implements(implements): |
|
598 """accept if entity class specified using an 'etype' parameters in name |
|
599 argument or request form implements at least one of the interfaces given as |
|
600 argument. Returned score is the number of implemented interfaces. |
|
601 |
|
602 :param *expected_ifaces: expected interfaces. An interface may be a class |
|
603 or an entity type (e.g. `basestring`) in which case |
|
604 the associated class will be searched in the |
|
605 registry (at selection time) |
|
606 |
|
607 note: when interface is an entity class, the score will reflect class |
|
608 proximity so the most specific object'll be selected |
|
609 """ |
|
610 |
|
611 @lltrace |
|
612 def __call__(self, cls, req, *args, **kwargs): |
|
613 try: |
|
614 etype = req.form['etype'] |
|
615 except KeyError: |
|
616 try: |
|
617 etype = kwargs['etype'] |
|
618 except KeyError: |
|
619 return 0 |
|
620 return self.score_class(cls.vreg.etype_class(etype), req) |
|
621 |
|
622 |
|
623 class entity_implements(ImplementsMixIn, EntitySelector): |
|
624 """accept if entity instances found in the result set implements at least one |
|
625 of the interfaces given as argument. Returned score is the number of |
|
626 implemented interfaces. |
|
627 |
|
628 See `EntitySelector` documentation for behaviour when row is not specified. |
|
629 |
|
630 :param *expected_ifaces: expected interfaces. An interface may be a class |
|
631 or an entity type (e.g. `basestring`) in which case |
|
632 the associated class will be searched in the |
|
633 registry (at selection time) |
|
634 |
|
635 note: when interface is an entity class, the score will reflect class |
|
636 proximity so the most specific object'll be selected |
|
637 """ |
|
638 def score_entity(self, entity): |
|
639 return self.score_interfaces(entity, entity.__class__) |
|
640 |
|
641 |
|
642 class relation_possible(EClassSelector): |
|
643 """accept if entity class found in the result set support the relation. |
|
644 |
|
645 See `EClassSelector` documentation for behaviour when row is not specified. |
|
646 |
|
647 :param rtype: a relation type (`basestring`) |
|
648 :param role: the role of the result set entity in the relation. 'subject' or |
|
649 'object', default to 'subject'. |
|
650 :param target_type: if specified, check the relation's end may be of this |
|
651 target type (`basestring`) |
|
652 :param action: a relation schema action (one of 'read', 'add', 'delete') |
|
653 which must be granted to the logged user, else a 0 score will |
|
654 be returned |
|
655 """ |
|
656 def __init__(self, rtype, role='subject', target_etype=None, |
|
657 action='read', once_is_enough=False): |
|
658 super(relation_possible, self).__init__(once_is_enough) |
|
659 self.rtype = rtype |
|
660 self.role = role |
|
661 self.target_etype = target_etype |
|
662 self.action = action |
|
663 |
|
664 @lltrace |
|
665 def __call__(self, cls, req, *args, **kwargs): |
|
666 rschema = cls.schema.rschema(self.rtype) |
|
667 if not (rschema.has_perm(req, self.action) |
|
668 or rschema.has_local_role(self.action)): |
|
669 return 0 |
|
670 score = super(relation_possible, self).__call__(cls, req, *args, **kwargs) |
|
671 return score |
|
672 |
|
673 def score_class(self, eclass, req): |
|
674 eschema = eclass.e_schema |
|
675 try: |
|
676 if self.role == 'object': |
|
677 rschema = eschema.object_relation(self.rtype) |
|
678 else: |
|
679 rschema = eschema.subject_relation(self.rtype) |
|
680 except KeyError: |
|
681 return 0 |
|
682 if self.target_etype is not None: |
|
683 try: |
|
684 if self.role == 'subject': |
|
685 return int(self.target_etype in rschema.objects(eschema)) |
|
686 else: |
|
687 return int(self.target_etype in rschema.subjects(eschema)) |
|
688 except KeyError: |
|
689 return 0 |
|
690 return 1 |
|
691 |
|
692 |
|
693 class partial_relation_possible(PartialSelectorMixIn, relation_possible): |
|
694 """partial version of the relation_possible selector |
|
695 |
|
696 The selector will look for class attributes to find its missing |
|
697 information. The list of attributes required on the class |
|
698 for this selector are: |
|
699 |
|
700 - `rtype`: same as `rtype` parameter of the `relation_possible` selector |
|
701 |
|
702 - `role`: this attribute will be passed to the `cubicweb.role` function |
|
703 to determine the role of class in the relation |
|
704 |
|
705 - `etype` (optional): the entity type on the other side of the relation |
|
706 |
|
707 :param action: a relation schema action (one of 'read', 'add', 'delete') |
|
708 which must be granted to the logged user, else a 0 score will |
|
709 be returned |
|
710 """ |
|
711 def __init__(self, action='read', once_is_enough=False): |
|
712 super(partial_relation_possible, self).__init__(None, None, None, |
|
713 action, once_is_enough) |
|
714 |
|
715 def complete(self, cls): |
|
716 self.rtype = cls.rtype |
|
717 self.role = role(cls) |
|
718 self.target_etype = getattr(cls, 'etype', None) |
|
719 |
|
720 |
|
721 class may_add_relation(EntitySelector): |
|
722 """accept if the relation can be added to an entity found in the result set |
|
723 by the logged user. |
|
724 |
|
725 See `EntitySelector` documentation for behaviour when row is not specified. |
|
726 |
|
727 :param rtype: a relation type (`basestring`) |
|
728 :param role: the role of the result set entity in the relation. 'subject' or |
|
729 'object', default to 'subject'. |
|
730 """ |
|
731 |
|
732 def __init__(self, rtype, role='subject', once_is_enough=False): |
|
733 super(may_add_relation, self).__init__(once_is_enough) |
|
734 self.rtype = rtype |
|
735 self.role = role |
|
736 |
|
737 def score_entity(self, entity): |
|
738 rschema = entity.schema.rschema(self.rtype) |
|
739 if self.role == 'subject': |
|
740 if not rschema.has_perm(entity.req, 'add', fromeid=entity.eid): |
|
741 return 0 |
|
742 elif not rschema.has_perm(entity.req, 'add', toeid=entity.eid): |
|
743 return 0 |
|
744 return 1 |
|
745 |
|
746 |
|
747 class partial_may_add_relation(PartialSelectorMixIn, may_add_relation): |
|
748 """partial version of the may_add_relation selector |
|
749 |
|
750 The selector will look for class attributes to find its missing |
|
751 information. The list of attributes required on the class |
|
752 for this selector are: |
|
753 |
|
754 - `rtype`: same as `rtype` parameter of the `relation_possible` selector |
|
755 |
|
756 - `role`: this attribute will be passed to the `cubicweb.role` function |
|
757 to determine the role of class in the relation. |
|
758 |
|
759 :param action: a relation schema action (one of 'read', 'add', 'delete') |
|
760 which must be granted to the logged user, else a 0 score will |
|
761 be returned |
|
762 """ |
|
763 def __init__(self, once_is_enough=False): |
|
764 super(partial_may_add_relation, self).__init__(None, None, once_is_enough) |
|
765 |
|
766 def complete(self, cls): |
|
767 self.rtype = cls.rtype |
|
768 self.role = role(cls) |
|
769 |
|
770 |
|
771 class has_related_entities(EntitySelector): |
|
772 """accept if entity found in the result set has some linked entities using |
|
773 the specified relation (optionaly filtered according to the specified target |
|
774 type). Checks first if the relation is possible. |
|
775 |
|
776 See `EntitySelector` documentation for behaviour when row is not specified. |
|
777 |
|
778 :param rtype: a relation type (`basestring`) |
|
779 :param role: the role of the result set entity in the relation. 'subject' or |
|
780 'object', default to 'subject'. |
|
781 :param target_type: if specified, check the relation's end may be of this |
|
782 target type (`basestring`) |
|
783 """ |
|
784 def __init__(self, rtype, role='subject', target_etype=None, |
|
785 once_is_enough=False): |
|
786 super(has_related_entities, self).__init__(once_is_enough) |
|
787 self.rtype = rtype |
|
788 self.role = role |
|
789 self.target_etype = target_etype |
|
790 |
|
791 def score_entity(self, entity): |
|
792 relpossel = relation_possible(self.rtype, self.role, self.target_etype) |
|
793 if not relpossel.score_class(entity.__class__, entity.req): |
|
794 return 0 |
|
795 rset = entity.related(self.rtype, self.role) |
|
796 if self.target_etype: |
|
797 return any(r for r in rset.description if r[0] == self.target_etype) |
|
798 return rset and 1 or 0 |
|
799 |
|
800 |
|
801 class partial_has_related_entities(PartialSelectorMixIn, has_related_entities): |
|
802 """partial version of the has_related_entities selector |
|
803 |
|
804 The selector will look for class attributes to find its missing |
|
805 information. The list of attributes required on the class |
|
806 for this selector are: |
|
807 |
|
808 - `rtype`: same as `rtype` parameter of the `relation_possible` selector |
|
809 |
|
810 - `role`: this attribute will be passed to the `cubicweb.role` function |
|
811 to determine the role of class in the relation. |
|
812 |
|
813 - `etype` (optional): the entity type on the other side of the relation |
|
814 |
|
815 :param action: a relation schema action (one of 'read', 'add', 'delete') |
|
816 which must be granted to the logged user, else a 0 score will |
|
817 be returned |
|
818 """ |
|
819 def __init__(self, once_is_enough=False): |
|
820 super(partial_has_related_entities, self).__init__(None, None, |
|
821 None, once_is_enough) |
|
822 def complete(self, cls): |
|
823 self.rtype = cls.rtype |
|
824 self.role = role(cls) |
|
825 self.target_etype = getattr(cls, 'etype', None) |
|
826 |
|
827 |
|
828 class has_permission(EntitySelector): |
|
829 """accept if user has the permission to do the requested action on a result |
|
830 set entity. |
|
831 |
|
832 * if row is specified, return 1 if user has the permission on the entity |
|
833 instance found in the specified cell |
|
834 * else return a positive score if user has the permission for every entity |
|
835 in the found in the specified column |
|
836 |
|
837 note: None values (resulting from some outer join in the query) are not |
|
838 considered. |
|
839 |
|
840 :param action: an entity schema action (eg 'read'/'add'/'delete'/'update') |
|
841 """ |
|
842 def __init__(self, action, once_is_enough=False): |
|
843 super(has_permission, self).__init__(once_is_enough) |
|
844 self.action = action |
|
845 |
|
846 @lltrace |
|
847 def __call__(self, cls, req, rset, row=None, col=0, **kwargs): |
|
848 if rset is None: |
|
849 return 0 |
|
850 user = req.user |
|
851 action = self.action |
|
852 if row is None: |
|
853 score = 0 |
|
854 need_local_check = [] |
|
855 geteschema = cls.schema.eschema |
|
856 for etype in rset.column_types(0): |
|
857 if etype in BASE_TYPES: |
|
858 return 0 |
|
859 eschema = geteschema(etype) |
|
860 if not user.matching_groups(eschema.get_groups(action)): |
|
861 if eschema.has_local_role(action): |
|
862 # have to ckeck local roles |
|
863 need_local_check.append(eschema) |
|
864 continue |
|
865 else: |
|
866 # even a local role won't be enough |
|
867 return 0 |
|
868 score += 1 |
|
869 if need_local_check: |
|
870 # check local role for entities of necessary types |
|
871 for i, row in enumerate(rset): |
|
872 if not rset.description[i][0] in need_local_check: |
|
873 continue |
|
874 if not self.score(req, rset, i, col): |
|
875 return 0 |
|
876 score += 1 |
|
877 return score |
|
878 return self.score(req, rset, row, col) |
|
879 |
|
880 def score_entity(self, entity): |
|
881 if entity.has_perm(self.action): |
|
882 return 1 |
|
883 return 0 |
|
884 |
|
885 |
|
886 class has_add_permission(EClassSelector): |
|
887 """accept if logged user has the add permission on entity class found in the |
|
888 result set, and class is not a strict subobject. |
|
889 |
|
890 See `EClassSelector` documentation for behaviour when row is not specified. |
|
891 """ |
|
892 def score(self, cls, req, etype): |
|
893 eschema = cls.schema.eschema(etype) |
|
894 if not (eschema.is_final() or eschema.is_subobject(strict=True)) \ |
|
895 and eschema.has_perm(req, 'add'): |
|
896 return 1 |
|
897 return 0 |
|
898 |
|
899 |
|
900 class rql_condition(EntitySelector): |
|
901 """accept if an arbitrary rql return some results for an eid found in the |
|
902 result set. Returned score is the number of items returned by the rql |
|
903 condition. |
|
904 |
|
905 See `EntitySelector` documentation for behaviour when row is not specified. |
|
906 |
|
907 :param expression: basestring containing an rql expression, which should use |
|
908 X variable to represent the context entity and may use U |
|
909 to represent the logged user |
|
910 |
|
911 return the sum of the number of items returned by the rql condition as score |
|
912 or 0 at the first entity scoring to zero. |
|
913 """ |
|
914 def __init__(self, expression, once_is_enough=False): |
|
915 super(rql_condition, self).__init__(once_is_enough) |
|
916 if 'U' in frozenset(split_expression(expression)): |
|
917 rql = 'Any X WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression |
|
918 else: |
|
919 rql = 'Any X WHERE X eid %%(x)s, %s' % expression |
|
920 self.rql = rql |
|
921 |
|
922 def score(self, req, rset, row, col): |
|
923 try: |
|
924 return len(req.execute(self.rql, {'x': rset[row][col], |
|
925 'u': req.user.eid}, 'x')) |
|
926 except Unauthorized: |
|
927 return 0 |
|
928 |
|
929 |
|
930 class but_etype(EntitySelector): |
|
931 """accept if the given entity types are not found in the result set. |
|
932 |
|
933 See `EntitySelector` documentation for behaviour when row is not specified. |
|
934 |
|
935 :param *etypes: entity types (`basestring`) which should be refused |
|
936 """ |
|
937 def __init__(self, *etypes): |
|
938 super(but_etype, self).__init__() |
|
939 self.but_etypes = etypes |
|
940 |
|
941 def score(self, req, rset, row, col): |
|
942 if rset.description[row][col] in self.but_etypes: |
|
943 return 0 |
|
944 return 1 |
|
945 |
|
946 |
|
947 class score_entity(EntitySelector): |
|
948 """accept if some arbitrary function return a positive score for an entity |
|
949 found in the result set. |
|
950 |
|
951 See `EntitySelector` documentation for behaviour when row is not specified. |
|
952 |
|
953 :param scorefunc: callable expected to take an entity as argument and to |
|
954 return a score >= 0 |
|
955 """ |
|
956 def __init__(self, scorefunc, once_is_enough=False): |
|
957 super(score_entity, self).__init__(once_is_enough) |
|
958 self.score_entity = scorefunc |
|
959 |
|
960 |
|
961 # XXX DEPRECATED ############################################################## |
|
962 |
|
963 yes_selector = deprecated_function(yes) |
|
964 norset_selector = deprecated_function(none_rset) |
|
965 rset_selector = deprecated_function(any_rset) |
|
966 anyrset_selector = deprecated_function(nonempty_rset) |
|
967 emptyrset_selector = deprecated_function(empty_rset) |
|
968 onelinerset_selector = deprecated_function(one_line_rset) |
|
969 twolinerset_selector = deprecated_function(two_lines_rset) |
|
970 twocolrset_selector = deprecated_function(two_cols_rset) |
|
971 largerset_selector = deprecated_function(paginated_rset) |
|
972 sortedrset_selector = deprecated_function(sorted_rset) |
|
973 oneetyperset_selector = deprecated_function(one_etype_rset) |
|
974 multitype_selector = deprecated_function(two_etypes_rset) |
|
975 anonymous_selector = deprecated_function(anonymous_user) |
|
976 not_anonymous_selector = deprecated_function(authenticated_user) |
|
977 primaryview_selector = deprecated_function(primary_view) |
|
978 contextprop_selector = deprecated_function(match_context_prop) |
|
979 |
|
980 def nfentity_selector(cls, req, rset, row=None, col=0, **kwargs): |
|
981 return non_final_entity()(cls, req, rset, row, col) |
|
982 nfentity_selector = deprecated_function(nfentity_selector) |
|
983 |
|
984 def implement_interface(cls, req, rset, row=None, col=0, **kwargs): |
|
985 return implements(*cls.accepts_interfaces)(cls, req, rset, row, col) |
|
986 _interface_selector = deprecated_function(implement_interface) |
|
987 interface_selector = deprecated_function(implement_interface) |
|
988 implement_interface = deprecated_function(implement_interface, 'use implements') |
|
989 |
|
990 def accept_etype(cls, req, *args, **kwargs): |
|
991 """check etype presence in request form *and* accepts conformance""" |
|
992 return specified_etype_implements(*cls.accepts)(cls, req, *args) |
|
993 etype_form_selector = deprecated_function(accept_etype) |
|
994 accept_etype = deprecated_function(accept_etype, 'use specified_etype_implements') |
|
995 |
|
996 def searchstate_selector(cls, req, rset, row=None, col=0, **kwargs): |
|
997 return match_search_state(cls.search_states)(cls, req, rset, row, col) |
|
998 searchstate_selector = deprecated_function(searchstate_selector) |
|
999 |
|
1000 def match_user_group(cls, req, rset=None, row=None, col=0, **kwargs): |
|
1001 return match_user_groups(*cls.require_groups)(cls, req, rset, row, col, **kwargs) |
|
1002 in_group_selector = deprecated_function(match_user_group) |
|
1003 match_user_group = deprecated_function(match_user_group) |
|
1004 |
|
1005 def has_relation(cls, req, rset, row=None, col=0, **kwargs): |
|
1006 return relation_possible(cls.rtype, role(cls), cls.etype, |
|
1007 getattr(cls, 'require_permission', 'read'))(cls, req, rset, row, col, **kwargs) |
|
1008 has_relation = deprecated_function(has_relation) |
|
1009 |
|
1010 def one_has_relation(cls, req, rset, row=None, col=0, **kwargs): |
|
1011 return relation_possible(cls.rtype, role(cls), cls.etype, |
|
1012 getattr(cls, 'require_permission', 'read', |
|
1013 once_is_enough=True))(cls, req, rset, row, col, **kwargs) |
|
1014 one_has_relation = deprecated_function(one_has_relation, 'use relation_possible selector') |
|
1015 |
|
1016 def accept_rset(cls, req, rset, row=None, col=0, **kwargs): |
|
1017 """simply delegate to cls.accept_rset method""" |
|
1018 return implements(*cls.accepts)(cls, req, rset, row=row, col=col) |
|
1019 accept_rset_selector = deprecated_function(accept_rset) |
|
1020 accept_rset = deprecated_function(accept_rset, 'use implements selector') |
|
1021 |
|
1022 accept = chainall(non_final_entity(), accept_rset, name='accept') |
|
1023 accept_selector = deprecated_function(accept) |
|
1024 accept = deprecated_function(accept, 'use implements selector') |
|
1025 |
|
1026 accept_one = deprecated_function(chainall(one_line_rset, accept, |
|
1027 name='accept_one')) |
|
1028 accept_one_selector = deprecated_function(accept_one) |
|
1029 |
|
1030 |
|
1031 def _rql_condition(cls, req, rset, row=None, col=0, **kwargs): |
|
1032 if cls.condition: |
|
1033 return rql_condition(cls.condition)(cls, req, rset, row, col) |
|
1034 return 1 |
|
1035 _rqlcondition_selector = deprecated_function(_rql_condition) |
|
1036 |
|
1037 rqlcondition_selector = deprecated_function(chainall(non_final_entity(), one_line_rset, _rql_condition, |
|
1038 name='rql_condition')) |
|
1039 |
|
1040 def but_etype_selector(cls, req, rset, row=None, col=0, **kwargs): |
|
1041 return but_etype(cls.etype)(cls, req, rset, row, col) |
|
1042 but_etype_selector = deprecated_function(but_etype_selector) |
|
1043 |
|
1044 @lltrace |
|
1045 def etype_rtype_selector(cls, req, rset, row=None, col=0, **kwargs): |
|
1046 schema = cls.schema |
|
1047 perm = getattr(cls, 'require_permission', 'read') |
|
1048 if hasattr(cls, 'etype'): |
|
1049 eschema = schema.eschema(cls.etype) |
|
1050 if not (eschema.has_perm(req, perm) or eschema.has_local_role(perm)): |
|
1051 return 0 |
|
1052 if hasattr(cls, 'rtype'): |
|
1053 rschema = schema.rschema(cls.rtype) |
|
1054 if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)): |
|
1055 return 0 |
|
1056 return 1 |
|
1057 etype_rtype_selector = deprecated_function(etype_rtype_selector) |
|
1058 |
|
1059 #req_form_params_selector = deprecated_function(match_form_params) # form_params |
|
1060 #kwargs_selector = deprecated_function(match_kwargs) # expected_kwargs |
|
1061 |
|
1062 # compound selectors ########################################################## |
|
1063 |
|
1064 searchstate_accept = chainall(nonempty_rset(), accept, |
|
1065 name='searchstate_accept') |
|
1066 searchstate_accept_selector = deprecated_function(searchstate_accept) |
|
1067 |
|
1068 searchstate_accept_one = chainall(one_line_rset, accept, _rql_condition, |
|
1069 name='searchstate_accept_one') |
|
1070 searchstate_accept_one_selector = deprecated_function(searchstate_accept_one) |
|
1071 |
|
1072 searchstate_accept = deprecated_function(searchstate_accept) |
|
1073 searchstate_accept_one = deprecated_function(searchstate_accept_one) |
|
1074 |
|
1075 |
|
1076 def unbind_method(selector): |
|
1077 def new_selector(registered): |
|
1078 # get the unbound method |
|
1079 if hasattr(registered, 'im_func'): |
|
1080 registered = registered.im_func |
|
1081 # don't rebind since it will be done automatically during |
|
1082 # the assignment, inside the destination class body |
|
1083 return selector(registered) |
|
1084 new_selector.__name__ = selector.__name__ |
|
1085 return new_selector |
|
1086 |
|
1087 |
|
1088 def deprecate(registered, msg): |
|
1089 # get the unbound method |
|
1090 if hasattr(registered, 'im_func'): |
|
1091 registered = registered.im_func |
|
1092 def _deprecate(cls, vreg): |
|
1093 warn(msg, DeprecationWarning) |
|
1094 return registered(cls, vreg) |
|
1095 return _deprecate |
|
1096 |
|
1097 @unbind_method |
|
1098 def require_group_compat(registered): |
|
1099 def plug_selector(cls, vreg): |
|
1100 cls = registered(cls, vreg) |
|
1101 if getattr(cls, 'require_groups', None): |
|
1102 warn('use "match_user_groups(group1, group2)" instead of using require_groups', |
|
1103 DeprecationWarning) |
|
1104 cls.__select__ &= match_user_groups(cls.require_groups) |
|
1105 return cls |
|
1106 return plug_selector |
|
1107 |
|
1108 @unbind_method |
|
1109 def accepts_compat(registered): |
|
1110 def plug_selector(cls, vreg): |
|
1111 cls = registered(cls, vreg) |
|
1112 if getattr(cls, 'accepts', None): |
|
1113 warn('use "implements("EntityType", IFace)" instead of using accepts on %s' |
|
1114 % cls, |
|
1115 DeprecationWarning) |
|
1116 cls.__select__ &= implements(*cls.accepts) |
|
1117 return cls |
|
1118 return plug_selector |
|
1119 |
|
1120 @unbind_method |
|
1121 def accepts_etype_compat(registered): |
|
1122 def plug_selector(cls, vreg): |
|
1123 cls = registered(cls, vreg) |
|
1124 if getattr(cls, 'accepts', None): |
|
1125 warn('use "specified_etype_implements("EntityType", IFace)" instead of using accepts', |
|
1126 DeprecationWarning) |
|
1127 cls.__select__ &= specified_etype_implements(*cls.accepts) |
|
1128 return cls |
|
1129 return plug_selector |
|
1130 |
|
1131 @unbind_method |
|
1132 def condition_compat(registered): |
|
1133 def plug_selector(cls, vreg): |
|
1134 cls = registered(cls, vreg) |
|
1135 if getattr(cls, 'condition', None): |
|
1136 warn('use "use rql_condition(expression)" instead of using condition', |
|
1137 DeprecationWarning) |
|
1138 cls.__select__ &= rql_condition(cls.condition) |
|
1139 return cls |
|
1140 return plug_selector |
|
1141 |
|
1142 @unbind_method |
|
1143 def has_relation_compat(registered): |
|
1144 def plug_selector(cls, vreg): |
|
1145 cls = registered(cls, vreg) |
|
1146 if getattr(cls, 'etype', None): |
|
1147 warn('use relation_possible selector instead of using etype_rtype', |
|
1148 DeprecationWarning) |
|
1149 cls.__select__ &= relation_possible(cls.rtype, role(cls), |
|
1150 getattr(cls, 'etype', None), |
|
1151 action=getattr(cls, 'require_permission', 'read')) |
|
1152 return cls |
|
1153 return plug_selector |