27 |
27 |
28 ## rqlst manipulation functions used by facets ################################ |
28 ## rqlst manipulation functions used by facets ################################ |
29 |
29 |
30 def prepare_facets_rqlst(rqlst, args=None): |
30 def prepare_facets_rqlst(rqlst, args=None): |
31 """prepare a syntax tree to generate facet filters |
31 """prepare a syntax tree to generate facet filters |
32 |
32 |
33 * remove ORDERBY clause |
33 * remove ORDERBY clause |
34 * cleanup selection (remove everything) |
34 * cleanup selection (remove everything) |
35 * undefine unnecessary variables |
35 * undefine unnecessary variables |
36 * set DISTINCT |
36 * set DISTINCT |
37 * unset LIMIT/OFFSET |
37 * unset LIMIT/OFFSET |
62 |
62 |
63 |
63 |
64 def get_facet(req, facetid, rqlst, mainvar): |
64 def get_facet(req, facetid, rqlst, mainvar): |
65 return req.vreg.object_by_id('facets', facetid, req, rqlst=rqlst, |
65 return req.vreg.object_by_id('facets', facetid, req, rqlst=rqlst, |
66 filtered_variable=mainvar) |
66 filtered_variable=mainvar) |
67 |
67 |
68 |
68 |
69 def filter_hiddens(w, **kwargs): |
69 def filter_hiddens(w, **kwargs): |
70 for key, val in kwargs.items(): |
70 for key, val in kwargs.items(): |
71 w(u'<input type="hidden" name="%s" value="%s" />' % ( |
71 w(u'<input type="hidden" name="%s" value="%s" />' % ( |
72 key, html_escape(val))) |
72 key, html_escape(val))) |
137 newvar, rel = _add_rtype_relation(rqlst, mainvar, rtype, role) |
137 newvar, rel = _add_rtype_relation(rqlst, mainvar, rtype, role) |
138 if rqlst.groupby: |
138 if rqlst.groupby: |
139 rqlst.add_group_var(newvar) |
139 rqlst.add_group_var(newvar) |
140 rqlst.add_selected(newvar) |
140 rqlst.add_selected(newvar) |
141 return newvar, rel |
141 return newvar, rel |
142 |
142 |
143 def _remove_relation(rqlst, rel, var): |
143 def _remove_relation(rqlst, rel, var): |
144 """remove a constraint relation from the syntax tree""" |
144 """remove a constraint relation from the syntax tree""" |
145 # remove the relation |
145 # remove the relation |
146 rqlst.remove_node(rel) |
146 rqlst.remove_node(rel) |
147 # remove relations where the filtered variable appears on the |
147 # remove relations where the filtered variable appears on the |
227 # and have no path to the main variable |
227 # and have no path to the main variable |
228 for ovarname in linkedvars: |
228 for ovarname in linkedvars: |
229 if ovarname == mainvar.name: |
229 if ovarname == mainvar.name: |
230 continue |
230 continue |
231 if not has_path(vargraph, ovarname, mainvar.name): |
231 if not has_path(vargraph, ovarname, mainvar.name): |
232 toremove.add(rqlst.defined_vars[ovarname]) |
232 toremove.add(rqlst.defined_vars[ovarname]) |
233 |
233 |
234 |
234 |
235 |
235 |
236 ## base facet classes ######################################################### |
236 ## base facet classes ######################################################### |
237 class AbstractFacet(AcceptMixIn, AppRsetObject): |
237 class AbstractFacet(AcceptMixIn, AppRsetObject): |
238 __registerer__ = priority_registerer |
238 __registerer__ = priority_registerer |
239 __abstract__ = True |
239 __abstract__ = True |
240 __registry__ = 'facets' |
240 __registry__ = 'facets' |
250 } |
250 } |
251 visible = True |
251 visible = True |
252 context = '' |
252 context = '' |
253 needs_update = False |
253 needs_update = False |
254 start_unfolded = True |
254 start_unfolded = True |
255 |
255 |
256 @classmethod |
256 @classmethod |
257 def selected(cls, req, rset=None, rqlst=None, context=None, |
257 def selected(cls, req, rset=None, rqlst=None, context=None, |
258 filtered_variable=None): |
258 filtered_variable=None): |
259 assert rset is not None or rqlst is not None |
259 assert rset is not None or rqlst is not None |
260 assert filtered_variable |
260 assert filtered_variable |
278 |
278 |
279 @property |
279 @property |
280 def operator(self): |
280 def operator(self): |
281 # OR between selected values by default |
281 # OR between selected values by default |
282 return self.req.form.get(self.id + '_andor', 'OR') |
282 return self.req.form.get(self.id + '_andor', 'OR') |
283 |
283 |
284 def get_widget(self): |
284 def get_widget(self): |
285 """return the widget instance to use to display this facet |
285 """return the widget instance to use to display this facet |
286 """ |
286 """ |
287 raise NotImplementedError |
287 raise NotImplementedError |
288 |
288 |
289 def add_rql_restrictions(self): |
289 def add_rql_restrictions(self): |
290 """add restriction for this facet into the rql syntax tree""" |
290 """add restriction for this facet into the rql syntax tree""" |
291 raise NotImplementedError |
291 raise NotImplementedError |
292 |
292 |
293 |
293 |
294 class VocabularyFacet(AbstractFacet): |
294 class VocabularyFacet(AbstractFacet): |
295 needs_update = True |
295 needs_update = True |
296 |
296 |
297 def get_widget(self): |
297 def get_widget(self): |
298 """return the widget instance to use to display this facet |
298 """return the widget instance to use to display this facet |
299 |
299 |
300 default implentation expects a .vocabulary method on the facet and |
300 default implentation expects a .vocabulary method on the facet and |
301 return a combobox displaying this vocabulary |
301 return a combobox displaying this vocabulary |
309 if value is None: |
309 if value is None: |
310 wdg.append(FacetSeparator(label)) |
310 wdg.append(FacetSeparator(label)) |
311 else: |
311 else: |
312 wdg.append(FacetItem(self.req, label, value, value in selected)) |
312 wdg.append(FacetItem(self.req, label, value, value in selected)) |
313 return wdg |
313 return wdg |
314 |
314 |
315 def vocabulary(self): |
315 def vocabulary(self): |
316 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
316 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
317 """ |
317 """ |
318 raise NotImplementedError |
318 raise NotImplementedError |
319 |
319 |
320 def possible_values(self): |
320 def possible_values(self): |
321 """return a list of possible values (as string since it's used to |
321 """return a list of possible values (as string since it's used to |
322 compare to a form value in javascript) for this facet |
322 compare to a form value in javascript) for this facet |
323 """ |
323 """ |
324 raise NotImplementedError |
324 raise NotImplementedError |
325 |
325 |
326 def support_and(self): |
326 def support_and(self): |
327 return False |
327 return False |
328 |
328 |
329 def rqlexec(self, rql, args=None, cachekey=None): |
329 def rqlexec(self, rql, args=None, cachekey=None): |
330 try: |
330 try: |
331 return self.req.execute(rql, args, cachekey) |
331 return self.req.execute(rql, args, cachekey) |
332 except Unauthorized: |
332 except Unauthorized: |
333 return [] |
333 return [] |
334 |
334 |
335 |
335 |
336 class RelationFacet(VocabularyFacet): |
336 class RelationFacet(VocabularyFacet): |
337 __selectors__ = (one_has_relation, match_context_prop) |
337 __selectors__ = (one_has_relation, match_context_prop) |
338 # class attributes to configure the relation facet |
338 # class attributes to configure the relation facet |
339 rtype = None |
339 rtype = None |
342 # set this to a stored procedure name if you want to sort on the result of |
342 # set this to a stored procedure name if you want to sort on the result of |
343 # this function's result instead of direct value |
343 # this function's result instead of direct value |
344 sortfunc = None |
344 sortfunc = None |
345 # ascendant/descendant sorting |
345 # ascendant/descendant sorting |
346 sortasc = True |
346 sortasc = True |
347 |
347 |
348 @property |
348 @property |
349 def title(self): |
349 def title(self): |
350 return display_name(self.req, self.rtype, form=self.role) |
350 return display_name(self.req, self.rtype, form=self.role) |
351 |
351 |
352 def vocabulary(self): |
352 def vocabulary(self): |
353 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
353 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
354 """ |
354 """ |
355 rqlst = self.rqlst |
355 rqlst = self.rqlst |
365 self, rqlst.as_string()) |
365 self, rqlst.as_string()) |
366 return () |
366 return () |
367 finally: |
367 finally: |
368 rqlst.recover() |
368 rqlst.recover() |
369 return self.rset_vocabulary(rset) |
369 return self.rset_vocabulary(rset) |
370 |
370 |
371 def possible_values(self): |
371 def possible_values(self): |
372 """return a list of possible values (as string since it's used to |
372 """return a list of possible values (as string since it's used to |
373 compare to a form value in javascript) for this facet |
373 compare to a form value in javascript) for this facet |
374 """ |
374 """ |
375 rqlst = self.rqlst |
375 rqlst = self.rqlst |
378 _cleanup_rqlst(rqlst, self.filtered_variable) |
378 _cleanup_rqlst(rqlst, self.filtered_variable) |
379 _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype, self.role) |
379 _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype, self.role) |
380 return [str(x) for x, in self.rqlexec(rqlst.as_string())] |
380 return [str(x) for x, in self.rqlexec(rqlst.as_string())] |
381 finally: |
381 finally: |
382 rqlst.recover() |
382 rqlst.recover() |
383 |
383 |
384 def rset_vocabulary(self, rset): |
384 def rset_vocabulary(self, rset): |
385 _ = self.req._ |
385 _ = self.req._ |
386 return [(_(label), eid) for eid, label in rset] |
386 return [(_(label), eid) for eid, label in rset] |
387 |
387 |
388 @cached |
388 @cached |
430 |
430 |
431 |
431 |
432 class AttributeFacet(RelationFacet): |
432 class AttributeFacet(RelationFacet): |
433 # attribute type |
433 # attribute type |
434 attrtype = 'String' |
434 attrtype = 'String' |
435 |
435 # type of comparison: default is an exact match on the attribute value |
|
436 comparator = '=' # could be '<', '<=', '>', '>=' |
|
437 |
436 def vocabulary(self): |
438 def vocabulary(self): |
437 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
439 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
438 """ |
440 """ |
439 rqlst = self.rqlst |
441 rqlst = self.rqlst |
440 rqlst.save_state() |
442 rqlst.save_state() |
450 self, rqlst.as_string()) |
452 self, rqlst.as_string()) |
451 return () |
453 return () |
452 finally: |
454 finally: |
453 rqlst.recover() |
455 rqlst.recover() |
454 return self.rset_vocabulary(rset) |
456 return self.rset_vocabulary(rset) |
455 |
457 |
456 def rset_vocabulary(self, rset): |
458 def rset_vocabulary(self, rset): |
457 _ = self.req._ |
459 _ = self.req._ |
458 return [(_(value), value) for value, in rset] |
460 return [(_(value), value) for value, in rset] |
459 |
461 |
460 def support_and(self): |
462 def support_and(self): |
461 return False |
463 return False |
462 |
464 |
463 def add_rql_restrictions(self): |
465 def add_rql_restrictions(self): |
464 """add restriction for this facet into the rql syntax tree""" |
466 """add restriction for this facet into the rql syntax tree""" |
465 value = self.req.form.get(self.id) |
467 value = self.req.form.get(self.id) |
466 if not value: |
468 if not value: |
467 return |
469 return |
468 mainvar = self.filtered_variable |
470 mainvar = self.filtered_variable |
469 self.rqlst.add_constant_restriction(mainvar, self.rtype, value, |
471 self.rqlst.add_constant_restriction(mainvar, self.rtype, value, |
470 self.attrtype) |
472 self.attrtype, self.comparator) |
471 |
473 |
472 |
474 |
473 |
475 |
474 class FilterRQLBuilder(object): |
476 class FilterRQLBuilder(object): |
475 """called by javascript to get a rql string from filter form""" |
477 """called by javascript to get a rql string from filter form""" |
476 |
478 |
477 def __init__(self, req): |
479 def __init__(self, req): |
478 self.req = req |
480 self.req = req |
479 |
481 |
480 def build_rql(self):#, tablefilter=False): |
482 def build_rql(self):#, tablefilter=False): |
481 form = self.req.form |
483 form = self.req.form |
482 facetids = form['facets'].split(',') |
484 facetids = form['facets'].split(',') |
483 select = parse(form['baserql']).children[0] # XXX Union unsupported yet |
485 select = parse(form['baserql']).children[0] # XXX Union unsupported yet |
484 mainvar = filtered_variable(select) |
486 mainvar = filtered_variable(select) |
488 facet.add_rql_restrictions() |
490 facet.add_rql_restrictions() |
489 if facet.needs_update: |
491 if facet.needs_update: |
490 toupdate.append(facetid) |
492 toupdate.append(facetid) |
491 return select.as_string(), toupdate |
493 return select.as_string(), toupdate |
492 |
494 |
493 |
495 |
494 ## html widets ################################################################ |
496 ## html widets ################################################################ |
495 |
497 |
496 class FacetVocabularyWidget(HTMLWidget): |
498 class FacetVocabularyWidget(HTMLWidget): |
497 |
499 |
498 def __init__(self, facet): |
500 def __init__(self, facet): |
499 self.facet = facet |
501 self.facet = facet |
500 self.items = [] |
502 self.items = [] |
501 |
503 |
502 def append(self, item): |
504 def append(self, item): |
503 self.items.append(item) |
505 self.items.append(item) |
504 |
506 |
505 def _render(self): |
507 def _render(self): |
506 title = html_escape(self.facet.title) |
508 title = html_escape(self.facet.title) |
507 facetid = html_escape(self.facet.id) |
509 facetid = html_escape(self.facet.id) |
508 if len(self.items) > 6: |
510 if len(self.items) > 6: |
509 self.w(u'<div id="%s" class="facet overflowed">\n' % facetid) |
511 self.w(u'<div id="%s" class="facet overflowed">\n' % facetid) |
558 if self.selected: |
560 if self.selected: |
559 cssclass = ' facetValueSelected' |
561 cssclass = ' facetValueSelected' |
560 imgsrc = self.req.datadir_url + self.selected_img |
562 imgsrc = self.req.datadir_url + self.selected_img |
561 else: |
563 else: |
562 cssclass = '' |
564 cssclass = '' |
563 imgsrc = self.req.datadir_url + self.unselected_img |
565 imgsrc = self.req.datadir_url + self.unselected_img |
564 self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n' |
566 self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n' |
565 % (cssclass, html_escape(unicode(self.value)))) |
567 % (cssclass, html_escape(unicode(self.value)))) |
566 self.w(u'<img src="%s" /> ' % imgsrc) |
568 self.w(u'<img src="%s" /> ' % imgsrc) |
567 self.w(u'<a href="javascript: {}">%s</a>' % html_escape(self.label)) |
569 self.w(u'<a href="javascript: {}">%s</a>' % html_escape(self.label)) |
568 self.w(u'</div>') |
570 self.w(u'</div>') |
569 |
571 |
570 |
572 |
571 class FacetSeparator(HTMLWidget): |
573 class FacetSeparator(HTMLWidget): |
572 def __init__(self, label=None): |
574 def __init__(self, label=None): |
573 self.label = label or u' ' |
575 self.label = label or u' ' |
574 |
576 |
575 def _render(self): |
577 def _render(self): |
576 pass |
578 pass |
577 |
579 |