13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
14 # details. |
14 # details. |
15 # |
15 # |
16 # You should have received a copy of the GNU Lesser General Public License along |
16 # You should have received a copy of the GNU Lesser General Public License along |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """contains utility functions and some visual component to restrict results of |
18 """ |
19 a search |
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.RangeFacet |
|
34 .. autoclass:: cubicweb.web.facet.DateRangeFacet |
|
35 |
|
36 Classes for facets implementor |
|
37 ------------------------------ |
|
38 Unless you didn't find the class that does the job you want above, you may want |
|
39 to skip those classes... |
|
40 |
|
41 .. autoclass:: cubicweb.web.facet.AbstractFacet |
|
42 .. autoclass:: cubicweb.web.facet.VocabularyFacet |
|
43 |
|
44 .. comment: XXX widgets |
20 """ |
45 """ |
21 |
46 |
22 __docformat__ = "restructuredtext en" |
47 __docformat__ = "restructuredtext en" |
23 |
48 |
24 from copy import deepcopy |
49 from copy import deepcopy |
266 continue |
291 continue |
267 if not has_path(vargraph, ovarname, mainvar.name): |
292 if not has_path(vargraph, ovarname, mainvar.name): |
268 toremove.add(rqlst.defined_vars[ovarname]) |
293 toremove.add(rqlst.defined_vars[ovarname]) |
269 |
294 |
270 |
295 |
271 |
296 ## base facet classes ########################################################## |
272 ## base facet classes ######################################################### |
297 |
273 class AbstractFacet(AppObject): |
298 class AbstractFacet(AppObject): |
|
299 """Abstract base class for all facets. Facets are stored in their own |
|
300 'facets' registry. They are similar to contextual components since the use |
|
301 the following configurable properties: |
|
302 |
|
303 * `visible`, boolean flag telling if a facet should be displayed or not |
|
304 |
|
305 * `order`, integer to control facets display order |
|
306 |
|
307 * `context`, telling if a facet should be displayed in the table form filter |
|
308 (context = 'tablefilter') or in the facet box (context = 'facetbox') or in |
|
309 both (context = '') |
|
310 |
|
311 The following methods define the facet API: |
|
312 |
|
313 .. automethod:: cubicweb.web.facet.AbstractFacet.get_widget |
|
314 .. automethod:: cubicweb.web.facet.AbstractFacet.add_rql_restrictions |
|
315 |
|
316 Facets will have the following attributes set (beside the standard |
|
317 :class:`~cubicweb.appobject.AppObject` ones): |
|
318 |
|
319 * `rqlst`, the rql syntax tree being facetted |
|
320 |
|
321 * `filtered_variable`, the variable node in this rql syntax tree that we're |
|
322 interested in filtering |
|
323 |
|
324 Facets implementors may also be interested in the following properties / |
|
325 methods: |
|
326 |
|
327 .. automethod:: cubicweb.web.facet.AbstractFacet.operator |
|
328 .. automethod:: cubicweb.web.facet.AbstractFacet.rqlexec |
|
329 """ |
274 __abstract__ = True |
330 __abstract__ = True |
275 __registry__ = 'facets' |
331 __registry__ = 'facets' |
276 cw_property_defs = { |
332 cw_property_defs = { |
277 _('visible'): dict(type='Boolean', default=True, |
333 _('visible'): dict(type='Boolean', default=True, |
278 help=_('display the facet or not')), |
334 help=_('display the facet or not')), |
300 self.rqlst = rqlst |
356 self.rqlst = rqlst |
301 self.filtered_variable = filtered_variable |
357 self.filtered_variable = filtered_variable |
302 |
358 |
303 @property |
359 @property |
304 def operator(self): |
360 def operator(self): |
|
361 """Return the operator (AND or OR) to use for this facet when multiple |
|
362 values are selected. |
|
363 """ |
305 # OR between selected values by default |
364 # OR between selected values by default |
306 return self._cw.form.get(self.__regid__ + '_andor', 'OR') |
365 return self._cw.form.get(self.__regid__ + '_andor', 'OR') |
307 |
366 |
|
367 def rqlexec(self, rql, args=None): |
|
368 """Utility method to execute some rql queries, and simply returning an |
|
369 empty list if :exc:`Unauthorized` is raised. |
|
370 """ |
|
371 try: |
|
372 return self._cw.execute(rql, args) |
|
373 except Unauthorized: |
|
374 return [] |
|
375 |
308 def get_widget(self): |
376 def get_widget(self): |
309 """return the widget instance to use to display this facet |
377 """Return the widget instance to use to display this facet, or None if |
|
378 the facet can't do anything valuable (only one value in the vocabulary |
|
379 for instance). |
310 """ |
380 """ |
311 raise NotImplementedError |
381 raise NotImplementedError |
312 |
382 |
313 def add_rql_restrictions(self): |
383 def add_rql_restrictions(self): |
314 """add restriction for this facet into the rql syntax tree""" |
384 """When some facet criteria has been updated, this method is called to |
|
385 add restriction for this facet into the rql syntax tree. It should get |
|
386 back its value in form parameters, and modify the syntax tree |
|
387 (`self.rqlst`) accordingly. |
|
388 """ |
315 raise NotImplementedError |
389 raise NotImplementedError |
316 |
390 |
317 |
391 |
318 class VocabularyFacet(AbstractFacet): |
392 class VocabularyFacet(AbstractFacet): |
|
393 """This abstract class extend :class:`AbstractFacet` to use the |
|
394 :class:`FacetVocabularyWidget` as widget, suitable for facets that may |
|
395 restrict values according to a (usually computed) vocabulary. |
|
396 |
|
397 A class which inherits from VocabularyFacet must define at least these methods: |
|
398 |
|
399 .. automethod:: cubicweb.web.facet.VocabularyFacet.vocabulary |
|
400 .. automethod:: cubicweb.web.facet.VocabularyFacet.possible_values |
|
401 """ |
319 needs_update = True |
402 needs_update = True |
320 |
403 |
321 def get_widget(self): |
404 def get_widget(self): |
322 """return the widget instance to use to display this facet |
405 """Return the widget instance to use to display this facet. |
323 |
406 |
324 default implentation expects a .vocabulary method on the facet and |
407 This implementation expects a .vocabulary method on the facet and |
325 return a combobox displaying this vocabulary |
408 return a combobox displaying this vocabulary. |
326 """ |
409 """ |
327 vocab = self.vocabulary() |
410 vocab = self.vocabulary() |
328 if len(vocab) <= 1: |
411 if len(vocab) <= 1: |
329 return None |
412 return None |
330 wdg = FacetVocabularyWidget(self) |
413 wdg = FacetVocabularyWidget(self) |
335 else: |
418 else: |
336 wdg.append(FacetItem(self._cw, label, value, value in selected)) |
419 wdg.append(FacetItem(self._cw, label, value, value in selected)) |
337 return wdg |
420 return wdg |
338 |
421 |
339 def vocabulary(self): |
422 def vocabulary(self): |
340 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
423 """Return vocabulary for this facet, eg a list of 2-uple (label, value). |
341 """ |
424 """ |
342 raise NotImplementedError |
425 raise NotImplementedError |
343 |
426 |
344 def possible_values(self): |
427 def possible_values(self): |
345 """return a list of possible values (as string since it's used to |
428 """Return a list of possible values (as string since it's used to |
346 compare to a form value in javascript) for this facet |
429 compare to a form value in javascript) for this facet. |
347 """ |
430 """ |
348 raise NotImplementedError |
431 raise NotImplementedError |
349 |
432 |
350 def support_and(self): |
433 def support_and(self): |
351 return False |
434 return False |
352 |
|
353 def rqlexec(self, rql, args=None): |
|
354 try: |
|
355 return self._cw.execute(rql, args) |
|
356 except Unauthorized: |
|
357 return [] |
|
358 |
435 |
359 |
436 |
360 class RelationFacet(VocabularyFacet): |
437 class RelationFacet(VocabularyFacet): |
361 """Base facet to filter some entities according to other entities to which |
438 """Base facet to filter some entities according to other entities to which |
362 they are related. Create concret facet by inheriting from this class an then |
439 they are related. Create concret facet by inheriting from this class an then |
380 |
457 |
381 * `sortfunc`: set this to a stored procedure name if you want to sort on the |
458 * `sortfunc`: set this to a stored procedure name if you want to sort on the |
382 result of this function's result instead of direct value |
459 result of this function's result instead of direct value |
383 |
460 |
384 * `sortasc`: boolean flag to control ascendant/descendant sorting |
461 * `sortasc`: boolean flag to control ascendant/descendant sorting |
|
462 |
|
463 To illustrate this facet, let's take for example an *excerpt* of the schema |
|
464 of an office location search application: |
|
465 |
|
466 .. sourcecode:: python |
|
467 |
|
468 class Office(WorkflowableEntityType): |
|
469 price = Int(description='euros / m2 / HC / HT') |
|
470 surface = Int(description='m2') |
|
471 has_address = SubjectRelation('PostalAddress', |
|
472 cardinality='1?', |
|
473 composite='subject') |
|
474 proposed_by = SubjectRelation('Agency') |
|
475 |
|
476 |
|
477 We can simply define a facet to filter offices according to the agency |
|
478 proposing it: |
|
479 |
|
480 .. sourcecode:: python |
|
481 |
|
482 class AgencyFacet(RelationFacet): |
|
483 __regid__ = 'agency' |
|
484 # this facet should only be selected when visualizing offices |
|
485 __select__ = RelationFacet.__select__ & implements('Office') |
|
486 # this facet is a filter on the 'Agency' entities linked to the office |
|
487 # through the 'proposed_by' relation, where the office is the subject |
|
488 # of the relation |
|
489 rtype = 'has_address' |
|
490 # 'subject' is the default but setting it explicitly doesn't hurt... |
|
491 role = 'subject' |
|
492 # we want to display the agency's name |
|
493 target_attr = 'name' |
385 """ |
494 """ |
386 __select__ = partial_relation_possible() & match_context_prop() |
495 __select__ = partial_relation_possible() & match_context_prop() |
387 # class attributes to configure the relation facet |
496 # class attributes to configure the relation facet |
388 rtype = None |
497 rtype = None |
389 role = 'subject' |
498 role = 'subject' |
530 |
639 |
531 * you should specify the attribute type using `attrtype` if it's not a |
640 * you should specify the attribute type using `attrtype` if it's not a |
532 String |
641 String |
533 |
642 |
534 * you can specify a comparison operator using `comparator` |
643 * you can specify a comparison operator using `comparator` |
|
644 |
|
645 |
|
646 Back to our example... if you want to search office by postal code and that |
|
647 you use a :class:`RelationFacet` for that, you won't get the expected |
|
648 behaviour: if two offices have the same postal code, they've however two |
|
649 different addresses. So you'll see in the facet the same postal code twice, |
|
650 though linked to a different address entity. There is a great chance your |
|
651 users won't understand that... |
|
652 |
|
653 That's where this class come in ! It's used to said that you want to filter |
|
654 according to the *attribute value* of a relatied entity, not to the entity |
|
655 itself. Now here is the source code for the facet: |
|
656 |
|
657 .. sourcecode:: python |
|
658 |
|
659 class PostalCodeFacet(RelationAttributeFacet): |
|
660 __regid__ = 'postalcode' |
|
661 # this facet should only be selected when visualizing offices |
|
662 __select__ = RelationAttributeFacet.__select__ & implements('Office') |
|
663 # this facet is a filter on the PostalAddress entities linked to the |
|
664 # office through the 'has_address' relation, where the office is the |
|
665 # subject of the relation |
|
666 rtype = 'has_address' |
|
667 role = 'subject' |
|
668 # we want to search according to address 'postal_code' attribute |
|
669 target_attr = 'postalcode' |
535 """ |
670 """ |
536 _select_target_entity = False |
671 _select_target_entity = False |
537 # attribute type |
672 # attribute type |
538 attrtype = 'String' |
673 attrtype = 'String' |
539 # type of comparison: default is an exact match on the attribute value |
674 # type of comparison: default is an exact match on the attribute value |
577 """Base facet to filter some entities according one of their attribute. |
712 """Base facet to filter some entities according one of their attribute. |
578 Configuration is mostly similarly as :class:`RelationAttributeFacet`, except that: |
713 Configuration is mostly similarly as :class:`RelationAttributeFacet`, except that: |
579 |
714 |
580 * `target_attr` doesn't make sense here (you specify the attribute using `rtype` |
715 * `target_attr` doesn't make sense here (you specify the attribute using `rtype` |
581 * `role` neither, it's systematically 'subject' |
716 * `role` neither, it's systematically 'subject' |
582 """ |
717 |
|
718 So, suppose that in our office search example you want to refine search according |
|
719 to the office's surface. Here is a code snippet achieving this: |
|
720 |
|
721 .. sourcecode:: python |
|
722 |
|
723 class SurfaceFacet(AttributeFacet): |
|
724 __regid__ = 'surface' |
|
725 __select__ = AttributeFacet.__select__ & implements('Office') |
|
726 # this facet is a filter on the office'surface |
|
727 rtype = 'surface' |
|
728 # override the default value of operator since we want to filter |
|
729 # according to a minimal value, not an exact one |
|
730 comparator = '>=' |
|
731 |
|
732 def vocabulary(self): |
|
733 '''override the default vocabulary method since we want to |
|
734 hard-code our threshold values. |
|
735 |
|
736 Not overriding would generate a filter containing all existing |
|
737 surfaces defined in the database. |
|
738 ''' |
|
739 return [('> 200', '200'), ('> 250', '250'), |
|
740 ('> 275', '275'), ('> 300', '300')] |
|
741 """ |
|
742 |
583 _select_target_entity = True |
743 _select_target_entity = True |
584 |
744 |
585 def vocabulary(self): |
745 def vocabulary(self): |
586 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
746 """return vocabulary for this facet, eg a list of 2-uple (label, value) |
587 """ |
747 """ |
615 mainvar = self.filtered_variable |
775 mainvar = self.filtered_variable |
616 self.rqlst.add_constant_restriction(mainvar, self.rtype, value, |
776 self.rqlst.add_constant_restriction(mainvar, self.rtype, value, |
617 self.attrtype, self.comparator) |
777 self.attrtype, self.comparator) |
618 |
778 |
619 |
779 |
620 class FilterRQLBuilder(object): |
|
621 """called by javascript to get a rql string from filter form""" |
|
622 |
|
623 def __init__(self, req): |
|
624 self._cw = req |
|
625 |
|
626 def build_rql(self):#, tablefilter=False): |
|
627 form = self._cw.form |
|
628 facetids = form['facets'].split(',') |
|
629 select = self._cw.vreg.parse(self._cw, form['baserql']).children[0] # XXX Union unsupported yet |
|
630 mainvar = filtered_variable(select) |
|
631 toupdate = [] |
|
632 for facetid in facetids: |
|
633 facet = get_facet(self._cw, facetid, select, mainvar) |
|
634 facet.add_rql_restrictions() |
|
635 if facet.needs_update: |
|
636 toupdate.append(facetid) |
|
637 return select.as_string(), toupdate |
|
638 |
|
639 |
|
640 class RangeFacet(AttributeFacet): |
780 class RangeFacet(AttributeFacet): |
|
781 """This class allows to filter entities according to an attribute of |
|
782 numerical type. |
|
783 |
|
784 It displays a slider using `jquery`_ to choose a lower bound and an upper |
|
785 bound. |
|
786 |
|
787 The example below provides an alternative to the surface facet seen earlier, |
|
788 in a more powerful way since |
|
789 |
|
790 * lower/upper boundaries are computed according to entities to filter |
|
791 * user can specify lower/upper boundaries, not only the lower one |
|
792 |
|
793 .. sourcecode:: python |
|
794 |
|
795 class SurfaceFacet(RangeFacet): |
|
796 __regid__ = 'surface' |
|
797 __select__ = RangeFacet.__select__ & implements('Office') |
|
798 # this facet is a filter on the office'surface |
|
799 rtype = 'surface' |
|
800 |
|
801 All this with even less code! |
|
802 |
|
803 The image below display the rendering of the slider: |
|
804 |
|
805 .. image:: ../images/facet_range.png |
|
806 |
|
807 .. _jquery: http://www.jqueryui.com/ |
|
808 """ |
641 attrtype = 'Float' # only numerical types are supported |
809 attrtype = 'Float' # only numerical types are supported |
642 |
810 |
643 @property |
811 @property |
644 def wdgclass(self): |
812 def wdgclass(self): |
645 return FacetRangeWidget |
813 return FacetRangeWidget |
676 self.formatvalue(supvalue), |
844 self.formatvalue(supvalue), |
677 self.attrtype, '<=') |
845 self.attrtype, '<=') |
678 |
846 |
679 |
847 |
680 class DateRangeFacet(RangeFacet): |
848 class DateRangeFacet(RangeFacet): |
|
849 """This class works similarly as the :class:`RangeFacet` but for attribute |
|
850 of date type. |
|
851 |
|
852 The image below display the rendering of the slider for a date range: |
|
853 |
|
854 .. image:: ../images/facet_date_range.png |
|
855 """ |
681 attrtype = 'Date' # only date types are supported |
856 attrtype = 'Date' # only date types are supported |
682 |
857 |
683 @property |
858 @property |
684 def wdgclass(self): |
859 def wdgclass(self): |
685 return DateFacetRangeWidget |
860 return DateFacetRangeWidget |
688 """format `value` before in order to insert it in the RQL query""" |
863 """format `value` before in order to insert it in the RQL query""" |
689 return '"%s"' % date.fromtimestamp(float(value) / 1000).strftime('%Y/%m/%d') |
864 return '"%s"' % date.fromtimestamp(float(value) / 1000).strftime('%Y/%m/%d') |
690 |
865 |
691 |
866 |
692 class HasRelationFacet(AbstractFacet): |
867 class HasRelationFacet(AbstractFacet): |
|
868 """This class simply filter according to the presence of a relation |
|
869 (whatever the entity at the other end). It display a simple checkbox that |
|
870 lets you refine your selection in order to get only entities that actually |
|
871 have this relation. You simply have to define which relation using the |
|
872 `rtype` and `role` attributes. |
|
873 |
|
874 Here is an example of the rendering of thos facet to filter book with image |
|
875 and the corresponding code: |
|
876 |
|
877 .. image:: ../images/facet_has_image.png |
|
878 |
|
879 .. sourcecode:: python |
|
880 |
|
881 class HasImageFacet(HasRelationFacet): |
|
882 __regid__ = 'hasimage' |
|
883 __select__ = HasRelationFacet.__select__ & implements('Book') |
|
884 rtype = 'has_image' |
|
885 role = 'subject' |
|
886 """ |
693 rtype = None # override me in subclass |
887 rtype = None # override me in subclass |
694 role = 'subject' # role of filtered entity in the relation |
888 role = 'subject' # role of filtered entity in the relation |
695 |
889 |
696 @property |
890 @property |
697 def title(self): |
891 def title(self): |
906 def __init__(self, label=None): |
1100 def __init__(self, label=None): |
907 self.label = label or u' ' |
1101 self.label = label or u' ' |
908 |
1102 |
909 def _render(self): |
1103 def _render(self): |
910 pass |
1104 pass |
|
1105 |
|
1106 # other classes ################################################################ |
|
1107 |
|
1108 class FilterRQLBuilder(object): |
|
1109 """called by javascript to get a rql string from filter form""" |
|
1110 |
|
1111 def __init__(self, req): |
|
1112 self._cw = req |
|
1113 |
|
1114 def build_rql(self):#, tablefilter=False): |
|
1115 form = self._cw.form |
|
1116 facetids = form['facets'].split(',') |
|
1117 # XXX Union unsupported yet |
|
1118 select = self._cw.vreg.parse(self._cw, form['baserql']).children[0] |
|
1119 mainvar = filtered_variable(select) |
|
1120 toupdate = [] |
|
1121 for facetid in facetids: |
|
1122 facet = get_facet(self._cw, facetid, select, mainvar) |
|
1123 facet.add_rql_restrictions() |
|
1124 if facet.needs_update: |
|
1125 toupdate.append(facetid) |
|
1126 return select.as_string(), toupdate |