web/facet.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 03 Nov 2011 11:18:37 +0100
changeset 8046 bdd555df7a91
parent 8029 805d4e121b65
child 8067 3841d8793efb
permissions -rw-r--r--
[entity table view] add count default to true on main entity column

# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
"""
The :mod:`cubicweb.web.facet` module contains a set of abstract classes to use
as bases to build your own facets

All facet classes inherits from the :class:`AbstractFacet` class, though you'll
usually find some more handy class that do what you want.

Let's see available classes.

Classes you'll want to use
--------------------------
.. autoclass:: cubicweb.web.facet.RelationFacet
.. autoclass:: cubicweb.web.facet.RelationAttributeFacet
.. autoclass:: cubicweb.web.facet.HasRelationFacet
.. autoclass:: cubicweb.web.facet.AttributeFacet
.. autoclass:: cubicweb.web.facet.RQLPathFacet
.. autoclass:: cubicweb.web.facet.RangeFacet
.. autoclass:: cubicweb.web.facet.DateRangeFacet
.. autoclass:: cubicweb.web.facet.BitFieldFacet

Classes for facets implementor
------------------------------
Unless you didn't find the class that does the job you want above, you may want
to skip those classes...

.. autoclass:: cubicweb.web.facet.AbstractFacet
.. autoclass:: cubicweb.web.facet.VocabularyFacet

.. comment: XXX widgets
"""

__docformat__ = "restructuredtext en"
_ = unicode

from warnings import warn
from copy import deepcopy
from datetime import datetime, timedelta

from logilab.mtconverter import xml_escape
from logilab.common.graph import has_path
from logilab.common.decorators import cached
from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
from logilab.common.compat import all
from logilab.common.deprecation import deprecated

from rql import nodes, utils

from cubicweb import Unauthorized, typed_eid
from cubicweb.schema import display_name
from cubicweb.utils import make_uid
from cubicweb.selectors import match_context_prop, partial_relation_possible, yes
from cubicweb.appobject import AppObject
from cubicweb.web import RequestError, htmlwidgets


def rtype_facet_title(facet):
    ptypes = facet.cw_rset.column_types(0)
    if len(ptypes) == 1:
        return display_name(facet._cw, facet.rtype, form=facet.role,
                            context=iter(ptypes).next())
    return display_name(facet._cw, facet.rtype, form=facet.role)

def get_facet(req, facetid, select, filtered_variable):
    return req.vreg['facets'].object_by_id(facetid, req, select=select,
                                           filtered_variable=filtered_variable)

@deprecated('[3.13] filter_hiddens moved to cubicweb.web.views.facets with '
            'slightly modified prototype')
def filter_hiddens(w, baserql, **kwargs):
    from cubicweb.web.views.facets import filter_hiddens
    return filter_hiddens(w, baserql, wdgs=kwargs.pop('facets'), **kwargs)


## rqlst manipulation functions used by facets ################################

def init_facets(rset, select, mainvar=None):
    """Alters in place the <select> for filtering and returns related data.

    Calls :func:`prepare_select` to prepare the syntaxtree for selection and
    :func:`get_filtered_variable` that selects the variable to be filtered and
    drops several parts of the select tree. See each function docstring for
    details.

    :param rset: ResultSet we init facet for.
    :type rset: :class:`~cubicweb.rset.ResultSet`

    :param select: Select statement to be *altered* to support filtering.
    :type select:   :class:`~rql.stmts.Select` from the ``rset`` parameters.

    :param mainvar: Name of the variable we want to filter with facets.
    :type mainvar:  string

    :rtype: (filtered_variable, baserql) tuple.
    :return filtered_variable:  A rql class:`~rql.node.VariableRef`
                                instance as returned by
                                :func:`get_filtered_variable`.

    :return baserql: A string containing the rql before
                     :func:`prepare_select` but after
                     :func:`get_filtered_variable`.
    """
    rset.req.vreg.rqlhelper.annotate(select)
    filtered_variable = get_filtered_variable(select, mainvar)
    baserql = select.as_string(kwargs=rset.args) # before call to prepare_select
    prepare_select(select, filtered_variable)
    return filtered_variable, baserql

def get_filtered_variable(select, mainvar=None):
    """drop any limit/offset from select (in-place modification) and return the
    variable whose name is `mainvar` or the first variable selected in column 0
    """
    select.set_limit(None)
    select.set_offset(None)
    if mainvar is None:
        vref = select.selection[0].iget_nodes(nodes.VariableRef).next()
        return vref.variable
    return select.defined_vars[mainvar]

def prepare_select(select, filtered_variable):
    """prepare a syntax tree to generate facet filters

    * remove ORDERBY/GROUPBY clauses
    * cleanup selection (remove everything)
    * undefine unnecessary variables
    * set DISTINCT

    Notice unset of LIMIT/OFFSET us expected to be done by a previous call to
    :func:`get_filtered_variable`.
    """
    # cleanup sort terms / group by
    select.remove_sort_terms()
    select.remove_groups()
    # XXX remove aggregat from having
    # selection: only vocabulary entity
    for term in select.selection[:]:
        select.remove_selected(term)
    # remove unbound variables which only have some type restriction
    for dvar in select.defined_vars.values():
        if not (dvar is filtered_variable or dvar.stinfo['relations']):
            select.undefine_variable(dvar)
    # global tree config: DISTINCT, LIMIT, OFFSET
    select.set_distinct(True)

@deprecated('[3.13] use init_facets instead')
def prepare_facets_rqlst(rqlst, args=None):
    assert len(rqlst.children) == 1, 'FIXME: union not yet supported'
    select = rqlst.children[0]
    filtered_variable = get_filtered_variable(select)
    baserql = select.as_string(args)
    prepare_select(select, filtered_variable)
    return filtered_variable, baserql

def prepare_vocabulary_select(select, filtered_variable, rtype, role,
                              select_target_entity=True):
    """prepare a syntax tree to generate a filter vocabulary rql using the given
    relation:
    * create a variable to filter on this relation
    * add the relation
    * add the new variable to GROUPBY clause if necessary
    * add the new variable to the selection
    """
    newvar = _add_rtype_relation(select, filtered_variable, rtype, role)[0]
    if select_target_entity:
        # if select.groupby: XXX we remove groupby now
        #     select.add_group_var(newvar)
        select.add_selected(newvar)
    # add is restriction if necessary
    if filtered_variable.stinfo['typerel'] is None:
        etypes = frozenset(sol[filtered_variable.name] for sol in select.solutions)
        select.add_type_restriction(filtered_variable, etypes)
    return newvar


def insert_attr_select_relation(select, filtered_variable, rtype, role, attrname,
                                sortfuncname=None, sortasc=True,
                                select_target_entity=True):
    """modify a syntax tree to :
    * link a new variable to `filtered_variable` through `rtype` (where filtered_variable has `role`)
    * retrieve only the newly inserted variable and its `attrname`

    Sorting:
    * on `attrname` ascendant (`sortasc`=True) or descendant (`sortasc`=False)
    * on `sortfuncname`(`attrname`) if `sortfuncname` is specified
    * no sort if `sortasc` is None
    """
    cleanup_select(select, filtered_variable)
    var = prepare_vocabulary_select(select, filtered_variable, rtype, role,
                                   select_target_entity)
    attrvar = select.make_variable()
    select.add_relation(var, attrname, attrvar)
    # if query is grouped, we have to add the attribute variable
    #if select.groupby: XXX may not occur anymore
    #    if not attrvar in select.groupby:
    #        select.add_group_var(attrvar)
    if sortasc is not None:
        _set_orderby(select, attrvar, sortasc, sortfuncname)
    # add attribute variable to selection
    select.add_selected(attrvar)
    return var


def cleanup_select(select, filtered_variable):
    """cleanup tree from unnecessary restrictions:
    * attribute selection
    * optional relations linked to the main variable
    * mandatory relations linked to the main variable
    """
    if select.where is None:
        return
    schema = select.root.schema
    toremove = set()
    vargraph = deepcopy(select.vargraph) # graph representing links between variable
    for rel in select.where.get_nodes(nodes.Relation):
        ovar = _may_be_removed(rel, schema, filtered_variable)
        if ovar is not None:
            toremove.add(ovar)
    removed = set()
    while toremove:
        trvar = toremove.pop()
        trvarname = trvar.name
        # remove paths using this variable from the graph
        linkedvars = vargraph.pop(trvarname)
        for ovarname in linkedvars:
            vargraph[ovarname].remove(trvarname)
        # remove relation using this variable
        for rel in trvar.stinfo['relations']:
            if rel in removed:
                # already removed
                continue
            select.remove_node(rel)
            removed.add(rel)
        rel = trvar.stinfo['typerel']
        if rel is not None and not rel in removed:
            select.remove_node(rel)
            removed.add(rel)
        # cleanup groupby clause
        if select.groupby:
            for vref in select.groupby[:]:
                if vref.name == trvarname:
                    select.remove_group_var(vref)
        # we can also remove all variables which are linked to this variable
        # and have no path to the main variable
        for ovarname in linkedvars:
            if ovarname == filtered_variable.name:
                continue
            if not has_path(vargraph, ovarname, filtered_variable.name):
                toremove.add(select.defined_vars[ovarname])


def _may_be_removed(rel, schema, variable):
    """if the given relation may be removed from the tree, return the variable
    on the other side of `variable`, else return None
    Conditions:
    * the relation is an attribute selection of the main variable
    * the relation is optional relation linked to the main variable
    * the relation is a mandatory relation linked to the main variable
      without any restriction on the other variable
    """
    lhs, rhs = rel.get_variable_parts()
    rschema = schema.rschema(rel.r_type)
    if lhs.variable is variable:
        try:
            ovar = rhs.variable
        except AttributeError:
            # constant restriction
            # XXX: X title LOWER(T) if it makes sense?
            return None
        if rschema.final:
            if len(ovar.stinfo['relations']) == 1 \
                   and not ovar.stinfo.get('having'):
                # attribute selection
                return ovar
            return None
        opt = 'right'
        cardidx = 0
    elif getattr(rhs, 'variable', None) is variable:
        ovar = lhs.variable
        opt = 'left'
        cardidx = 1
    else:
        # not directly linked to the main variable
        return None
    if rel.optional in (opt, 'both'):
        # optional relation
        return ovar
    if all(rdef.cardinality[cardidx] in '1+'
           for rdef in rschema.rdefs.values()):
        # mandatory relation without any restriction on the other variable
        for orel in ovar.stinfo['relations']:
            if rel is orel:
                continue
            if _may_be_removed(orel, schema, ovar) is None:
                return None
        return ovar
    return None

def _make_relation(select, variable, rtype, role):
    newvar = select.make_variable()
    if role == 'object':
        rel = nodes.make_relation(newvar, rtype, (variable,), nodes.VariableRef)
    else:
        rel = nodes.make_relation(variable, rtype, (newvar,), nodes.VariableRef)
    return newvar, rel

def _add_rtype_relation(select, variable, rtype, role):
    """add a relation relying `variable` to entities linked by the `rtype`
    relation (where `variable` has `role`)

    return the inserted variable for linked entities.
    """
    newvar, newrel = _make_relation(select, variable, rtype, role)
    select.add_restriction(newrel)
    return newvar, newrel

def _remove_relation(select, rel, var):
    """remove a constraint relation from the syntax tree"""
    # remove the relation
    select.remove_node(rel)
    # remove relations where the filtered variable appears on the
    # lhs and rhs is a constant restriction
    extra = []
    for vrel in var.stinfo['relations']:
        if vrel is rel:
            continue
        if vrel.children[0].variable is var:
            if not vrel.children[1].get_nodes(nodes.Constant):
                extra.append(vrel)
            select.remove_node(vrel)
    return extra

def _set_orderby(select, newvar, sortasc, sortfuncname):
    if sortfuncname is None:
        select.add_sort_var(newvar, sortasc)
    else:
        vref = nodes.variable_ref(newvar)
        vref.register_reference()
        sortfunc = nodes.Function(sortfuncname)
        sortfunc.append(vref)
        term = nodes.SortTerm(sortfunc, sortasc)
        select.add_sort_term(term)

def _get_var(select, varname, varmap):
    try:
        return varmap[varname]
    except KeyError:
        varmap[varname] = var = select.make_variable()
        return var


_prepare_vocabulary_rqlst = deprecated('[3.13] renamed prepare_vocabulary_select')(
    prepare_vocabulary_select)
_cleanup_rqlst = deprecated('[3.13] renamed to cleanup_select')(cleanup_select)


## base facet classes ##########################################################

class AbstractFacet(AppObject):
    """Abstract base class for all facets. Facets are stored in their own
    'facets' registry. They are similar to contextual components since the use
    the following configurable properties:

    * `visible`, boolean flag telling if a facet should be displayed or not

    * `order`, integer to control facets display order

    * `context`, telling if a facet should be displayed in the table form filter
      (context = 'tablefilter') or in the facet box (context = 'facetbox') or in
      both (context = '')

    The following methods define the facet API:

    .. automethod:: cubicweb.web.facet.AbstractFacet.get_widget
    .. automethod:: cubicweb.web.facet.AbstractFacet.add_rql_restrictions

    Facets will have the following attributes set (beside the standard
    :class:`~cubicweb.appobject.AppObject` ones):

    * `select`, the :class:`rql.stmts.Select` node of the rql syntax tree being
      filtered

    * `filtered_variable`, the variable node in this rql syntax tree that we're
      interested in filtering

    Facets implementors may also be interested in the following properties /
    methods:

    .. autoattribute:: cubicweb.web.facet.AbstractFacet.operator
    .. automethod:: cubicweb.web.facet.AbstractFacet.rqlexec
    """
    __abstract__ = True
    __registry__ = 'facets'
    cw_property_defs = {
        _('visible'): dict(type='Boolean', default=True,
                           help=_('display the facet or not')),
        _('order'):   dict(type='Int', default=99,
                           help=_('display order of the facet')),
        _('context'): dict(type='String', default='',
                           # None <-> both
                           vocabulary=(_('tablefilter'), _('facetbox'), ''),
                           help=_('context where this facet should be displayed, '
                                  'leave empty for both')),
        }
    visible = True
    context = ''
    needs_update = False
    start_unfolded = True
    cw_rset = None # ensure facets have a cw_rset attribute

    def __init__(self, req, select=None, filtered_variable=None,
                 **kwargs):
        super(AbstractFacet, self).__init__(req, **kwargs)
        assert select is not None
        assert filtered_variable
        # take care: facet may be retreived using `object_by_id` from an ajax call
        # or from `select` using the result set to filter
        self.select = select
        self.filtered_variable = filtered_variable

    def __repr__(self):
        return '<%s>' % self.__class__.__name__

    def get_widget(self):
        """Return the widget instance to use to display this facet, or None if
        the facet can't do anything valuable (only one value in the vocabulary
        for instance).
        """
        raise NotImplementedError

    def add_rql_restrictions(self):
        """When some facet criteria has been updated, this method is called to
        add restriction for this facet into the rql syntax tree. It should get
        back its value in form parameters, and modify the syntax tree
        (`self.select`) accordingly.
        """
        raise NotImplementedError

    @property
    def operator(self):
        """Return the operator (AND or OR) to use for this facet when multiple
        values are selected.
        """
        # OR between selected values by default
        return self._cw.form.get(xml_escape(self.__regid__) + '_andor', 'OR')

    def rqlexec(self, rql, args=None):
        """Utility method to execute some rql queries, and simply returning an
        empty list if :exc:`Unauthorized` is raised.
        """
        try:
            return self._cw.execute(rql, args)
        except Unauthorized:
            return []

    @property
    def wdgclass(self):
        raise NotImplementedError

    @property
    @deprecated('[3.13] renamed .select')
    def rqlst(self):
        return self.select


class VocabularyFacet(AbstractFacet):
    """This abstract class extend :class:`AbstractFacet` to use the
    :class:`FacetVocabularyWidget` as widget, suitable for facets that may
    restrict values according to a (usually computed) vocabulary.

    A class which inherits from VocabularyFacet must define at least these methods:

    .. automethod:: cubicweb.web.facet.VocabularyFacet.vocabulary
    .. automethod:: cubicweb.web.facet.VocabularyFacet.possible_values
    """
    needs_update = True
    support_and = False

    @property
    def wdgclass(self):
        return FacetVocabularyWidget

    def get_selected(self):
        return frozenset(typed_eid(eid)
                         for eid in self._cw.list_form_param(self.__regid__))

    def get_widget(self):
        """Return the widget instance to use to display this facet.

        This implementation expects a .vocabulary method on the facet and
        return a combobox displaying this vocabulary.
        """
        vocab = self.vocabulary()
        if len(vocab) <= 1:
            return None
        wdg = self.wdgclass(self)
        selected = self.get_selected()
        for label, value in vocab:
            wdg.items.append((value, label, value in selected))
        return wdg

    def vocabulary(self):
        """Return vocabulary for this facet, eg a list of 2-uple (label, value).
        """
        raise NotImplementedError

    def possible_values(self):
        """Return a list of possible values (as string since it's used to
        compare to a form value in javascript) for this facet.
        """
        raise NotImplementedError


class RelationFacet(VocabularyFacet):
    """Base facet to filter some entities according to other entities to which
    they are related. Create concrete facet by inheriting from this class an then
    configuring it by setting class attribute described below.

    The relation is defined by the `rtype` and `role` attributes.

    The `no_relation` boolean flag tells if a special 'no relation' value should be
    added (allowing to filter on entities which *do not* have the relation set).
    Default is computed according the relation's cardinality.

    The values displayed for related entities will be:

    * result of calling their `label_vid` view if specified
    * else their `target_attr` attribute value if specified
    * else their eid (you usually want something nicer...)

    When no `label_vid` is set, you will get translated value if `i18nable` is
    set. By default, `i18nable` will be set according to the schema, but you can
    force its value by setting it has a class attribute.

    You can filter out target entity types by specifying `target_type`.

    By default, vocabulary will be displayed sorted on `target_attr` value in an
    ascending way. You can control sorting with:

    * `sortfunc`: set this to a stored procedure name if you want to sort on the
      result of this function's result instead of direct value

    * `sortasc`: boolean flag to control ascendant/descendant sorting

    To illustrate this facet, let's take for example an *excerpt* of the schema
    of an office location search application:

    .. sourcecode:: python

      class Office(WorkflowableEntityType):
          price = Int(description='euros / m2 / HC / HT')
          surface = Int(description='m2')
          has_address = SubjectRelation('PostalAddress',
                                        cardinality='1?',
                                        composite='subject')
          proposed_by = SubjectRelation('Agency')


    We can simply define a facet to filter offices according to the agency
    proposing it:

    .. sourcecode:: python

      class AgencyFacet(RelationFacet):
          __regid__ = 'agency'
          # this facet should only be selected when visualizing offices
          __select__ = RelationFacet.__select__ & is_instance('Office')
          # this facet is a filter on the 'Agency' entities linked to the office
          # through the 'proposed_by' relation, where the office is the subject
          # of the relation
          rtype = 'has_address'
          # 'subject' is the default but setting it explicitly doesn't hurt...
          role = 'subject'
          # we want to display the agency's name
          target_attr = 'name'
    """
    __select__ = partial_relation_possible() & match_context_prop()
    # class attributes to configure the relation facet
    rtype = None
    role = 'subject'
    target_type = None
    target_attr = 'eid'
    # for subclasses parametrization, should not change if you want a
    # RelationFacet
    target_attr_type = 'Int'
    restr_attr = 'eid'
    restr_attr_type = 'Int'
    comparator = '=' # could be '<', '<=', '>', '>='
    # set this to a stored procedure name if you want to sort on the result of
    # this function's result instead of direct value
    sortfunc = None
    # ascendant/descendant sorting
    sortasc = True
    # if you want to call a view on the entity instead of using `target_attr`
    label_vid = None

    # internal purpose
    _select_target_entity = True

    title = property(rtype_facet_title)
    no_relation_label = _('<no relation>')

    def __repr__(self):
        return '<%s on (%s-%s)>' % (self.__class__.__name__, self.rtype, self.role)

    # facet public API #########################################################

    def vocabulary(self):
        """return vocabulary for this facet, eg a list of 2-uple (label, value)
        """
        select = self.select
        select.save_state()
        if self.rql_sort:
            sort = self.sortasc
        else:
            sort = None # will be sorted on label
        try:
            var = insert_attr_select_relation(
                select, self.filtered_variable, self.rtype, self.role,
                self.target_attr, self.sortfunc, sort,
                self._select_target_entity)
            if self.target_type is not None:
                select.add_type_restriction(var, self.target_type)
            try:
                rset = self.rqlexec(select.as_string(), self.cw_rset.args)
            except Exception:
                self.exception('error while getting vocabulary for %s, rql: %s',
                               self, select.as_string())
                return ()
        finally:
            select.recover()
        # don't call rset_vocabulary on empty result set, it may be an empty
        # *list* (see rqlexec implementation)
        values = rset and self.rset_vocabulary(rset) or []
        if self._include_no_relation():
            values.insert(0, (self._cw._(self.no_relation_label), ''))
        return values

    def possible_values(self):
        """return a list of possible values (as string since it's used to
        compare to a form value in javascript) for this facet
        """
        select = self.select
        select.save_state()
        try:
            cleanup_select(select, self.filtered_variable)
            if self._select_target_entity:
                prepare_vocabulary_select(select, self.filtered_variable, self.rtype,
                                         self.role, select_target_entity=True)
            else:
                insert_attr_select_relation(
                    select, self.filtered_variable, self.rtype, self.role,
                    self.target_attr, select_target_entity=False)
            values = [unicode(x) for x, in self.rqlexec(select.as_string())]
        except Exception:
            self.exception('while computing values for %s', self)
            return []
        finally:
            select.recover()
        if self._include_no_relation():
            values.append('')
        return values

    def add_rql_restrictions(self):
        """add restriction for this facet into the rql syntax tree"""
        value = self._cw.form.get(self.__regid__)
        if value is None:
            return
        filtered_variable = self.filtered_variable
        restrvar, rel = _add_rtype_relation(self.select, filtered_variable,
                                            self.rtype, self.role)
        self.value_restriction(restrvar, rel, value)

    # internal control API #####################################################

    @property
    def i18nable(self):
        """should label be internationalized"""
        if self.target_type:
            eschema = self._cw.vreg.schema.eschema(self.target_type)
        elif self.role == 'subject':
            eschema = self._cw.vreg.schema.rschema(self.rtype).objects()[0]
        else:
            eschema = self._cw.vreg.schema.rschema(self.rtype).subjects()[0]
        return getattr(eschema.rdef(self.target_attr), 'internationalizable', False)

    @property
    def no_relation(self):
        return (not self._cw.vreg.schema.rschema(self.rtype).final
                and self._search_card('?*'))

    @property
    def rql_sort(self):
        """return true if we can handle sorting in the rql query. E.g.  if
        sortfunc is set or if we have not to transform the returned value (eg no
        label_vid and not i18nable)
        """
        return self.sortfunc is not None or (self.label_vid is None
                                             and not self.i18nable)

    def rset_vocabulary(self, rset):
        if self.i18nable:
            _ = self._cw._
        else:
            _ = unicode
        if self.rql_sort:
            values = [(_(label), eid) for eid, label in rset]
        else:
            if self.label_vid is None:
                values = [(_(label), eid) for eid, label in rset]
            else:
                values = [(entity.view(self.label_vid), entity.eid)
                          for entity in rset.entities()]
            values = sorted(values)
            if not self.sortasc:
                values = list(reversed(values))
        return values

    @property
    def support_and(self):
        return self._search_card('+*')

    # internal utilities #######################################################

    @cached
    def _support_and_compat(self):
        support = self.support_and
        if callable(support):
            warn('[3.13] %s.support_and is now a property' % self.__class__,
                 DeprecationWarning)
            support = support()
        return support

    def value_restriction(self, restrvar, rel, value):
        # XXX handle rel is None case in RQLPathFacet?
        if self.restr_attr != 'eid':
            self.select.set_distinct(True)
        if isinstance(value, basestring):
            # only one value selected
            if value:
                self.select.add_constant_restriction(
                    restrvar, self.restr_attr, value,
                    self.restr_attr_type)
            else:
                rel.parent.replace(rel, nodes.Not(rel))
        elif self.operator == 'OR':
            # set_distinct only if rtype cardinality is > 1
            if self._support_and_compat():
                self.select.set_distinct(True)
            # multiple ORed values: using IN is fine
            if '' in value:
                value.remove('')
                self._add_not_rel_restr(rel)
            self._and_restriction(rel, restrvar, value)
        else:
            # multiple values with AND operator. We've to generate a query like
            # "X relation A, A eid 1, X relation B, B eid 1", hence the new
            # relations at each iteration in the while loop below 
            if '' in value:
                raise RequestError("this doesn't make sense")
            self._and_restriction(rel, restrvar, value.pop())
            while value:
                restrvar, rtrel = _make_relation(self.select, self.filtered_variable,
                                                 self.rtype, self.role)
                if rel is None:
                    self.select.add_restriction(rtrel)
                else:
                    rel.parent.replace(rel, nodes.And(rel, rtrel))
                self._and_restriction(rel, restrvar, value.pop())

    def _and_restriction(self, rel, restrvar, value):
        if rel is None:
            self.select.add_constant_restriction(restrvar, self.restr_attr,
                                                 value, self.restr_attr_type)
        else:
            rrel = nodes.make_constant_restriction(restrvar, self.restr_attr,
                                                   value, self.restr_attr_type)
            rel.parent.replace(rel, nodes.And(rel, rrel))


    @cached
    def _search_card(self, cards):
        for rdef in self._iter_rdefs():
            if rdef.role_cardinality(self.role) in cards:
                return True
        return False

    def _iter_rdefs(self):
        rschema = self._cw.vreg.schema.rschema(self.rtype)
        # XXX when called via ajax, no rset to compute possible types
        possibletypes = self.cw_rset and self.cw_rset.column_types(0)
        for rdef in rschema.rdefs.itervalues():
            if possibletypes is not None:
                if self.role == 'subject':
                    if rdef.subject not in possibletypes:
                        continue
                elif rdef.object not in possibletypes:
                    continue
            if self.target_type is not None:
                if self.role == 'subject':
                    if rdef.object != self.target_type:
                        continue
                elif rdef.subject != self.target_type:
                    continue
            yield rdef

    def _include_no_relation(self):
        if not self.no_relation:
            return False
        if self._cw.vreg.schema.rschema(self.rtype).final:
            return False
        if self.role == 'object':
            subj = utils.rqlvar_maker(defined=self.select.defined_vars,
                                      aliases=self.select.aliases).next()
            obj = self.filtered_variable.name
        else:
            subj = self.filtered_variable.name
            obj = utils.rqlvar_maker(defined=self.select.defined_vars,
                                     aliases=self.select.aliases).next()
        restrictions = []
        if self.select.where:
            restrictions.append(self.select.where.as_string())
        if self.select.with_:
            restrictions.append('WITH ' + ','.join(
                term.as_string() for term in self.select.with_))
        if restrictions:
            restrictions = ',' + ','.join(restrictions)
        else:
            restrictions = ''
        rql = 'Any %s LIMIT 1 WHERE NOT %s %s %s%s' % (
            self.filtered_variable.name, subj, self.rtype, obj, restrictions)
        try:
            return bool(self.rqlexec(rql, self.cw_rset and self.cw_rset.args))
        except Exception:
            # catch exception on executing rql, work-around #1356884 until a
            # proper fix
            self.exception('cant handle rql generated by %s', self)
            return False

    def _add_not_rel_restr(self, rel):
        nrrel = nodes.Not(_make_relation(self.select, self.filtered_variable,
                                         self.rtype, self.role)[1])
        rel.parent.replace(rel, nodes.Or(nrrel, rel))


class RelationAttributeFacet(RelationFacet):
    """Base facet to filter some entities according to an attribute of other
    entities to which they are related. Most things work similarly as
    :class:`RelationFacet`, except that:

    * `label_vid` doesn't make sense here

    * you should specify the attribute type using `target_attr_type` if it's not a
      String

    * you can specify a comparison operator using `comparator`


    Back to our example... if you want to search office by postal code and that
    you use a :class:`RelationFacet` for that, you won't get the expected
    behaviour: if two offices have the same postal code, they've however two
    different addresses.  So you'll see in the facet the same postal code twice,
    though linked to a different address entity. There is a great chance your
    users won't understand that...

    That's where this class come in ! It's used to said that you want to filter
    according to the *attribute value* of a relatied entity, not to the entity
    itself. Now here is the source code for the facet:

    .. sourcecode:: python

      class PostalCodeFacet(RelationAttributeFacet):
          __regid__ = 'postalcode'
          # this facet should only be selected when visualizing offices
          __select__ = RelationAttributeFacet.__select__ & is_instance('Office')
          # this facet is a filter on the PostalAddress entities linked to the
          # office through the 'has_address' relation, where the office is the
          # subject of the relation
          rtype = 'has_address'
          role = 'subject'
          # we want to search according to address 'postal_code' attribute
          target_attr = 'postalcode'
    """
    _select_target_entity = False
    # attribute type
    target_attr_type = 'String'
    # type of comparison: default is an exact match on the attribute value
    comparator = '=' # could be '<', '<=', '>', '>='

    @property
    def restr_attr(self):
        return self.target_attr

    @property
    def restr_attr_type(self):
        return self.target_attr_type

    def rset_vocabulary(self, rset):
        if self.i18nable:
            _ = self._cw._
        else:
            _ = unicode
        if self.rql_sort:
            return [(_(value), value) for value, in rset]
        values = [(_(value), value) for value, in rset]
        if self.sortasc:
            return sorted(values)
        return reversed(sorted(values))


class AttributeFacet(RelationAttributeFacet):
    """Base facet to filter some entities according one of their attribute.
    Configuration is mostly similarly as :class:`RelationAttributeFacet`, except that:

    * `target_attr` doesn't make sense here (you specify the attribute using `rtype`
    * `role` neither, it's systematically 'subject'

    So, suppose that in our office search example you want to refine search according
    to the office's surface. Here is a code snippet achieving this:

    .. sourcecode:: python

      class SurfaceFacet(AttributeFacet):
          __regid__ = 'surface'
          __select__ = AttributeFacet.__select__ & is_instance('Office')
          # this facet is a filter on the office'surface
          rtype = 'surface'
          # override the default value of operator since we want to filter
          # according to a minimal value, not an exact one
          comparator = '>='

          def vocabulary(self):
              '''override the default vocabulary method since we want to
              hard-code our threshold values.

              Not overriding would generate a filter containing all existing
              surfaces defined in the database.
              '''
              return [('> 200', '200'), ('> 250', '250'),
                      ('> 275', '275'), ('> 300', '300')]
    """

    support_and = False
    _select_target_entity = True

    @property
    def i18nable(self):
        """should label be internationalized"""
        for rdef in self._iter_rdefs():
            # no 'internationalizable' property for rdef whose object is not a
            # String
            if not getattr(rdef, 'internationalizable', False):
                return False
        return True

    def vocabulary(self):
        """return vocabulary for this facet, eg a list of 2-uple (label, value)
        """
        select = self.select
        select.save_state()
        try:
            filtered_variable = self.filtered_variable
            cleanup_select(select, filtered_variable)
            newvar = prepare_vocabulary_select(select, filtered_variable, self.rtype, self.role)
            _set_orderby(select, newvar, self.sortasc, self.sortfunc)
            if self.cw_rset:
                args = self.cw_rset.args
            else: # vocabulary used for possible_values
                args = None
            try:
                rset = self.rqlexec(select.as_string(), args)
            except Exception:
                self.exception('error while getting vocabulary for %s, rql: %s',
                               self, select.as_string())
                return ()
        finally:
            select.recover()
        # don't call rset_vocabulary on empty result set, it may be an empty
        # *list* (see rqlexec implementation)
        return rset and self.rset_vocabulary(rset)

    def add_rql_restrictions(self):
        """add restriction for this facet into the rql syntax tree"""
        value = self._cw.form.get(self.__regid__)
        if not value:
            return
        filtered_variable = self.filtered_variable
        self.select.add_constant_restriction(filtered_variable, self.rtype, value,
                                            self.target_attr_type, self.comparator)


class RQLPathFacet(RelationFacet):
    """Base facet to filter some entities according to an arbitrary rql
    path. Path should be specified as a list of 3-uples or triplet string, where
    'X' represent the filtered variable. You should specify using
    `filter_variable` the snippet variable that will be used to filter out
    results. You may also specify a `label_variable`. If you want to filter on
    an attribute value, you usually don't want to specify the later since it's
    the same as the filter variable, though you may have to specify the attribute
    type using `restr_attr_type` if there are some type ambiguity in the schema
    for the attribute.

    Using this facet, we can rewrite facets we defined previously:

    .. sourcecode:: python

      class AgencyFacet(RQLPathFacet):
          __regid__ = 'agency'
          # this facet should only be selected when visualizing offices
          __select__ = is_instance('Office')
          # this facet is a filter on the 'Agency' entities linked to the office
          # through the 'proposed_by' relation, where the office is the subject
          # of the relation
          path = ['X has_address O', 'O name N']
          filter_variable = 'O'
          label_variable = 'N'

      class PostalCodeFacet(RQLPathFacet):
          __regid__ = 'postalcode'
          # this facet should only be selected when visualizing offices
          __select__ = is_instance('Office')
          # this facet is a filter on the PostalAddress entities linked to the
          # office through the 'has_address' relation, where the office is the
          # subject of the relation
          path = ['X has_address O', 'O postal_code PC']
          filter_variable = 'PC'

    Though some features, such as 'no value' or automatic internationalization,
    won't work. This facet class is designed to be used for cases where
    :class:`RelationFacet` or :class:`RelationAttributeFacet` can't do the trick
    (e.g when you want to filter on entities where are not directly linked to
    the filtered entities).
    """
    __select__ = yes() # we don't want RelationFacet's selector
    # must be specified
    path = None
    filter_variable = None
    # may be specified
    label_variable = None
    # usually guessed, but may be explicitly specified
    restr_attr = None
    restr_attr_type = None

    # XXX disabled features
    i18nable = False
    no_relation = False
    support_and = False

    def __init__(self, *args, **kwargs):
        super(RQLPathFacet, self).__init__(*args, **kwargs)
        assert self.filter_variable != self.label_variable, \
            ('filter_variable and label_variable should be different. '
             'You may want to let label_variable undefined (ie None).')
        assert self.path and isinstance(self.path, (list, tuple)), \
            'path should be a list of 3-uples, not %s' % self.path
        for part in self.path:
            if isinstance(part, basestring):
                part = part.split()
            assert len(part) == 3, \
                   'path should be a list of 3-uples, not %s' % part

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__,
                            ','.join(str(p) for p in self.path))

    def vocabulary(self):
        """return vocabulary for this facet, eg a list of (label, value)"""
        select = self.select
        select.save_state()
        if self.rql_sort:
            sort = self.sortasc
        else:
            sort = None # will be sorted on label
        try:
            cleanup_select(select, self.filtered_variable)
            varmap, restrvar = self.add_path_to_select()
            select.append_selected(nodes.VariableRef(restrvar))
            if self.label_variable:
                attrvar = varmap[self.label_variable]
            else:
                attrvar = restrvar
            select.append_selected(nodes.VariableRef(attrvar))
            if sort is not None:
                _set_orderby(select, attrvar, sort, self.sortfunc)
            try:
                rset = self.rqlexec(select.as_string(), self.cw_rset.args)
            except Exception:
                self.exception('error while getting vocabulary for %s, rql: %s',
                               self, select.as_string())
                return ()
        finally:
            select.recover()
        # don't call rset_vocabulary on empty result set, it may be an empty
        # *list* (see rqlexec implementation)
        values = rset and self.rset_vocabulary(rset) or []
        if self._include_no_relation():
            values.insert(0, (self._cw._(self.no_relation_label), ''))
        return values

    def possible_values(self):
        """return a list of possible values (as string since it's used to
        compare to a form value in javascript) for this facet
        """
        select = self.select
        select.save_state()
        try:
            cleanup_select(select, self.filtered_variable)
            varmap, restrvar = self.add_path_to_select(skiplabel=True)
            select.append_selected(nodes.VariableRef(restrvar))
            values = [unicode(x) for x, in self.rqlexec(select.as_string())]
        except Exception:
            self.exception('while computing values for %s', self)
            return []
        finally:
            select.recover()
        if self._include_no_relation():
            values.append('')
        return values

    def add_rql_restrictions(self):
        """add restriction for this facet into the rql syntax tree"""
        value = self._cw.form.get(self.__regid__)
        if value is None:
            return
        varmap, restrvar = self.add_path_to_select(
            skiplabel=True, skipattrfilter=True)
        self.value_restriction(restrvar, None, value)

    def add_path_to_select(self, skiplabel=False, skipattrfilter=False):
        varmap = {'X': self.filtered_variable}
        actual_filter_variable = None
        for part in self.path:
            if isinstance(part, basestring):
                part = part.split()
            subject, rtype, object = part
            if skiplabel and object == self.label_variable:
                continue
            if object == self.filter_variable:
                rschema = self._cw.vreg.schema.rschema(rtype)
                if rschema.final:
                    # filter variable is an attribute variable
                    if self.restr_attr is None:
                        self.restr_attr = rtype
                    if self.restr_attr_type is None:
                        attrtypes = set(obj for subj,obj in rschema.rdefs)
                        if len(attrtypes) > 1:
                            raise Exception('ambigous attribute %s, specify attrtype on %s'
                                            % (rtype, self.__class__))
                        self.restr_attr_type = iter(attrtypes).next()
                    if skipattrfilter:
                        actual_filter_variable = subject
                        continue
            subjvar = _get_var(self.select, subject, varmap)
            objvar = _get_var(self.select, object, varmap)
            rel = nodes.make_relation(subjvar, rtype, (objvar,),
                                      nodes.VariableRef)
            self.select.add_restriction(rel)
        if self.restr_attr is None:
            self.restr_attr = 'eid'
        if self.restr_attr_type is None:
            self.restr_attr_type = 'Int'
        if actual_filter_variable:
            restrvar = varmap[actual_filter_variable]
        else:
            restrvar = varmap[self.filter_variable]
        return varmap, restrvar


class RangeFacet(AttributeFacet):
    """This class allows to filter entities according to an attribute of
    numerical type.

    It displays a slider using `jquery`_ to choose a lower bound and an upper
    bound.

    The example below provides an alternative to the surface facet seen earlier,
    in a more powerful way since

    * lower/upper boundaries are computed according to entities to filter
    * user can specify lower/upper boundaries, not only the lower one

    .. sourcecode:: python

      class SurfaceFacet(RangeFacet):
          __regid__ = 'surface'
          __select__ = RangeFacet.__select__ & is_instance('Office')
          # this facet is a filter on the office'surface
          rtype = 'surface'

    All this with even less code!

    The image below display the rendering of the slider:

    .. image:: ../images/facet_range.png

    .. _jquery: http://www.jqueryui.com/
    """
    target_attr_type = 'Float' # only numerical types are supported
    needs_update = False # not supported actually

    @property
    def wdgclass(self):
        return FacetRangeWidget

    def _range_rset(self):
        select = self.select
        select.save_state()
        try:
            filtered_variable = self.filtered_variable
            cleanup_select(select, filtered_variable)
            newvar = _add_rtype_relation(select, filtered_variable, self.rtype, self.role)[0]
            minf = nodes.Function('MIN')
            minf.append(nodes.VariableRef(newvar))
            select.add_selected(minf)
            maxf = nodes.Function('MAX')
            maxf.append(nodes.VariableRef(newvar))
            select.add_selected(maxf)
            # add is restriction if necessary
            if filtered_variable.stinfo['typerel'] is None:
                etypes = frozenset(sol[filtered_variable.name] for sol in select.solutions)
                select.add_type_restriction(filtered_variable, etypes)
            try:
                return self.rqlexec(select.as_string(), self.cw_rset.args)
            except Exception:
                self.exception('error while getting vocabulary for %s, rql: %s',
                               self, select.as_string())
                return ()
        finally:
            select.recover()

    def vocabulary(self):
        """return vocabulary for this facet, eg a list of 2-uple (label, value)
        """
        rset = self._range_rset()
        if rset:
            minv, maxv = rset[0]
            return [(unicode(minv), minv), (unicode(maxv), maxv)]
        return []

    def possible_values(self):
        """Return a list of possible values (as string since it's used to
        compare to a form value in javascript) for this facet.
        """
        return [strval for strval, val in self.vocabulary()]

    def get_widget(self):
        """return the widget instance to use to display this facet"""
        values = set(value for _, value in self.vocabulary() if value is not None)
        # Rset with entities (the facet is selected) but without values
        if len(values) < 2:
            return None
        return self.wdgclass(self, min(values), max(values))

    def formatvalue(self, value):
        """format `value` before in order to insert it in the RQL query"""
        return unicode(value)

    def infvalue(self, min=False):
        if min:
            return self._cw.form.get('min_%s_inf' % self.__regid__)
        return self._cw.form.get('%s_inf' % self.__regid__)

    def supvalue(self, max=False):
        if max:
            return self._cw.form.get('max_%s_sup' % self.__regid__)
        return self._cw.form.get('%s_sup' % self.__regid__)

    def add_rql_restrictions(self):
        infvalue = self.infvalue()
        supvalue = self.supvalue()
        if infvalue is None or supvalue is None: # nothing sent
            return
        # when a value is equal to one of the limit, don't add the restriction,
        # else we filter out NULL values implicitly
        if infvalue != self.infvalue(min=True):
            self._add_restriction(infvalue, '>=')
        if supvalue != self.supvalue(max=True):
            self._add_restriction(supvalue, '<=')

    def _add_restriction(self, value, operator):
        self.select.add_constant_restriction(self.filtered_variable,
                                             self.rtype,
                                             self.formatvalue(value),
                                             self.target_attr_type, operator)



class DateRangeFacet(RangeFacet):
    """This class works similarly as the :class:`RangeFacet` but for attribute
    of date type.

    The image below display the rendering of the slider for a date range:

    .. image:: ../images/facet_date_range.png
    """
    target_attr_type = 'Date' # only date types are supported

    @property
    def wdgclass(self):
        return DateFacetRangeWidget

    def formatvalue(self, value):
        """format `value` before in order to insert it in the RQL query"""
        try:
            date_value = ticks2datetime(float(value))
        except (ValueError, OverflowError):
            return u'"date out-of-range"'
        return '"%s"' % ustrftime(date_value, '%Y/%m/%d')


class HasRelationFacet(AbstractFacet):
    """This class simply filter according to the presence of a relation
    (whatever the entity at the other end). It display a simple checkbox that
    lets you refine your selection in order to get only entities that actually
    have this relation. You simply have to define which relation using the
    `rtype` and `role` attributes.

    Here is an example of the rendering of thos facet to filter book with image
    and the corresponding code:

    .. image:: ../images/facet_has_image.png

    .. sourcecode:: python

      class HasImageFacet(HasRelationFacet):
          __regid__ = 'hasimage'
          __select__ = HasRelationFacet.__select__ & is_instance('Book')
          rtype = 'has_image'
          role = 'subject'
    """
    __select__ = partial_relation_possible() & match_context_prop()
    rtype = None # override me in subclass
    role = 'subject' # role of filtered entity in the relation

    title = property(rtype_facet_title)
    needs_update = False # not supported actually
    support_and = False

    def get_widget(self):
        return CheckBoxFacetWidget(self._cw, self,
                                   '%s:%s' % (self.rtype, self),
                                   self._cw.form.get(self.__regid__))

    def add_rql_restrictions(self):
        """add restriction for this facet into the rql syntax tree"""
        self.select.set_distinct(True) # XXX
        value = self._cw.form.get(self.__regid__)
        if not value: # no value sent for this facet
            return
        var = self.select.make_variable()
        if self.role == 'subject':
            self.select.add_relation(self.filtered_variable, self.rtype, var)
        else:
            self.select.add_relation(var, self.rtype, self.filtered_variable)


class BitFieldFacet(AttributeFacet):
    """Base facet class for Int field holding some bit values using binary
    masks.

    label / value for each bit should be given using the :attr:`choices`
    attribute.

    See also :class:`~cubicweb.web.formwidgets.BitSelect`.
    """
    choices = None # to be set on concret class
    def add_rql_restrictions(self):
        value = self._cw.form.get(self.__regid__)
        if not value:
            return
        if isinstance(value, list):
            value = reduce(lambda x, y: int(x) | int(y), value)
        attr_var = self.select.make_variable()
        self.select.add_relation(self.filtered_variable, self.rtype, attr_var)
        comp = nodes.Comparison('=', nodes.Constant(value, 'Int'))
        comp.append(nodes.MathExpression('&', nodes.variable_ref(attr_var),
                                         nodes.Constant(value, 'Int')))
        having = self.select.having
        if having:
            self.select.replace(having[0], nodes.And(having[0], comp))
        else:
            self.select.set_having([comp])

    def rset_vocabulary(self, rset):
        mask = reduce(lambda x, y: x | (y[0] or 0), rset, 0)
        return sorted([(self._cw._(label), val) for label, val in self.choices
                       if val & mask])

    def possible_values(self):
        return [unicode(val) for label, val in self.vocabulary()]


## html widets ################################################################
_DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT = 12

class FacetVocabularyWidget(htmlwidgets.HTMLWidget):

    def __init__(self, facet):
        self.facet = facet
        self.items = []

    @property
    @cached
    def css_overflow_limit(self):
        """ we try to deduce a number of displayed lines from a css property
        if we get another unit we're out of luck and resort to one constant
        hence, it is strongly advised not to specify but ems for this css prop
        """
        vreg = self.facet._cw.vreg
        cssprop = vreg.config.uiprops['facet_overflowedHeight'].lower().strip()
        if cssprop.endswith('em'):
            try:
                return int(cssprop[:-2])
            except Exception:
                vreg.warning('css property facet_overflowedHeight looks malformed (%r)',
                             cssprop)
        return _DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT

    @property
    @cached
    def height(self):
        return 1 + min(len(self.items),
                       self.css_overflow_limit + int(self.facet._support_and_compat()))

    @property
    @cached
    def overflows(self):
        return len(self.items) >= self.css_overflow_limit

    scrollbar_padding_factor = 4

    def _render(self):
        w = self.w
        title = xml_escape(self.facet.title)
        facetid = make_uid(self.facet.__regid__)
        w(u'<div id="%s" class="facet">\n' % facetid)
        w(u'<div class="facetTitle" cubicweb:facetName="%s">%s</div>\n' %
          (xml_escape(self.facet.__regid__), title))
        if self.facet._support_and_compat():
            self._render_and_or(w)
        cssclass = 'vocabularyFacet'
        if not self.facet.start_unfolded:
            cssclass += ' hidden'
        overflow = self.overflows
        if overflow:
            if self.facet._support_and_compat():
                cssclass += ' vocabularyFacetBodyWithLogicalSelector'
            else:
                cssclass += ' vocabularyFacetBody'
        w(u'<div class="%s">\n' % cssclass)
        for value, label, selected in self.items:
            if value is None:
                continue
            self._render_value(w, value, label, selected, overflow)
        w(u'</div>\n')
        w(u'</div>\n')

    def _render_and_or(self, w):
        _ = self.facet._cw._
        w(u"""<select name='%s' class='radio facetOperator' title='%s'>
  <option value='OR'>%s</option>
  <option value='AND'>%s</option>
</select>""" % (xml_escape(self.facet.__regid__) + '_andor',
                _('and/or between different values'),
                _('OR'), _('AND')))

    def _render_value(self, w, value, label, selected, overflow):
        cssclass = 'facetValue facetCheckBox'
        if selected:
            cssclass += ' facetValueSelected'
        w(u'<div class="%s" cubicweb:value="%s">\n'
          % (cssclass, xml_escape(unicode(value))))
        # If it is overflowed one must add padding to compensate for the vertical
        # scrollbar; given current css values, 4 blanks work perfectly ...
        padding = u'&#160;' * self.scrollbar_padding_factor if overflow else u''
        w('<span>%s</span>' % xml_escape(label))
        w(padding)
        w(u'</div>')

class FacetStringWidget(htmlwidgets.HTMLWidget):
    def __init__(self, facet):
        self.facet = facet
        self.value = None

    @property
    def height(self):
        return 3

    def _render(self):
        w = self.w
        title = xml_escape(self.facet.title)
        facetid = make_uid(self.facet.__regid__)
        w(u'<div id="%s" class="facet">\n' % facetid)
        w(u'<div class="facetTitle" cubicweb:facetName="%s">%s</div>\n' %
               (xml_escape(self.facet.__regid__), title))
        w(u'<input name="%s" type="text" value="%s" />\n' % (facetid, self.value or u''))
        w(u'</div>\n')


class FacetRangeWidget(htmlwidgets.HTMLWidget):
    formatter = 'function (value) {return value;}'
    onload = u'''
    var _formatter = %(formatter)s;
    jQuery("#%(sliderid)s").slider({
        range: true,
        min: %(minvalue)s,
        max: %(maxvalue)s,
        values: [%(minvalue)s, %(maxvalue)s],
        stop: function(event, ui) { // submit when the user stops sliding
           var form = $('#%(sliderid)s').closest('form');
           buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
        },
        slide: function(event, ui) {
            jQuery('#%(sliderid)s_inf').html(_formatter(ui.values[0]));
            jQuery('#%(sliderid)s_sup').html(_formatter(ui.values[1]));
            jQuery('input[name="%(facetname)s_inf"]').val(ui.values[0]);
            jQuery('input[name="%(facetname)s_sup"]').val(ui.values[1]);
        }
   });
   // use JS formatter to format value on page load
   jQuery('#%(sliderid)s_inf').html(_formatter(jQuery('input[name="%(facetname)s_inf"]').val()));
   jQuery('#%(sliderid)s_sup').html(_formatter(jQuery('input[name="%(facetname)s_sup"]').val()));
'''
    #'# make emacs happier
    def __init__(self, facet, minvalue, maxvalue):
        self.facet = facet
        self.minvalue = minvalue
        self.maxvalue = maxvalue

    @property
    def height(self):
        return 3

    def _render(self):
        w = self.w
        facet = self.facet
        facet._cw.add_js('jquery.ui.js')
        facet._cw.add_css('jquery.ui.css')
        sliderid = make_uid('theslider')
        facetname = self.facet.__regid__
        facetid = make_uid(facetname)
        facet._cw.html_headers.add_onload(self.onload % {
            'sliderid': sliderid,
            'facetid': facetid,
            'facetname': facetname,
            'minvalue': self.minvalue,
            'maxvalue': self.maxvalue,
            'formatter': self.formatter,
            })
        title = xml_escape(self.facet.title)
        facetname = xml_escape(facetname)
        w(u'<div id="%s" class="facet">\n' % facetid)
        w(u'<div class="facetTitle" cubicweb:facetName="%s">%s</div>\n' %
          (facetname, title))
        cssclass = 'facetBody'
        if not self.facet.start_unfolded:
            cssclass += ' hidden'
        w(u'<div class="%s">\n' % cssclass)
        w(u'<span id="%s_inf"></span> - <span id="%s_sup"></span>'
          % (sliderid, sliderid))
        w(u'<input type="hidden" name="%s_inf" value="%s" />'
          % (facetname, self.minvalue))
        w(u'<input type="hidden" name="%s_sup" value="%s" />'
          % (facetname, self.maxvalue))
        w(u'<input type="hidden" name="min_%s_inf" value="%s" />'
          % (facetname, self.minvalue))
        w(u'<input type="hidden" name="max_%s_sup" value="%s" />'
          % (facetname, self.maxvalue))
        w(u'<div id="%s"></div>' % sliderid)
        w(u'</div>\n')
        w(u'</div>\n')


class DateFacetRangeWidget(FacetRangeWidget):

    formatter = 'function (value) {return (new Date(parseFloat(value))).strftime(DATE_FMT);}'

    def round_max_value(self, d):
        'round to upper value to avoid filtering out the max value'
        return datetime(d.year, d.month, d.day) + timedelta(days=1)

    def __init__(self, facet, minvalue, maxvalue):
        maxvalue = self.round_max_value(maxvalue)
        super(DateFacetRangeWidget, self).__init__(facet,
                                                   datetime2ticks(minvalue),
                                                   datetime2ticks(maxvalue))
        fmt = facet._cw.property_value('ui.date-format')
        facet._cw.html_headers.define_var('DATE_FMT', fmt)


class CheckBoxFacetWidget(htmlwidgets.HTMLWidget):
    selected_img = "black-check.png"
    unselected_img = "black-uncheck.png"

    def __init__(self, req, facet, value, selected):
        self._cw = req
        self.facet = facet
        self.value = value
        self.selected = selected

    @property
    def height(self):
        return 2

    def _render(self):
        w = self.w
        title = xml_escape(self.facet.title)
        facetid = make_uid(self.facet.__regid__)
        w(u'<div id="%s" class="facet">\n' % facetid)
        cssclass = 'facetValue facetCheckBox'
        if self.selected:
            cssclass += ' facetValueSelected'
            imgsrc = self._cw.data_url(self.selected_img)
            imgalt = self._cw._('selected')
        else:
            imgsrc = self._cw.data_url(self.unselected_img)
            imgalt = self._cw._('not selected')
        w(u'<div class="%s" cubicweb:value="%s">\n'
          % (cssclass, xml_escape(unicode(self.value))))
        w(u'<div>')
        w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" />&#160;' % (imgsrc, imgalt))
        w(u'<label class="facetTitle" cubicweb:facetName="%s">%s</label>'
          % (xml_escape(self.facet.__regid__), title))
        w(u'</div>\n')
        w(u'</div>\n')
        w(u'</div>\n')


class FacetSeparator(htmlwidgets.HTMLWidget):
    def __init__(self, label=None):
        self.label = label or u'&#160;'

    def _render(self):
        pass

# other classes ################################################################

class FilterRQLBuilder(object):
    """called by javascript to get a rql string from filter form"""

    def __init__(self, req):
        self._cw = req

    def build_rql(self):
        form = self._cw.form
        facetids = form['facets'].split(',')
        # XXX Union unsupported yet
        select = self._cw.vreg.parse(self._cw, form['baserql']).children[0]
        filtered_variable = get_filtered_variable(select, form.get('mainvar'))
        toupdate = []
        for facetid in facetids:
            facet = get_facet(self._cw, facetid, select, filtered_variable)
            facet.add_rql_restrictions()
            if facet.needs_update:
                toupdate.append(facetid)
        return select.as_string(), toupdate