|
1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """ |
|
19 The :mod:`cubicweb.web.facet` module contains a set of abstract classes to use |
|
20 as bases to build your own facets |
|
21 |
|
22 All facet classes inherits from the :class:`AbstractFacet` class, though you'll |
|
23 usually find some more handy class that do what you want. |
|
24 |
|
25 Let's see available classes. |
|
26 |
|
27 Classes you'll want to use |
|
28 -------------------------- |
|
29 .. autoclass:: cubicweb.web.facet.RelationFacet |
|
30 .. autoclass:: cubicweb.web.facet.RelationAttributeFacet |
|
31 .. autoclass:: cubicweb.web.facet.HasRelationFacet |
|
32 .. autoclass:: cubicweb.web.facet.AttributeFacet |
|
33 .. autoclass:: cubicweb.web.facet.RQLPathFacet |
|
34 .. autoclass:: cubicweb.web.facet.RangeFacet |
|
35 .. autoclass:: cubicweb.web.facet.DateRangeFacet |
|
36 .. autoclass:: cubicweb.web.facet.BitFieldFacet |
|
37 .. autoclass:: cubicweb.web.facet.AbstractRangeRQLPathFacet |
|
38 .. autoclass:: cubicweb.web.facet.RangeRQLPathFacet |
|
39 .. autoclass:: cubicweb.web.facet.DateRangeRQLPathFacet |
|
40 |
|
41 Classes for facets implementor |
|
42 ------------------------------ |
|
43 Unless you didn't find the class that does the job you want above, you may want |
|
44 to skip those classes... |
|
45 |
|
46 .. autoclass:: cubicweb.web.facet.AbstractFacet |
|
47 .. autoclass:: cubicweb.web.facet.VocabularyFacet |
|
48 |
|
49 .. comment: XXX widgets |
|
50 """ |
|
51 |
|
52 __docformat__ = "restructuredtext en" |
|
53 from cubicweb import _ |
|
54 |
|
55 from functools import reduce |
|
56 from warnings import warn |
|
57 from copy import deepcopy |
|
58 from datetime import datetime, timedelta |
|
59 |
|
60 from six import text_type, string_types |
|
61 |
|
62 from logilab.mtconverter import xml_escape |
|
63 from logilab.common.graph import has_path |
|
64 from logilab.common.decorators import cached, cachedproperty |
|
65 from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime |
|
66 from logilab.common.deprecation import deprecated |
|
67 from logilab.common.registry import yes |
|
68 |
|
69 from rql import nodes, utils |
|
70 |
|
71 from cubicweb import Unauthorized |
|
72 from cubicweb.schema import display_name |
|
73 from cubicweb.uilib import css_em_num_value, domid |
|
74 from cubicweb.utils import make_uid |
|
75 from cubicweb.predicates import match_context_prop, partial_relation_possible |
|
76 from cubicweb.appobject import AppObject |
|
77 from cubicweb.web import RequestError, htmlwidgets |
|
78 |
|
79 |
|
80 def rtype_facet_title(facet): |
|
81 if facet.cw_rset: |
|
82 ptypes = facet.cw_rset.column_types(0) |
|
83 if len(ptypes) == 1: |
|
84 return display_name(facet._cw, facet.rtype, form=facet.role, |
|
85 context=next(iter(ptypes))) |
|
86 return display_name(facet._cw, facet.rtype, form=facet.role) |
|
87 |
|
88 def get_facet(req, facetid, select, filtered_variable): |
|
89 return req.vreg['facets'].object_by_id(facetid, req, select=select, |
|
90 filtered_variable=filtered_variable) |
|
91 |
|
92 @deprecated('[3.13] filter_hiddens moved to cubicweb.web.views.facets with ' |
|
93 'slightly modified prototype') |
|
94 def filter_hiddens(w, baserql, **kwargs): |
|
95 from cubicweb.web.views.facets import filter_hiddens |
|
96 return filter_hiddens(w, baserql, wdgs=kwargs.pop('facets'), **kwargs) |
|
97 |
|
98 |
|
99 ## rqlst manipulation functions used by facets ################################ |
|
100 |
|
101 def init_facets(rset, select, mainvar=None): |
|
102 """Alters in place the <select> for filtering and returns related data. |
|
103 |
|
104 Calls :func:`prepare_select` to prepare the syntaxtree for selection and |
|
105 :func:`get_filtered_variable` that selects the variable to be filtered and |
|
106 drops several parts of the select tree. See each function docstring for |
|
107 details. |
|
108 |
|
109 :param rset: ResultSet we init facet for. |
|
110 :type rset: :class:`~cubicweb.rset.ResultSet` |
|
111 |
|
112 :param select: Select statement to be *altered* to support filtering. |
|
113 :type select: :class:`~rql.stmts.Select` from the ``rset`` parameters. |
|
114 |
|
115 :param mainvar: Name of the variable we want to filter with facets. |
|
116 :type mainvar: string |
|
117 |
|
118 :rtype: (filtered_variable, baserql) tuple. |
|
119 :return filtered_variable: A rql class:`~rql.node.VariableRef` |
|
120 instance as returned by |
|
121 :func:`get_filtered_variable`. |
|
122 |
|
123 :return baserql: A string containing the rql before |
|
124 :func:`prepare_select` but after |
|
125 :func:`get_filtered_variable`. |
|
126 """ |
|
127 rset.req.vreg.rqlhelper.annotate(select) |
|
128 filtered_variable = get_filtered_variable(select, mainvar) |
|
129 baserql = select.as_string(kwargs=rset.args) # before call to prepare_select |
|
130 prepare_select(select, filtered_variable) |
|
131 return filtered_variable, baserql |
|
132 |
|
133 def get_filtered_variable(select, mainvar=None): |
|
134 """ Return the variable whose name is `mainvar` |
|
135 or the first variable selected in column 0 |
|
136 """ |
|
137 if mainvar is None: |
|
138 vref = next(select.selection[0].iget_nodes(nodes.VariableRef)) |
|
139 return vref.variable |
|
140 return select.defined_vars[mainvar] |
|
141 |
|
142 def prepare_select(select, filtered_variable): |
|
143 """prepare a syntax tree to generate facet filters |
|
144 |
|
145 * remove ORDERBY/GROUPBY clauses |
|
146 * cleanup selection (remove everything) |
|
147 * undefine unnecessary variables |
|
148 * set DISTINCT |
|
149 |
|
150 Notice unset of LIMIT/OFFSET us expected to be done by a previous call to |
|
151 :func:`get_filtered_variable`. |
|
152 """ |
|
153 # cleanup sort terms / group by |
|
154 select.remove_sort_terms() |
|
155 select.remove_groups() |
|
156 # XXX remove aggregat from having |
|
157 # selection: only vocabulary entity |
|
158 for term in select.selection[:]: |
|
159 select.remove_selected(term) |
|
160 # remove unbound variables which only have some type restriction |
|
161 for dvar in list(select.defined_vars.values()): |
|
162 if not (dvar is filtered_variable or dvar.stinfo['relations']): |
|
163 select.undefine_variable(dvar) |
|
164 # global tree config: DISTINCT, LIMIT, OFFSET |
|
165 select.set_distinct(True) |
|
166 |
|
167 @deprecated('[3.13] use init_facets instead') |
|
168 def prepare_facets_rqlst(rqlst, args=None): |
|
169 assert len(rqlst.children) == 1, 'FIXME: union not yet supported' |
|
170 select = rqlst.children[0] |
|
171 filtered_variable = get_filtered_variable(select) |
|
172 baserql = select.as_string(args) |
|
173 prepare_select(select, filtered_variable) |
|
174 return filtered_variable, baserql |
|
175 |
|
176 def prepare_vocabulary_select(select, filtered_variable, rtype, role, |
|
177 select_target_entity=True): |
|
178 """prepare a syntax tree to generate a filter vocabulary rql using the given |
|
179 relation: |
|
180 * create a variable to filter on this relation |
|
181 * add the relation |
|
182 * add the new variable to GROUPBY clause if necessary |
|
183 * add the new variable to the selection |
|
184 """ |
|
185 newvar = _add_rtype_relation(select, filtered_variable, rtype, role)[0] |
|
186 if select_target_entity: |
|
187 # if select.groupby: XXX we remove groupby now |
|
188 # select.add_group_var(newvar) |
|
189 select.add_selected(newvar) |
|
190 # add is restriction if necessary |
|
191 if filtered_variable.stinfo['typerel'] is None: |
|
192 etypes = frozenset(sol[filtered_variable.name] for sol in select.solutions) |
|
193 select.add_type_restriction(filtered_variable, etypes) |
|
194 return newvar |
|
195 |
|
196 |
|
197 def insert_attr_select_relation(select, filtered_variable, rtype, role, attrname, |
|
198 sortfuncname=None, sortasc=True, |
|
199 select_target_entity=True): |
|
200 """modify a syntax tree to : |
|
201 * link a new variable to `filtered_variable` through `rtype` (where filtered_variable has `role`) |
|
202 * retrieve only the newly inserted variable and its `attrname` |
|
203 |
|
204 Sorting: |
|
205 * on `attrname` ascendant (`sortasc`=True) or descendant (`sortasc`=False) |
|
206 * on `sortfuncname`(`attrname`) if `sortfuncname` is specified |
|
207 * no sort if `sortasc` is None |
|
208 """ |
|
209 cleanup_select(select, filtered_variable) |
|
210 var = prepare_vocabulary_select(select, filtered_variable, rtype, role, |
|
211 select_target_entity) |
|
212 attrvar = select.make_variable() |
|
213 select.add_relation(var, attrname, attrvar) |
|
214 # if query is grouped, we have to add the attribute variable |
|
215 #if select.groupby: XXX may not occur anymore |
|
216 # if not attrvar in select.groupby: |
|
217 # select.add_group_var(attrvar) |
|
218 if sortasc is not None: |
|
219 _set_orderby(select, attrvar, sortasc, sortfuncname) |
|
220 # add attribute variable to selection |
|
221 select.add_selected(attrvar) |
|
222 return var |
|
223 |
|
224 |
|
225 def cleanup_select(select, filtered_variable): |
|
226 """cleanup tree from unnecessary restrictions: |
|
227 * attribute selection |
|
228 * optional relations linked to the main variable |
|
229 * mandatory relations linked to the main variable |
|
230 """ |
|
231 if select.where is None: |
|
232 return |
|
233 schema = select.root.schema |
|
234 toremove = set() |
|
235 vargraph = deepcopy(select.vargraph) # graph representing links between variable |
|
236 for rel in select.where.get_nodes(nodes.Relation): |
|
237 ovar = _may_be_removed(rel, schema, filtered_variable) |
|
238 if ovar is not None: |
|
239 toremove.add(ovar) |
|
240 removed = set() |
|
241 while toremove: |
|
242 trvar = toremove.pop() |
|
243 trvarname = trvar.name |
|
244 # remove paths using this variable from the graph |
|
245 linkedvars = vargraph.pop(trvarname) |
|
246 for ovarname in linkedvars: |
|
247 vargraph[ovarname].remove(trvarname) |
|
248 # remove relation using this variable |
|
249 for rel in trvar.stinfo['relations']: |
|
250 if rel in removed: |
|
251 # already removed |
|
252 continue |
|
253 select.remove_node(rel) |
|
254 removed.add(rel) |
|
255 rel = trvar.stinfo['typerel'] |
|
256 if rel is not None and not rel in removed: |
|
257 select.remove_node(rel) |
|
258 removed.add(rel) |
|
259 # cleanup groupby clause |
|
260 if select.groupby: |
|
261 for vref in select.groupby[:]: |
|
262 if vref.name == trvarname: |
|
263 select.remove_group_var(vref) |
|
264 # we can also remove all variables which are linked to this variable |
|
265 # and have no path to the main variable |
|
266 for ovarname in linkedvars: |
|
267 if ovarname == filtered_variable.name: |
|
268 continue |
|
269 if not has_path(vargraph, ovarname, filtered_variable.name): |
|
270 toremove.add(select.defined_vars[ovarname]) |
|
271 |
|
272 |
|
273 def _may_be_removed(rel, schema, variable): |
|
274 """if the given relation may be removed from the tree, return the variable |
|
275 on the other side of `variable`, else return None |
|
276 Conditions: |
|
277 * the relation is an attribute selection of the main variable |
|
278 * the relation is optional relation linked to the main variable |
|
279 * the relation is a mandatory relation linked to the main variable |
|
280 without any restriction on the other variable |
|
281 """ |
|
282 lhs, rhs = rel.get_variable_parts() |
|
283 rschema = schema.rschema(rel.r_type) |
|
284 if lhs.variable is variable: |
|
285 try: |
|
286 ovar = rhs.variable |
|
287 except AttributeError: |
|
288 # constant restriction |
|
289 # XXX: X title LOWER(T) if it makes sense? |
|
290 return None |
|
291 if rschema.final: |
|
292 if len(ovar.stinfo['relations']) == 1 \ |
|
293 and not ovar.stinfo.get('having'): |
|
294 # attribute selection |
|
295 return ovar |
|
296 return None |
|
297 opt = 'right' |
|
298 cardidx = 0 |
|
299 elif getattr(rhs, 'variable', None) is variable: |
|
300 ovar = lhs.variable |
|
301 opt = 'left' |
|
302 cardidx = 1 |
|
303 else: |
|
304 # not directly linked to the main variable |
|
305 return None |
|
306 if rel.optional in (opt, 'both'): |
|
307 # optional relation |
|
308 return ovar |
|
309 if all(rdef.cardinality[cardidx] in '1+' |
|
310 for rdef in rschema.rdefs.values()): |
|
311 # mandatory relation without any restriction on the other variable |
|
312 for orel in ovar.stinfo['relations']: |
|
313 if rel is orel: |
|
314 continue |
|
315 if _may_be_removed(orel, schema, ovar) is None: |
|
316 return None |
|
317 return ovar |
|
318 return None |
|
319 |
|
320 def _make_relation(select, variable, rtype, role): |
|
321 newvar = select.make_variable() |
|
322 if role == 'object': |
|
323 rel = nodes.make_relation(newvar, rtype, (variable,), nodes.VariableRef) |
|
324 else: |
|
325 rel = nodes.make_relation(variable, rtype, (newvar,), nodes.VariableRef) |
|
326 return newvar, rel |
|
327 |
|
328 def _add_rtype_relation(select, variable, rtype, role): |
|
329 """add a relation relying `variable` to entities linked by the `rtype` |
|
330 relation (where `variable` has `role`) |
|
331 |
|
332 return the inserted variable for linked entities. |
|
333 """ |
|
334 newvar, newrel = _make_relation(select, variable, rtype, role) |
|
335 select.add_restriction(newrel) |
|
336 return newvar, newrel |
|
337 |
|
338 def _remove_relation(select, rel, var): |
|
339 """remove a constraint relation from the syntax tree""" |
|
340 # remove the relation |
|
341 select.remove_node(rel) |
|
342 # remove relations where the filtered variable appears on the |
|
343 # lhs and rhs is a constant restriction |
|
344 extra = [] |
|
345 for vrel in var.stinfo['relations']: |
|
346 if vrel is rel: |
|
347 continue |
|
348 if vrel.children[0].variable is var: |
|
349 if not vrel.children[1].get_nodes(nodes.Constant): |
|
350 extra.append(vrel) |
|
351 select.remove_node(vrel) |
|
352 return extra |
|
353 |
|
354 def _set_orderby(select, newvar, sortasc, sortfuncname): |
|
355 if sortfuncname is None: |
|
356 select.add_sort_var(newvar, sortasc) |
|
357 else: |
|
358 vref = nodes.variable_ref(newvar) |
|
359 vref.register_reference() |
|
360 sortfunc = nodes.Function(sortfuncname) |
|
361 sortfunc.append(vref) |
|
362 term = nodes.SortTerm(sortfunc, sortasc) |
|
363 select.add_sort_term(term) |
|
364 |
|
365 def _get_var(select, varname, varmap): |
|
366 try: |
|
367 return varmap[varname] |
|
368 except KeyError: |
|
369 varmap[varname] = var = select.make_variable() |
|
370 return var |
|
371 |
|
372 |
|
373 _prepare_vocabulary_rqlst = deprecated('[3.13] renamed prepare_vocabulary_select')( |
|
374 prepare_vocabulary_select) |
|
375 _cleanup_rqlst = deprecated('[3.13] renamed to cleanup_select')(cleanup_select) |
|
376 |
|
377 |
|
378 ## base facet classes ########################################################## |
|
379 |
|
380 class AbstractFacet(AppObject): |
|
381 """Abstract base class for all facets. Facets are stored in their own |
|
382 'facets' registry. They are similar to contextual components since the use |
|
383 the following configurable properties: |
|
384 |
|
385 * `visible`, boolean flag telling if a facet should be displayed or not |
|
386 |
|
387 * `order`, integer to control facets display order |
|
388 |
|
389 * `context`, telling if a facet should be displayed in the table form filter |
|
390 (context = 'tablefilter') or in the facet box (context = 'facetbox') or in |
|
391 both (context = '') |
|
392 |
|
393 The following methods define the facet API: |
|
394 |
|
395 .. automethod:: cubicweb.web.facet.AbstractFacet.get_widget |
|
396 .. automethod:: cubicweb.web.facet.AbstractFacet.add_rql_restrictions |
|
397 |
|
398 Facets will have the following attributes set (beside the standard |
|
399 :class:`~cubicweb.appobject.AppObject` ones): |
|
400 |
|
401 * `select`, the :class:`rql.stmts.Select` node of the rql syntax tree being |
|
402 filtered |
|
403 |
|
404 * `filtered_variable`, the variable node in this rql syntax tree that we're |
|
405 interested in filtering |
|
406 |
|
407 Facets implementors may also be interested in the following properties / |
|
408 methods: |
|
409 |
|
410 .. autoattribute:: cubicweb.web.facet.AbstractFacet.operator |
|
411 .. automethod:: cubicweb.web.facet.AbstractFacet.rqlexec |
|
412 """ |
|
413 __abstract__ = True |
|
414 __registry__ = 'facets' |
|
415 cw_property_defs = { |
|
416 _('visible'): dict(type='Boolean', default=True, |
|
417 help=_('display the facet or not')), |
|
418 _('order'): dict(type='Int', default=99, |
|
419 help=_('display order of the facet')), |
|
420 _('context'): dict(type='String', default='', |
|
421 # None <-> both |
|
422 vocabulary=(_('tablefilter'), _('facetbox'), ''), |
|
423 help=_('context where this facet should be displayed, ' |
|
424 'leave empty for both')), |
|
425 } |
|
426 visible = True |
|
427 context = '' |
|
428 needs_update = False |
|
429 start_unfolded = True |
|
430 allow_hide = True |
|
431 cw_rset = None # ensure facets have a cw_rset attribute |
|
432 |
|
433 def __init__(self, req, select=None, filtered_variable=None, |
|
434 **kwargs): |
|
435 super(AbstractFacet, self).__init__(req, **kwargs) |
|
436 assert select is not None |
|
437 assert filtered_variable |
|
438 # take care: facet may be retreived using `object_by_id` from an ajax call |
|
439 # or from `select` using the result set to filter |
|
440 self.select = select |
|
441 self.filtered_variable = filtered_variable |
|
442 |
|
443 def __repr__(self): |
|
444 return '<%s>' % self.__class__.__name__ |
|
445 |
|
446 def get_widget(self): |
|
447 """Return the widget instance to use to display this facet, or None if |
|
448 the facet can't do anything valuable (only one value in the vocabulary |
|
449 for instance). |
|
450 """ |
|
451 raise NotImplementedError |
|
452 |
|
453 def add_rql_restrictions(self): |
|
454 """When some facet criteria has been updated, this method is called to |
|
455 add restriction for this facet into the rql syntax tree. It should get |
|
456 back its value in form parameters, and modify the syntax tree |
|
457 (`self.select`) accordingly. |
|
458 """ |
|
459 raise NotImplementedError |
|
460 |
|
461 @property |
|
462 def operator(self): |
|
463 """Return the operator (AND or OR) to use for this facet when multiple |
|
464 values are selected. |
|
465 """ |
|
466 # OR between selected values by default |
|
467 return self._cw.form.get(xml_escape(self.__regid__) + '_andor', 'OR') |
|
468 |
|
469 def rqlexec(self, rql, args=None): |
|
470 """Utility method to execute some rql queries, and simply returning an |
|
471 empty list if :exc:`Unauthorized` is raised. |
|
472 """ |
|
473 try: |
|
474 return self._cw.execute(rql, args) |
|
475 except Unauthorized: |
|
476 return [] |
|
477 |
|
478 @property |
|
479 def wdgclass(self): |
|
480 raise NotImplementedError |
|
481 |
|
482 @property |
|
483 @deprecated('[3.13] renamed .select') |
|
484 def rqlst(self): |
|
485 return self.select |
|
486 |
|
487 |
|
488 class VocabularyFacet(AbstractFacet): |
|
489 """This abstract class extend :class:`AbstractFacet` to use the |
|
490 :class:`FacetVocabularyWidget` as widget, suitable for facets that may |
|
491 restrict values according to a (usually computed) vocabulary. |
|
492 |
|
493 A class which inherits from VocabularyFacet must define at least these methods: |
|
494 |
|
495 .. automethod:: cubicweb.web.facet.VocabularyFacet.vocabulary |
|
496 .. automethod:: cubicweb.web.facet.VocabularyFacet.possible_values |
|
497 """ |
|
498 needs_update = True |
|
499 support_and = False |
|
500 |
|
501 @property |
|
502 def wdgclass(self): |
|
503 return FacetVocabularyWidget |
|
504 |
|
505 def get_selected(self): |
|
506 return frozenset(int(eid) for eid in self._cw.list_form_param(self.__regid__)) |
|
507 |
|
508 def get_widget(self): |
|
509 """Return the widget instance to use to display this facet. |
|
510 |
|
511 This implementation expects a .vocabulary method on the facet and |
|
512 return a combobox displaying this vocabulary. |
|
513 """ |
|
514 vocab = self.vocabulary() |
|
515 if len(vocab) <= 1: |
|
516 return None |
|
517 wdg = self.wdgclass(self) |
|
518 selected = self.get_selected() |
|
519 for label, value in vocab: |
|
520 wdg.items.append((value, label, value in selected)) |
|
521 return wdg |
|
522 |
|
523 def vocabulary(self): |
|
524 """Return vocabulary for this facet, eg a list of 2-uple (label, value). |
|
525 """ |
|
526 raise NotImplementedError |
|
527 |
|
528 def possible_values(self): |
|
529 """Return a list of possible values (as string since it's used to |
|
530 compare to a form value in javascript) for this facet. |
|
531 """ |
|
532 raise NotImplementedError |
|
533 |
|
534 |
|
535 class RelationFacet(VocabularyFacet): |
|
536 """Base facet to filter some entities according to other entities to which |
|
537 they are related. Create concrete facet by inheriting from this class an then |
|
538 configuring it by setting class attribute described below. |
|
539 |
|
540 The relation is defined by the `rtype` and `role` attributes. |
|
541 |
|
542 The `no_relation` boolean flag tells if a special 'no relation' value should be |
|
543 added (allowing to filter on entities which *do not* have the relation set). |
|
544 Default is computed according the relation's cardinality. |
|
545 |
|
546 The values displayed for related entities will be: |
|
547 |
|
548 * result of calling their `label_vid` view if specified |
|
549 * else their `target_attr` attribute value if specified |
|
550 * else their eid (you usually want something nicer...) |
|
551 |
|
552 When no `label_vid` is set, you will get translated value if `i18nable` is |
|
553 set. By default, `i18nable` will be set according to the schema, but you can |
|
554 force its value by setting it has a class attribute. |
|
555 |
|
556 You can filter out target entity types by specifying `target_type`. |
|
557 |
|
558 By default, vocabulary will be displayed sorted on `target_attr` value in an |
|
559 ascending way. You can control sorting with: |
|
560 |
|
561 * `sortfunc`: set this to a stored procedure name if you want to sort on the |
|
562 result of this function's result instead of direct value |
|
563 |
|
564 * `sortasc`: boolean flag to control ascendant/descendant sorting |
|
565 |
|
566 To illustrate this facet, let's take for example an *excerpt* of the schema |
|
567 of an office location search application: |
|
568 |
|
569 .. sourcecode:: python |
|
570 |
|
571 class Office(WorkflowableEntityType): |
|
572 price = Int(description='euros / m2 / HC / HT') |
|
573 surface = Int(description='m2') |
|
574 has_address = SubjectRelation('PostalAddress', |
|
575 cardinality='1?', |
|
576 composite='subject') |
|
577 proposed_by = SubjectRelation('Agency') |
|
578 |
|
579 |
|
580 We can simply define a facet to filter offices according to the agency |
|
581 proposing it: |
|
582 |
|
583 .. sourcecode:: python |
|
584 |
|
585 class AgencyFacet(RelationFacet): |
|
586 __regid__ = 'agency' |
|
587 # this facet should only be selected when visualizing offices |
|
588 __select__ = RelationFacet.__select__ & is_instance('Office') |
|
589 # this facet is a filter on the 'Agency' entities linked to the office |
|
590 # through the 'proposed_by' relation, where the office is the subject |
|
591 # of the relation |
|
592 rtype = 'has_address' |
|
593 # 'subject' is the default but setting it explicitly doesn't hurt... |
|
594 role = 'subject' |
|
595 # we want to display the agency's name |
|
596 target_attr = 'name' |
|
597 """ |
|
598 __select__ = partial_relation_possible() & match_context_prop() |
|
599 # class attributes to configure the relation facet |
|
600 rtype = None |
|
601 role = 'subject' |
|
602 target_type = None |
|
603 target_attr = 'eid' |
|
604 # for subclasses parametrization, should not change if you want a |
|
605 # RelationFacet |
|
606 target_attr_type = 'Int' |
|
607 restr_attr = 'eid' |
|
608 restr_attr_type = 'Int' |
|
609 comparator = '=' # could be '<', '<=', '>', '>=' |
|
610 # set this to a stored procedure name if you want to sort on the result of |
|
611 # this function's result instead of direct value |
|
612 sortfunc = None |
|
613 # ascendant/descendant sorting |
|
614 sortasc = True |
|
615 # if you want to call a view on the entity instead of using `target_attr` |
|
616 label_vid = None |
|
617 |
|
618 # internal purpose |
|
619 _select_target_entity = True |
|
620 |
|
621 title = property(rtype_facet_title) |
|
622 no_relation_label = _('<no relation>') |
|
623 |
|
624 def __repr__(self): |
|
625 return '<%s on (%s-%s)>' % (self.__class__.__name__, self.rtype, self.role) |
|
626 |
|
627 # facet public API ######################################################### |
|
628 |
|
629 def vocabulary(self): |
|
630 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
|
631 """ |
|
632 select = self.select |
|
633 select.save_state() |
|
634 if self.rql_sort: |
|
635 sort = self.sortasc |
|
636 else: |
|
637 sort = None # will be sorted on label |
|
638 try: |
|
639 var = insert_attr_select_relation( |
|
640 select, self.filtered_variable, self.rtype, self.role, |
|
641 self.target_attr, self.sortfunc, sort, |
|
642 self._select_target_entity) |
|
643 if self.target_type is not None: |
|
644 select.add_type_restriction(var, self.target_type) |
|
645 try: |
|
646 rset = self.rqlexec(select.as_string(), self.cw_rset.args) |
|
647 except Exception: |
|
648 self.exception('error while getting vocabulary for %s, rql: %s', |
|
649 self, select.as_string()) |
|
650 return () |
|
651 finally: |
|
652 select.recover() |
|
653 # don't call rset_vocabulary on empty result set, it may be an empty |
|
654 # *list* (see rqlexec implementation) |
|
655 values = rset and self.rset_vocabulary(rset) or [] |
|
656 if self._include_no_relation(): |
|
657 values.insert(0, (self._cw._(self.no_relation_label), '')) |
|
658 return values |
|
659 |
|
660 def possible_values(self): |
|
661 """return a list of possible values (as string since it's used to |
|
662 compare to a form value in javascript) for this facet |
|
663 """ |
|
664 select = self.select |
|
665 select.save_state() |
|
666 try: |
|
667 cleanup_select(select, self.filtered_variable) |
|
668 if self._select_target_entity: |
|
669 prepare_vocabulary_select(select, self.filtered_variable, self.rtype, |
|
670 self.role, select_target_entity=True) |
|
671 else: |
|
672 insert_attr_select_relation( |
|
673 select, self.filtered_variable, self.rtype, self.role, |
|
674 self.target_attr, select_target_entity=False) |
|
675 values = [text_type(x) for x, in self.rqlexec(select.as_string())] |
|
676 except Exception: |
|
677 self.exception('while computing values for %s', self) |
|
678 return [] |
|
679 finally: |
|
680 select.recover() |
|
681 if self._include_no_relation(): |
|
682 values.append('') |
|
683 return values |
|
684 |
|
685 def add_rql_restrictions(self): |
|
686 """add restriction for this facet into the rql syntax tree""" |
|
687 value = self._cw.form.get(self.__regid__) |
|
688 if value is None: |
|
689 return |
|
690 filtered_variable = self.filtered_variable |
|
691 restrvar, rel = _add_rtype_relation(self.select, filtered_variable, |
|
692 self.rtype, self.role) |
|
693 self.value_restriction(restrvar, rel, value) |
|
694 |
|
695 # internal control API ##################################################### |
|
696 |
|
697 @property |
|
698 def i18nable(self): |
|
699 """should label be internationalized""" |
|
700 if self.target_type: |
|
701 eschema = self._cw.vreg.schema.eschema(self.target_type) |
|
702 elif self.role == 'subject': |
|
703 eschema = self._cw.vreg.schema.rschema(self.rtype).objects()[0] |
|
704 else: |
|
705 eschema = self._cw.vreg.schema.rschema(self.rtype).subjects()[0] |
|
706 return getattr(eschema.rdef(self.target_attr), 'internationalizable', False) |
|
707 |
|
708 @property |
|
709 def no_relation(self): |
|
710 return (not self._cw.vreg.schema.rschema(self.rtype).final |
|
711 and self._search_card('?*')) |
|
712 |
|
713 @property |
|
714 def rql_sort(self): |
|
715 """return true if we can handle sorting in the rql query. E.g. if |
|
716 sortfunc is set or if we have not to transform the returned value (eg no |
|
717 label_vid and not i18nable) |
|
718 """ |
|
719 return self.sortfunc is not None or (self.label_vid is None |
|
720 and not self.i18nable) |
|
721 |
|
722 def rset_vocabulary(self, rset): |
|
723 if self.i18nable: |
|
724 tr = self._cw._ |
|
725 else: |
|
726 tr = text_type |
|
727 if self.rql_sort: |
|
728 values = [(tr(label), eid) for eid, label in rset] |
|
729 else: |
|
730 if self.label_vid is None: |
|
731 values = [(tr(label), eid) for eid, label in rset] |
|
732 else: |
|
733 values = [(entity.view(self.label_vid), entity.eid) |
|
734 for entity in rset.entities()] |
|
735 values = sorted(values) |
|
736 if not self.sortasc: |
|
737 values = list(reversed(values)) |
|
738 return values |
|
739 |
|
740 @property |
|
741 def support_and(self): |
|
742 return self._search_card('+*') |
|
743 |
|
744 # internal utilities ####################################################### |
|
745 |
|
746 @cached |
|
747 def _support_and_compat(self): |
|
748 support = self.support_and |
|
749 if callable(support): |
|
750 warn('[3.13] %s.support_and is now a property' % self.__class__, |
|
751 DeprecationWarning) |
|
752 support = support() |
|
753 return support |
|
754 |
|
755 def value_restriction(self, restrvar, rel, value): |
|
756 # XXX handle rel is None case in RQLPathFacet? |
|
757 if self.restr_attr != 'eid': |
|
758 self.select.set_distinct(True) |
|
759 if isinstance(value, string_types): |
|
760 # only one value selected |
|
761 if value: |
|
762 self.select.add_constant_restriction( |
|
763 restrvar, self.restr_attr, value, |
|
764 self.restr_attr_type) |
|
765 else: |
|
766 rel.parent.replace(rel, nodes.Not(rel)) |
|
767 elif self.operator == 'OR': |
|
768 # set_distinct only if rtype cardinality is > 1 |
|
769 if self._support_and_compat(): |
|
770 self.select.set_distinct(True) |
|
771 # multiple ORed values: using IN is fine |
|
772 if '' in value: |
|
773 value.remove('') |
|
774 self._add_not_rel_restr(rel) |
|
775 self._and_restriction(rel, restrvar, value) |
|
776 else: |
|
777 # multiple values with AND operator. We've to generate a query like |
|
778 # "X relation A, A eid 1, X relation B, B eid 1", hence the new |
|
779 # relations at each iteration in the while loop below |
|
780 if '' in value: |
|
781 raise RequestError("this doesn't make sense") |
|
782 self._and_restriction(rel, restrvar, value.pop()) |
|
783 while value: |
|
784 restrvar, rtrel = _make_relation(self.select, self.filtered_variable, |
|
785 self.rtype, self.role) |
|
786 if rel is None: |
|
787 self.select.add_restriction(rtrel) |
|
788 else: |
|
789 rel.parent.replace(rel, nodes.And(rel, rtrel)) |
|
790 self._and_restriction(rel, restrvar, value.pop()) |
|
791 |
|
792 def _and_restriction(self, rel, restrvar, value): |
|
793 if rel is None: |
|
794 self.select.add_constant_restriction(restrvar, self.restr_attr, |
|
795 value, self.restr_attr_type) |
|
796 else: |
|
797 rrel = nodes.make_constant_restriction(restrvar, self.restr_attr, |
|
798 value, self.restr_attr_type) |
|
799 rel.parent.replace(rel, nodes.And(rel, rrel)) |
|
800 |
|
801 |
|
802 @cached |
|
803 def _search_card(self, cards): |
|
804 for rdef in self._iter_rdefs(): |
|
805 if rdef.role_cardinality(self.role) in cards: |
|
806 return True |
|
807 return False |
|
808 |
|
809 def _iter_rdefs(self): |
|
810 rschema = self._cw.vreg.schema.rschema(self.rtype) |
|
811 # XXX when called via ajax, no rset to compute possible types |
|
812 possibletypes = self.cw_rset and self.cw_rset.column_types(0) |
|
813 for rdef in rschema.rdefs.values(): |
|
814 if possibletypes is not None: |
|
815 if self.role == 'subject': |
|
816 if rdef.subject not in possibletypes: |
|
817 continue |
|
818 elif rdef.object not in possibletypes: |
|
819 continue |
|
820 if self.target_type is not None: |
|
821 if self.role == 'subject': |
|
822 if rdef.object != self.target_type: |
|
823 continue |
|
824 elif rdef.subject != self.target_type: |
|
825 continue |
|
826 yield rdef |
|
827 |
|
828 def _include_no_relation(self): |
|
829 if not self.no_relation: |
|
830 return False |
|
831 if self._cw.vreg.schema.rschema(self.rtype).final: |
|
832 return False |
|
833 if self.role == 'object': |
|
834 subj = next(utils.rqlvar_maker(defined=self.select.defined_vars, |
|
835 aliases=self.select.aliases)) |
|
836 obj = self.filtered_variable.name |
|
837 else: |
|
838 subj = self.filtered_variable.name |
|
839 obj = next(utils.rqlvar_maker(defined=self.select.defined_vars, |
|
840 aliases=self.select.aliases)) |
|
841 restrictions = [] |
|
842 if self.select.where: |
|
843 restrictions.append(self.select.where.as_string()) |
|
844 if self.select.with_: |
|
845 restrictions.append('WITH ' + ','.join( |
|
846 term.as_string() for term in self.select.with_)) |
|
847 if restrictions: |
|
848 restrictions = ',' + ','.join(restrictions) |
|
849 else: |
|
850 restrictions = '' |
|
851 rql = 'Any %s LIMIT 1 WHERE NOT %s %s %s%s' % ( |
|
852 self.filtered_variable.name, subj, self.rtype, obj, restrictions) |
|
853 try: |
|
854 return bool(self.rqlexec(rql, self.cw_rset and self.cw_rset.args)) |
|
855 except Exception: |
|
856 # catch exception on executing rql, work-around #1356884 until a |
|
857 # proper fix |
|
858 self.exception('cant handle rql generated by %s', self) |
|
859 return False |
|
860 |
|
861 def _add_not_rel_restr(self, rel): |
|
862 nrrel = nodes.Not(_make_relation(self.select, self.filtered_variable, |
|
863 self.rtype, self.role)[1]) |
|
864 rel.parent.replace(rel, nodes.Or(nrrel, rel)) |
|
865 |
|
866 |
|
867 class RelationAttributeFacet(RelationFacet): |
|
868 """Base facet to filter some entities according to an attribute of other |
|
869 entities to which they are related. Most things work similarly as |
|
870 :class:`RelationFacet`, except that: |
|
871 |
|
872 * `label_vid` doesn't make sense here |
|
873 |
|
874 * you should specify the attribute type using `target_attr_type` if it's not a |
|
875 String |
|
876 |
|
877 * you can specify a comparison operator using `comparator` |
|
878 |
|
879 |
|
880 Back to our example... if you want to search office by postal code and that |
|
881 you use a :class:`RelationFacet` for that, you won't get the expected |
|
882 behaviour: if two offices have the same postal code, they've however two |
|
883 different addresses. So you'll see in the facet the same postal code twice, |
|
884 though linked to a different address entity. There is a great chance your |
|
885 users won't understand that... |
|
886 |
|
887 That's where this class come in! It's used to said that you want to filter |
|
888 according to the *attribute value* of a relatied entity, not to the entity |
|
889 itself. Now here is the source code for the facet: |
|
890 |
|
891 .. sourcecode:: python |
|
892 |
|
893 class PostalCodeFacet(RelationAttributeFacet): |
|
894 __regid__ = 'postalcode' |
|
895 # this facet should only be selected when visualizing offices |
|
896 __select__ = RelationAttributeFacet.__select__ & is_instance('Office') |
|
897 # this facet is a filter on the PostalAddress entities linked to the |
|
898 # office through the 'has_address' relation, where the office is the |
|
899 # subject of the relation |
|
900 rtype = 'has_address' |
|
901 role = 'subject' |
|
902 # we want to search according to address 'postal_code' attribute |
|
903 target_attr = 'postalcode' |
|
904 """ |
|
905 _select_target_entity = False |
|
906 # attribute type |
|
907 target_attr_type = 'String' |
|
908 # type of comparison: default is an exact match on the attribute value |
|
909 comparator = '=' # could be '<', '<=', '>', '>=' |
|
910 |
|
911 @property |
|
912 def restr_attr(self): |
|
913 return self.target_attr |
|
914 |
|
915 @property |
|
916 def restr_attr_type(self): |
|
917 return self.target_attr_type |
|
918 |
|
919 def rset_vocabulary(self, rset): |
|
920 if self.i18nable: |
|
921 tr = self._cw._ |
|
922 else: |
|
923 tr = text_type |
|
924 if self.rql_sort: |
|
925 return [(tr(value), value) for value, in rset] |
|
926 values = [(tr(value), value) for value, in rset] |
|
927 return sorted(values, reverse=not self.sortasc) |
|
928 |
|
929 |
|
930 class AttributeFacet(RelationAttributeFacet): |
|
931 """Base facet to filter some entities according one of their attribute. |
|
932 Configuration is mostly similarly as :class:`RelationAttributeFacet`, except that: |
|
933 |
|
934 * `target_attr` doesn't make sense here (you specify the attribute using `rtype` |
|
935 * `role` neither, it's systematically 'subject' |
|
936 |
|
937 So, suppose that in our office search example you want to refine search according |
|
938 to the office's surface. Here is a code snippet achieving this: |
|
939 |
|
940 .. sourcecode:: python |
|
941 |
|
942 class SurfaceFacet(AttributeFacet): |
|
943 __regid__ = 'surface' |
|
944 __select__ = AttributeFacet.__select__ & is_instance('Office') |
|
945 # this facet is a filter on the office'surface |
|
946 rtype = 'surface' |
|
947 # override the default value of operator since we want to filter |
|
948 # according to a minimal value, not an exact one |
|
949 comparator = '>=' |
|
950 |
|
951 def vocabulary(self): |
|
952 '''override the default vocabulary method since we want to |
|
953 hard-code our threshold values. |
|
954 |
|
955 Not overriding would generate a filter containing all existing |
|
956 surfaces defined in the database. |
|
957 ''' |
|
958 return [('> 200', '200'), ('> 250', '250'), |
|
959 ('> 275', '275'), ('> 300', '300')] |
|
960 """ |
|
961 |
|
962 support_and = False |
|
963 _select_target_entity = True |
|
964 |
|
965 @property |
|
966 def i18nable(self): |
|
967 """should label be internationalized""" |
|
968 for rdef in self._iter_rdefs(): |
|
969 # no 'internationalizable' property for rdef whose object is not a |
|
970 # String |
|
971 if not getattr(rdef, 'internationalizable', False): |
|
972 return False |
|
973 return True |
|
974 |
|
975 def vocabulary(self): |
|
976 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
|
977 """ |
|
978 select = self.select |
|
979 select.save_state() |
|
980 try: |
|
981 filtered_variable = self.filtered_variable |
|
982 cleanup_select(select, filtered_variable) |
|
983 newvar = prepare_vocabulary_select(select, filtered_variable, self.rtype, self.role) |
|
984 _set_orderby(select, newvar, self.sortasc, self.sortfunc) |
|
985 if self.cw_rset: |
|
986 args = self.cw_rset.args |
|
987 else: # vocabulary used for possible_values |
|
988 args = None |
|
989 try: |
|
990 rset = self.rqlexec(select.as_string(), args) |
|
991 except Exception: |
|
992 self.exception('error while getting vocabulary for %s, rql: %s', |
|
993 self, select.as_string()) |
|
994 return () |
|
995 finally: |
|
996 select.recover() |
|
997 # don't call rset_vocabulary on empty result set, it may be an empty |
|
998 # *list* (see rqlexec implementation) |
|
999 return rset and self.rset_vocabulary(rset) |
|
1000 |
|
1001 def add_rql_restrictions(self): |
|
1002 """add restriction for this facet into the rql syntax tree""" |
|
1003 value = self._cw.form.get(self.__regid__) |
|
1004 if not value: |
|
1005 return |
|
1006 filtered_variable = self.filtered_variable |
|
1007 self.select.add_constant_restriction(filtered_variable, self.rtype, value, |
|
1008 self.target_attr_type, self.comparator) |
|
1009 |
|
1010 |
|
1011 class RQLPathFacet(RelationFacet): |
|
1012 """Base facet to filter some entities according to an arbitrary rql |
|
1013 path. Path should be specified as a list of 3-uples or triplet string, where |
|
1014 'X' represent the filtered variable. You should specify using |
|
1015 `filter_variable` the snippet variable that will be used to filter out |
|
1016 results. You may also specify a `label_variable`. If you want to filter on |
|
1017 an attribute value, you usually don't want to specify the later since it's |
|
1018 the same as the filter variable, though you may have to specify the attribute |
|
1019 type using `restr_attr_type` if there are some type ambiguity in the schema |
|
1020 for the attribute. |
|
1021 |
|
1022 Using this facet, we can rewrite facets we defined previously: |
|
1023 |
|
1024 .. sourcecode:: python |
|
1025 |
|
1026 class AgencyFacet(RQLPathFacet): |
|
1027 __regid__ = 'agency' |
|
1028 # this facet should only be selected when visualizing offices |
|
1029 __select__ = is_instance('Office') |
|
1030 # this facet is a filter on the 'Agency' entities linked to the office |
|
1031 # through the 'proposed_by' relation, where the office is the subject |
|
1032 # of the relation |
|
1033 path = ['X has_address O', 'O name N'] |
|
1034 filter_variable = 'O' |
|
1035 label_variable = 'N' |
|
1036 |
|
1037 class PostalCodeFacet(RQLPathFacet): |
|
1038 __regid__ = 'postalcode' |
|
1039 # this facet should only be selected when visualizing offices |
|
1040 __select__ = is_instance('Office') |
|
1041 # this facet is a filter on the PostalAddress entities linked to the |
|
1042 # office through the 'has_address' relation, where the office is the |
|
1043 # subject of the relation |
|
1044 path = ['X has_address O', 'O postal_code PC'] |
|
1045 filter_variable = 'PC' |
|
1046 |
|
1047 Though some features, such as 'no value' or automatic internationalization, |
|
1048 won't work. This facet class is designed to be used for cases where |
|
1049 :class:`RelationFacet` or :class:`RelationAttributeFacet` can't do the trick |
|
1050 (e.g when you want to filter on entities where are not directly linked to |
|
1051 the filtered entities). |
|
1052 """ |
|
1053 __select__ = yes() # we don't want RelationFacet's selector |
|
1054 # must be specified |
|
1055 path = None |
|
1056 filter_variable = None |
|
1057 # may be specified |
|
1058 label_variable = None |
|
1059 # usually guessed, but may be explicitly specified |
|
1060 restr_attr = None |
|
1061 restr_attr_type = None |
|
1062 |
|
1063 # XXX disabled features |
|
1064 i18nable = False |
|
1065 no_relation = False |
|
1066 support_and = False |
|
1067 |
|
1068 def __init__(self, *args, **kwargs): |
|
1069 super(RQLPathFacet, self).__init__(*args, **kwargs) |
|
1070 assert self.filter_variable != self.label_variable, \ |
|
1071 ('filter_variable and label_variable should be different. ' |
|
1072 'You may want to let label_variable undefined (ie None).') |
|
1073 assert self.path and isinstance(self.path, (list, tuple)), \ |
|
1074 'path should be a list of 3-uples, not %s' % self.path |
|
1075 for part in self.path: |
|
1076 if isinstance(part, string_types): |
|
1077 part = part.split() |
|
1078 assert len(part) == 3, \ |
|
1079 'path should be a list of 3-uples, not %s' % part |
|
1080 |
|
1081 def __repr__(self): |
|
1082 return '<%s %s>' % (self.__class__.__name__, |
|
1083 ','.join(str(p) for p in self.path)) |
|
1084 |
|
1085 def vocabulary(self): |
|
1086 """return vocabulary for this facet, eg a list of (label, value)""" |
|
1087 select = self.select |
|
1088 select.save_state() |
|
1089 if self.rql_sort: |
|
1090 sort = self.sortasc |
|
1091 else: |
|
1092 sort = None # will be sorted on label |
|
1093 try: |
|
1094 cleanup_select(select, self.filtered_variable) |
|
1095 varmap, restrvar = self.add_path_to_select() |
|
1096 select.append_selected(nodes.VariableRef(restrvar)) |
|
1097 if self.label_variable: |
|
1098 attrvar = varmap[self.label_variable] |
|
1099 else: |
|
1100 attrvar = restrvar |
|
1101 select.append_selected(nodes.VariableRef(attrvar)) |
|
1102 if sort is not None: |
|
1103 _set_orderby(select, attrvar, sort, self.sortfunc) |
|
1104 try: |
|
1105 rset = self.rqlexec(select.as_string(), self.cw_rset.args) |
|
1106 except Exception: |
|
1107 self.exception('error while getting vocabulary for %s, rql: %s', |
|
1108 self, select.as_string()) |
|
1109 return () |
|
1110 finally: |
|
1111 select.recover() |
|
1112 # don't call rset_vocabulary on empty result set, it may be an empty |
|
1113 # *list* (see rqlexec implementation) |
|
1114 values = rset and self.rset_vocabulary(rset) or [] |
|
1115 if self._include_no_relation(): |
|
1116 values.insert(0, (self._cw._(self.no_relation_label), '')) |
|
1117 return values |
|
1118 |
|
1119 def possible_values(self): |
|
1120 """return a list of possible values (as string since it's used to |
|
1121 compare to a form value in javascript) for this facet |
|
1122 """ |
|
1123 select = self.select |
|
1124 select.save_state() |
|
1125 try: |
|
1126 cleanup_select(select, self.filtered_variable) |
|
1127 varmap, restrvar = self.add_path_to_select(skiplabel=True) |
|
1128 select.append_selected(nodes.VariableRef(restrvar)) |
|
1129 values = [text_type(x) for x, in self.rqlexec(select.as_string())] |
|
1130 except Exception: |
|
1131 self.exception('while computing values for %s', self) |
|
1132 return [] |
|
1133 finally: |
|
1134 select.recover() |
|
1135 if self._include_no_relation(): |
|
1136 values.append('') |
|
1137 return values |
|
1138 |
|
1139 def add_rql_restrictions(self): |
|
1140 """add restriction for this facet into the rql syntax tree""" |
|
1141 value = self._cw.form.get(self.__regid__) |
|
1142 if value is None: |
|
1143 return |
|
1144 varmap, restrvar = self.add_path_to_select( |
|
1145 skiplabel=True, skipattrfilter=True) |
|
1146 self.value_restriction(restrvar, None, value) |
|
1147 |
|
1148 def add_path_to_select(self, skiplabel=False, skipattrfilter=False): |
|
1149 varmap = {'X': self.filtered_variable} |
|
1150 actual_filter_variable = None |
|
1151 for part in self.path: |
|
1152 if isinstance(part, string_types): |
|
1153 part = part.split() |
|
1154 subject, rtype, object = part |
|
1155 if skiplabel and object == self.label_variable: |
|
1156 continue |
|
1157 if object == self.filter_variable: |
|
1158 rschema = self._cw.vreg.schema.rschema(rtype) |
|
1159 if rschema.final: |
|
1160 # filter variable is an attribute variable |
|
1161 if self.restr_attr is None: |
|
1162 self.restr_attr = rtype |
|
1163 if self.restr_attr_type is None: |
|
1164 attrtypes = set(obj for subj,obj in rschema.rdefs) |
|
1165 if len(attrtypes) > 1: |
|
1166 raise Exception('ambigous attribute %s, specify attrtype on %s' |
|
1167 % (rtype, self.__class__)) |
|
1168 self.restr_attr_type = next(iter(attrtypes)) |
|
1169 if skipattrfilter: |
|
1170 actual_filter_variable = subject |
|
1171 continue |
|
1172 subjvar = _get_var(self.select, subject, varmap) |
|
1173 objvar = _get_var(self.select, object, varmap) |
|
1174 rel = nodes.make_relation(subjvar, rtype, (objvar,), |
|
1175 nodes.VariableRef) |
|
1176 self.select.add_restriction(rel) |
|
1177 if self.restr_attr is None: |
|
1178 self.restr_attr = 'eid' |
|
1179 if self.restr_attr_type is None: |
|
1180 self.restr_attr_type = 'Int' |
|
1181 if actual_filter_variable: |
|
1182 restrvar = varmap[actual_filter_variable] |
|
1183 else: |
|
1184 restrvar = varmap[self.filter_variable] |
|
1185 return varmap, restrvar |
|
1186 |
|
1187 |
|
1188 class RangeFacet(AttributeFacet): |
|
1189 """This class allows to filter entities according to an attribute of |
|
1190 numerical type. |
|
1191 |
|
1192 It displays a slider using `jquery`_ to choose a lower bound and an upper |
|
1193 bound. |
|
1194 |
|
1195 The example below provides an alternative to the surface facet seen earlier, |
|
1196 in a more powerful way since |
|
1197 |
|
1198 * lower/upper boundaries are computed according to entities to filter |
|
1199 * user can specify lower/upper boundaries, not only the lower one |
|
1200 |
|
1201 .. sourcecode:: python |
|
1202 |
|
1203 class SurfaceFacet(RangeFacet): |
|
1204 __regid__ = 'surface' |
|
1205 __select__ = RangeFacet.__select__ & is_instance('Office') |
|
1206 # this facet is a filter on the office'surface |
|
1207 rtype = 'surface' |
|
1208 |
|
1209 All this with even less code! |
|
1210 |
|
1211 The image below display the rendering of the slider: |
|
1212 |
|
1213 .. image:: ../../images/facet_range.png |
|
1214 |
|
1215 .. _jquery: http://www.jqueryui.com/ |
|
1216 """ |
|
1217 target_attr_type = 'Float' # only numerical types are supported |
|
1218 needs_update = False # not supported actually |
|
1219 |
|
1220 @property |
|
1221 def wdgclass(self): |
|
1222 return FacetRangeWidget |
|
1223 |
|
1224 def _range_rset(self): |
|
1225 select = self.select |
|
1226 select.save_state() |
|
1227 try: |
|
1228 filtered_variable = self.filtered_variable |
|
1229 cleanup_select(select, filtered_variable) |
|
1230 newvar = _add_rtype_relation(select, filtered_variable, self.rtype, self.role)[0] |
|
1231 minf = nodes.Function('MIN') |
|
1232 minf.append(nodes.VariableRef(newvar)) |
|
1233 select.add_selected(minf) |
|
1234 maxf = nodes.Function('MAX') |
|
1235 maxf.append(nodes.VariableRef(newvar)) |
|
1236 select.add_selected(maxf) |
|
1237 # add is restriction if necessary |
|
1238 if filtered_variable.stinfo['typerel'] is None: |
|
1239 etypes = frozenset(sol[filtered_variable.name] for sol in select.solutions) |
|
1240 select.add_type_restriction(filtered_variable, etypes) |
|
1241 try: |
|
1242 return self.rqlexec(select.as_string(), self.cw_rset.args) |
|
1243 except Exception: |
|
1244 self.exception('error while getting vocabulary for %s, rql: %s', |
|
1245 self, select.as_string()) |
|
1246 return () |
|
1247 finally: |
|
1248 select.recover() |
|
1249 |
|
1250 def vocabulary(self): |
|
1251 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
|
1252 """ |
|
1253 rset = self._range_rset() |
|
1254 if rset: |
|
1255 minv, maxv = rset[0] |
|
1256 return [(text_type(minv), minv), (text_type(maxv), maxv)] |
|
1257 return [] |
|
1258 |
|
1259 def possible_values(self): |
|
1260 """Return a list of possible values (as string since it's used to |
|
1261 compare to a form value in javascript) for this facet. |
|
1262 """ |
|
1263 return [strval for strval, val in self.vocabulary()] |
|
1264 |
|
1265 def get_widget(self): |
|
1266 """return the widget instance to use to display this facet""" |
|
1267 values = set(value for _, value in self.vocabulary() if value is not None) |
|
1268 # Rset with entities (the facet is selected) but without values |
|
1269 if len(values) < 2: |
|
1270 return None |
|
1271 return self.wdgclass(self, min(values), max(values)) |
|
1272 |
|
1273 def formatvalue(self, value): |
|
1274 """format `value` before in order to insert it in the RQL query""" |
|
1275 return text_type(value) |
|
1276 |
|
1277 def infvalue(self, min=False): |
|
1278 if min: |
|
1279 return self._cw.form.get('min_%s_inf' % self.__regid__) |
|
1280 return self._cw.form.get('%s_inf' % self.__regid__) |
|
1281 |
|
1282 def supvalue(self, max=False): |
|
1283 if max: |
|
1284 return self._cw.form.get('max_%s_sup' % self.__regid__) |
|
1285 return self._cw.form.get('%s_sup' % self.__regid__) |
|
1286 |
|
1287 def add_rql_restrictions(self): |
|
1288 infvalue = self.infvalue() |
|
1289 supvalue = self.supvalue() |
|
1290 if infvalue is None or supvalue is None: # nothing sent |
|
1291 return |
|
1292 # when a value is equal to one of the limit, don't add the restriction, |
|
1293 # else we filter out NULL values implicitly |
|
1294 if infvalue != self.infvalue(min=True): |
|
1295 self._add_restriction(infvalue, '>=') |
|
1296 if supvalue != self.supvalue(max=True): |
|
1297 self._add_restriction(supvalue, '<=') |
|
1298 |
|
1299 def _add_restriction(self, value, operator): |
|
1300 self.select.add_constant_restriction(self.filtered_variable, |
|
1301 self.rtype, |
|
1302 self.formatvalue(value), |
|
1303 self.target_attr_type, operator) |
|
1304 |
|
1305 |
|
1306 class DateRangeFacet(RangeFacet): |
|
1307 """This class works similarly as the :class:`RangeFacet` but for attribute |
|
1308 of date type. |
|
1309 |
|
1310 The image below display the rendering of the slider for a date range: |
|
1311 |
|
1312 .. image:: ../../images/facet_date_range.png |
|
1313 """ |
|
1314 target_attr_type = 'Date' # only date types are supported |
|
1315 |
|
1316 @property |
|
1317 def wdgclass(self): |
|
1318 return DateFacetRangeWidget |
|
1319 |
|
1320 def formatvalue(self, value): |
|
1321 """format `value` before in order to insert it in the RQL query""" |
|
1322 try: |
|
1323 date_value = ticks2datetime(float(value)) |
|
1324 except (ValueError, OverflowError): |
|
1325 return u'"date out-of-range"' |
|
1326 return '"%s"' % ustrftime(date_value, '%Y/%m/%d') |
|
1327 |
|
1328 |
|
1329 class AbstractRangeRQLPathFacet(RQLPathFacet): |
|
1330 """ |
|
1331 The :class:`AbstractRangeRQLPathFacet` is the base class for |
|
1332 RQLPathFacet-type facets allowing the use of RangeWidgets-like |
|
1333 widgets (such as (:class:`FacetRangeWidget`, |
|
1334 class:`DateFacetRangeWidget`) on the parent :class:`RQLPathFacet` |
|
1335 target attribute. |
|
1336 """ |
|
1337 __abstract__ = True |
|
1338 |
|
1339 def vocabulary(self): |
|
1340 """return vocabulary for this facet, eg a list of (label, |
|
1341 value)""" |
|
1342 select = self.select |
|
1343 select.save_state() |
|
1344 try: |
|
1345 filtered_variable = self.filtered_variable |
|
1346 cleanup_select(select, filtered_variable) |
|
1347 varmap, restrvar = self.add_path_to_select() |
|
1348 if self.label_variable: |
|
1349 attrvar = varmap[self.label_variable] |
|
1350 else: |
|
1351 attrvar = restrvar |
|
1352 # start RangeRQLPathFacet |
|
1353 minf = nodes.Function('MIN') |
|
1354 minf.append(nodes.VariableRef(restrvar)) |
|
1355 select.add_selected(minf) |
|
1356 maxf = nodes.Function('MAX') |
|
1357 maxf.append(nodes.VariableRef(restrvar)) |
|
1358 select.add_selected(maxf) |
|
1359 # add is restriction if necessary |
|
1360 if filtered_variable.stinfo['typerel'] is None: |
|
1361 etypes = frozenset(sol[filtered_variable.name] for sol in select.solutions) |
|
1362 select.add_type_restriction(filtered_variable, etypes) |
|
1363 # end RangeRQLPathFacet |
|
1364 try: |
|
1365 rset = self.rqlexec(select.as_string(), self.cw_rset.args) |
|
1366 except Exception: |
|
1367 self.exception('error while getting vocabulary for %s, rql: %s', |
|
1368 self, select.as_string()) |
|
1369 return () |
|
1370 finally: |
|
1371 select.recover() |
|
1372 # don't call rset_vocabulary on empty result set, it may be an empty |
|
1373 # *list* (see rqlexec implementation) |
|
1374 if rset: |
|
1375 minv, maxv = rset[0] |
|
1376 return [(text_type(minv), minv), (text_type(maxv), maxv)] |
|
1377 return [] |
|
1378 |
|
1379 |
|
1380 def possible_values(self): |
|
1381 """return a list of possible values (as string since it's used to |
|
1382 compare to a form value in javascript) for this facet |
|
1383 """ |
|
1384 return [strval for strval, val in self.vocabulary()] |
|
1385 |
|
1386 def add_rql_restrictions(self): |
|
1387 infvalue = self.infvalue() |
|
1388 supvalue = self.supvalue() |
|
1389 if infvalue is None or supvalue is None: # nothing sent |
|
1390 return |
|
1391 varmap, restrvar = self.add_path_to_select( |
|
1392 skiplabel=True, skipattrfilter=True) |
|
1393 restrel = None |
|
1394 for part in self.path: |
|
1395 if isinstance(part, string_types): |
|
1396 part = part.split() |
|
1397 subject, rtype, object = part |
|
1398 if object == self.filter_variable: |
|
1399 restrel = rtype |
|
1400 assert restrel |
|
1401 # when a value is equal to one of the limit, don't add the restriction, |
|
1402 # else we filter out NULL values implicitly |
|
1403 if infvalue != self.infvalue(min=True): |
|
1404 |
|
1405 self._add_restriction(infvalue, '>=', restrvar, restrel) |
|
1406 if supvalue != self.supvalue(max=True): |
|
1407 self._add_restriction(supvalue, '<=', restrvar, restrel) |
|
1408 |
|
1409 def _add_restriction(self, value, operator, restrvar, restrel): |
|
1410 self.select.add_constant_restriction(restrvar, |
|
1411 restrel, |
|
1412 self.formatvalue(value), |
|
1413 self.target_attr_type, operator) |
|
1414 |
|
1415 |
|
1416 class RangeRQLPathFacet(AbstractRangeRQLPathFacet, RQLPathFacet): |
|
1417 """ |
|
1418 The :class:`RangeRQLPathFacet` uses the :class:`FacetRangeWidget` |
|
1419 on the :class:`AbstractRangeRQLPathFacet` target attribute |
|
1420 """ |
|
1421 pass |
|
1422 |
|
1423 |
|
1424 class DateRangeRQLPathFacet(AbstractRangeRQLPathFacet, DateRangeFacet): |
|
1425 """ |
|
1426 The :class:`DateRangeRQLPathFacet` uses the |
|
1427 :class:`DateFacetRangeWidget` on the |
|
1428 :class:`AbstractRangeRQLPathFacet` target attribute |
|
1429 """ |
|
1430 pass |
|
1431 |
|
1432 |
|
1433 class HasRelationFacet(AbstractFacet): |
|
1434 """This class simply filter according to the presence of a relation |
|
1435 (whatever the entity at the other end). It display a simple checkbox that |
|
1436 lets you refine your selection in order to get only entities that actually |
|
1437 have this relation. You simply have to define which relation using the |
|
1438 `rtype` and `role` attributes. |
|
1439 |
|
1440 Here is an example of the rendering of thos facet to filter book with image |
|
1441 and the corresponding code: |
|
1442 |
|
1443 .. image:: ../../images/facet_has_image.png |
|
1444 |
|
1445 .. sourcecode:: python |
|
1446 |
|
1447 class HasImageFacet(HasRelationFacet): |
|
1448 __regid__ = 'hasimage' |
|
1449 __select__ = HasRelationFacet.__select__ & is_instance('Book') |
|
1450 rtype = 'has_image' |
|
1451 role = 'subject' |
|
1452 """ |
|
1453 __select__ = partial_relation_possible() & match_context_prop() |
|
1454 rtype = None # override me in subclass |
|
1455 role = 'subject' # role of filtered entity in the relation |
|
1456 |
|
1457 title = property(rtype_facet_title) |
|
1458 needs_update = False # not supported actually |
|
1459 support_and = False |
|
1460 |
|
1461 def get_widget(self): |
|
1462 return CheckBoxFacetWidget(self._cw, self, |
|
1463 '%s:%s' % (self.rtype, self), |
|
1464 self._cw.form.get(self.__regid__)) |
|
1465 |
|
1466 def add_rql_restrictions(self): |
|
1467 """add restriction for this facet into the rql syntax tree""" |
|
1468 value = self._cw.form.get(self.__regid__) |
|
1469 if not value: # no value sent for this facet |
|
1470 return |
|
1471 exists = nodes.Exists() |
|
1472 self.select.add_restriction(exists) |
|
1473 var = self.select.make_variable() |
|
1474 if self.role == 'subject': |
|
1475 subj, obj = self.filtered_variable, var |
|
1476 else: |
|
1477 subj, obj = var, self.filtered_variable |
|
1478 exists.add_relation(subj, self.rtype, obj) |
|
1479 |
|
1480 |
|
1481 class BitFieldFacet(AttributeFacet): |
|
1482 """Base facet class for Int field holding some bit values using binary |
|
1483 masks. |
|
1484 |
|
1485 label / value for each bit should be given using the :attr:`choices` |
|
1486 attribute. |
|
1487 |
|
1488 See also :class:`~cubicweb.web.formwidgets.BitSelect`. |
|
1489 """ |
|
1490 choices = None # to be set on concret class |
|
1491 def add_rql_restrictions(self): |
|
1492 value = self._cw.form.get(self.__regid__) |
|
1493 if not value: |
|
1494 return |
|
1495 if isinstance(value, list): |
|
1496 value = reduce(lambda x, y: int(x) | int(y), value) |
|
1497 else: |
|
1498 value = int(value) |
|
1499 attr_var = self.select.make_variable() |
|
1500 self.select.add_relation(self.filtered_variable, self.rtype, attr_var) |
|
1501 comp = nodes.Comparison('=', nodes.Constant(value, 'Int')) |
|
1502 if value == 0: |
|
1503 comp.append(nodes.variable_ref(attr_var)) |
|
1504 else: |
|
1505 comp.append(nodes.MathExpression('&', nodes.variable_ref(attr_var), |
|
1506 nodes.Constant(value, 'Int'))) |
|
1507 having = self.select.having |
|
1508 if having: |
|
1509 self.select.replace(having[0], nodes.And(having[0], comp)) |
|
1510 else: |
|
1511 self.select.set_having([comp]) |
|
1512 |
|
1513 def rset_vocabulary(self, rset): |
|
1514 mask = reduce(lambda x, y: x | (y[0] or 0), rset, 0) |
|
1515 return sorted([(self._cw._(label), val) for label, val in self.choices |
|
1516 if not val or val & mask]) |
|
1517 |
|
1518 def possible_values(self): |
|
1519 return [text_type(val) for label, val in self.vocabulary()] |
|
1520 |
|
1521 |
|
1522 ## html widets ################################################################ |
|
1523 _DEFAULT_VOCAB_WIDGET_HEIGHT = 12 |
|
1524 _DEFAULT_FACET_GROUP_HEIGHT = 15 |
|
1525 |
|
1526 class FacetVocabularyWidget(htmlwidgets.HTMLWidget): |
|
1527 |
|
1528 def __init__(self, facet): |
|
1529 self.facet = facet |
|
1530 self.items = [] |
|
1531 |
|
1532 @cachedproperty |
|
1533 def css_overflow_limit(self): |
|
1534 """ we try to deduce a number of displayed lines from a css property |
|
1535 if we get another unit we're out of luck and resort to one constant |
|
1536 hence, it is strongly advised not to specify but ems for this css prop |
|
1537 """ |
|
1538 return css_em_num_value(self.facet._cw.vreg, 'facet_vocabMaxHeight', |
|
1539 _DEFAULT_VOCAB_WIDGET_HEIGHT) |
|
1540 |
|
1541 @cachedproperty |
|
1542 def height(self): |
|
1543 """ title, optional and/or dropdown, len(items) or upper limit """ |
|
1544 return (1.5 + # title + small magic constant |
|
1545 int(self.facet._support_and_compat() + |
|
1546 min(len(self.items), self.css_overflow_limit))) |
|
1547 |
|
1548 @property |
|
1549 @cached |
|
1550 def overflows(self): |
|
1551 return len(self.items) >= self.css_overflow_limit |
|
1552 |
|
1553 scrollbar_padding_factor = 4 |
|
1554 |
|
1555 def _render(self): |
|
1556 w = self.w |
|
1557 title = xml_escape(self.facet.title) |
|
1558 facetid = domid(make_uid(self.facet.__regid__)) |
|
1559 w(u'<div id="%s" class="facet">\n' % facetid) |
|
1560 cssclass = 'facetTitle' |
|
1561 if self.facet.allow_hide: |
|
1562 cssclass += ' hideFacetBody' |
|
1563 w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' % |
|
1564 (cssclass, xml_escape(self.facet.__regid__), title)) |
|
1565 if self.facet._support_and_compat(): |
|
1566 self._render_and_or(w) |
|
1567 cssclass = 'facetBody vocabularyFacet' |
|
1568 if not self.facet.start_unfolded: |
|
1569 cssclass += ' hidden' |
|
1570 overflow = self.overflows |
|
1571 if overflow: |
|
1572 if self.facet._support_and_compat(): |
|
1573 cssclass += ' vocabularyFacetBodyWithLogicalSelector' |
|
1574 else: |
|
1575 cssclass += ' vocabularyFacetBody' |
|
1576 w(u'<div class="%s">\n' % cssclass) |
|
1577 for value, label, selected in self.items: |
|
1578 if value is None: |
|
1579 continue |
|
1580 self._render_value(w, value, label, selected, overflow) |
|
1581 w(u'</div>\n') |
|
1582 w(u'</div>\n') |
|
1583 |
|
1584 def _render_and_or(self, w): |
|
1585 _ = self.facet._cw._ |
|
1586 w(u"""<select name='%s' class='radio facetOperator' title='%s'> |
|
1587 <option value='OR'>%s</option> |
|
1588 <option value='AND'>%s</option> |
|
1589 </select>""" % (xml_escape(self.facet.__regid__) + '_andor', |
|
1590 _('and/or between different values'), |
|
1591 _('OR'), _('AND'))) |
|
1592 |
|
1593 def _render_value(self, w, value, label, selected, overflow): |
|
1594 cssclass = 'facetValue facetCheckBox' |
|
1595 if selected: |
|
1596 cssclass += ' facetValueSelected' |
|
1597 w(u'<div class="%s" cubicweb:value="%s">\n' |
|
1598 % (cssclass, xml_escape(text_type(value)))) |
|
1599 # If it is overflowed one must add padding to compensate for the vertical |
|
1600 # scrollbar; given current css values, 4 blanks work perfectly ... |
|
1601 padding = u' ' * self.scrollbar_padding_factor if overflow else u'' |
|
1602 w('<span>%s</span>' % xml_escape(label)) |
|
1603 w(padding) |
|
1604 w(u'</div>') |
|
1605 |
|
1606 class FacetStringWidget(htmlwidgets.HTMLWidget): |
|
1607 def __init__(self, facet): |
|
1608 self.facet = facet |
|
1609 self.value = None |
|
1610 |
|
1611 @property |
|
1612 def height(self): |
|
1613 return 2.5 |
|
1614 |
|
1615 def _render(self): |
|
1616 w = self.w |
|
1617 title = xml_escape(self.facet.title) |
|
1618 facetid = make_uid(self.facet.__regid__) |
|
1619 w(u'<div id="%s" class="facet">\n' % facetid) |
|
1620 cssclass = 'facetTitle' |
|
1621 if self.facet.allow_hide: |
|
1622 cssclass += ' hideFacetBody' |
|
1623 w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' % |
|
1624 (cssclass, xml_escape(self.facet.__regid__), title)) |
|
1625 cssclass = 'facetBody' |
|
1626 if not self.facet.start_unfolded: |
|
1627 cssclass += ' hidden' |
|
1628 w(u'<div class="%s">\n' % cssclass) |
|
1629 w(u'<input name="%s" type="text" value="%s" />\n' % ( |
|
1630 xml_escape(self.facet.__regid__), self.value or u'')) |
|
1631 w(u'</div>\n') |
|
1632 w(u'</div>\n') |
|
1633 |
|
1634 |
|
1635 class FacetRangeWidget(htmlwidgets.HTMLWidget): |
|
1636 formatter = 'function (value) {return value;}' |
|
1637 onload = u''' |
|
1638 var _formatter = %(formatter)s; |
|
1639 jQuery("#%(sliderid)s").slider({ |
|
1640 range: true, |
|
1641 min: %(minvalue)s, |
|
1642 max: %(maxvalue)s, |
|
1643 values: [%(minvalue)s, %(maxvalue)s], |
|
1644 stop: function(event, ui) { // submit when the user stops sliding |
|
1645 var form = $('#%(sliderid)s').closest('form'); |
|
1646 buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs'))); |
|
1647 }, |
|
1648 slide: function(event, ui) { |
|
1649 jQuery('#%(sliderid)s_inf').html(_formatter(ui.values[0])); |
|
1650 jQuery('#%(sliderid)s_sup').html(_formatter(ui.values[1])); |
|
1651 jQuery('input[name="%(facetname)s_inf"]').val(ui.values[0]); |
|
1652 jQuery('input[name="%(facetname)s_sup"]').val(ui.values[1]); |
|
1653 } |
|
1654 }); |
|
1655 // use JS formatter to format value on page load |
|
1656 jQuery('#%(sliderid)s_inf').html(_formatter(jQuery('input[name="%(facetname)s_inf"]').val())); |
|
1657 jQuery('#%(sliderid)s_sup').html(_formatter(jQuery('input[name="%(facetname)s_sup"]').val())); |
|
1658 ''' |
|
1659 #'# make emacs happier |
|
1660 def __init__(self, facet, minvalue, maxvalue): |
|
1661 self.facet = facet |
|
1662 self.minvalue = minvalue |
|
1663 self.maxvalue = maxvalue |
|
1664 |
|
1665 @property |
|
1666 def height(self): |
|
1667 return 2.5 |
|
1668 |
|
1669 def _render(self): |
|
1670 w = self.w |
|
1671 facet = self.facet |
|
1672 facet._cw.add_js('jquery.ui.js') |
|
1673 facet._cw.add_css('jquery.ui.css') |
|
1674 sliderid = make_uid('theslider') |
|
1675 facetname = self.facet.__regid__ |
|
1676 facetid = make_uid(facetname) |
|
1677 facet._cw.html_headers.add_onload(self.onload % { |
|
1678 'sliderid': sliderid, |
|
1679 'facetid': facetid, |
|
1680 'facetname': facetname, |
|
1681 'minvalue': self.minvalue, |
|
1682 'maxvalue': self.maxvalue, |
|
1683 'formatter': self.formatter, |
|
1684 }) |
|
1685 title = xml_escape(self.facet.title) |
|
1686 facetname = xml_escape(facetname) |
|
1687 w(u'<div id="%s" class="facet rangeFacet">\n' % facetid) |
|
1688 cssclass = 'facetTitle' |
|
1689 if facet.allow_hide: |
|
1690 cssclass += ' hideFacetBody' |
|
1691 w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' % |
|
1692 (cssclass, facetname, title)) |
|
1693 cssclass = 'facetBody' |
|
1694 if not self.facet.start_unfolded: |
|
1695 cssclass += ' hidden' |
|
1696 w(u'<div class="%s">\n' % cssclass) |
|
1697 w(u'<span id="%s_inf"></span> - <span id="%s_sup"></span>' |
|
1698 % (sliderid, sliderid)) |
|
1699 w(u'<input type="hidden" name="%s_inf" value="%s" />' |
|
1700 % (facetname, self.minvalue)) |
|
1701 w(u'<input type="hidden" name="%s_sup" value="%s" />' |
|
1702 % (facetname, self.maxvalue)) |
|
1703 w(u'<input type="hidden" name="min_%s_inf" value="%s" />' |
|
1704 % (facetname, self.minvalue)) |
|
1705 w(u'<input type="hidden" name="max_%s_sup" value="%s" />' |
|
1706 % (facetname, self.maxvalue)) |
|
1707 w(u'<div id="%s"></div>' % sliderid) |
|
1708 w(u'</div>\n') |
|
1709 w(u'</div>\n') |
|
1710 |
|
1711 |
|
1712 class DateFacetRangeWidget(FacetRangeWidget): |
|
1713 |
|
1714 formatter = 'function (value) {return (new Date(parseFloat(value))).strftime(DATE_FMT);}' |
|
1715 |
|
1716 def round_max_value(self, d): |
|
1717 'round to upper value to avoid filtering out the max value' |
|
1718 return datetime(d.year, d.month, d.day) + timedelta(days=1) |
|
1719 |
|
1720 def __init__(self, facet, minvalue, maxvalue): |
|
1721 maxvalue = self.round_max_value(maxvalue) |
|
1722 super(DateFacetRangeWidget, self).__init__(facet, |
|
1723 datetime2ticks(minvalue), |
|
1724 datetime2ticks(maxvalue)) |
|
1725 fmt = facet._cw.property_value('ui.date-format') |
|
1726 facet._cw.html_headers.define_var('DATE_FMT', fmt) |
|
1727 |
|
1728 |
|
1729 class CheckBoxFacetWidget(htmlwidgets.HTMLWidget): |
|
1730 selected_img = "black-check.png" |
|
1731 unselected_img = "black-uncheck.png" |
|
1732 |
|
1733 def __init__(self, req, facet, value, selected): |
|
1734 self._cw = req |
|
1735 self.facet = facet |
|
1736 self.value = value |
|
1737 self.selected = selected |
|
1738 |
|
1739 @property |
|
1740 def height(self): |
|
1741 return 1.5 |
|
1742 |
|
1743 def _render(self): |
|
1744 w = self.w |
|
1745 title = xml_escape(self.facet.title) |
|
1746 facetid = make_uid(self.facet.__regid__) |
|
1747 w(u'<div id="%s" class="facet">\n' % facetid) |
|
1748 cssclass = 'facetValue facetCheckBox' |
|
1749 if self.selected: |
|
1750 cssclass += ' facetValueSelected' |
|
1751 imgsrc = self._cw.data_url(self.selected_img) |
|
1752 imgalt = self._cw._('selected') |
|
1753 else: |
|
1754 imgsrc = self._cw.data_url(self.unselected_img) |
|
1755 imgalt = self._cw._('not selected') |
|
1756 w(u'<div class="%s" cubicweb:value="%s">\n' |
|
1757 % (cssclass, xml_escape(text_type(self.value)))) |
|
1758 w(u'<div>') |
|
1759 w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" /> ' % (imgsrc, imgalt)) |
|
1760 w(u'<label class="facetTitle" cubicweb:facetName="%s">%s</label>' |
|
1761 % (xml_escape(self.facet.__regid__), title)) |
|
1762 w(u'</div>\n') |
|
1763 w(u'</div>\n') |
|
1764 w(u'</div>\n') |
|
1765 |
|
1766 |
|
1767 # other classes ################################################################ |
|
1768 |
|
1769 class FilterRQLBuilder(object): |
|
1770 """called by javascript to get a rql string from filter form""" |
|
1771 |
|
1772 def __init__(self, req): |
|
1773 self._cw = req |
|
1774 |
|
1775 def build_rql(self): |
|
1776 form = self._cw.form |
|
1777 facetids = form['facets'].split(',') |
|
1778 # XXX Union unsupported yet |
|
1779 select = self._cw.vreg.parse(self._cw, form['baserql']).children[0] |
|
1780 filtered_variable = get_filtered_variable(select, form.get('mainvar')) |
|
1781 toupdate = [] |
|
1782 for facetid in facetids: |
|
1783 facet = get_facet(self._cw, facetid, select, filtered_variable) |
|
1784 facet.add_rql_restrictions() |
|
1785 if facet.needs_update: |
|
1786 toupdate.append(facetid) |
|
1787 return select.as_string(), toupdate |