web/facet.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     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'&#160;' * 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" />&#160;' % (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