--- a/web/facet.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1787 +0,0 @@
-# copyright 2003-2012 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
-.. autoclass:: cubicweb.web.facet.AbstractRangeRQLPathFacet
-.. autoclass:: cubicweb.web.facet.RangeRQLPathFacet
-.. autoclass:: cubicweb.web.facet.DateRangeRQLPathFacet
-
-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"
-from cubicweb import _
-
-from functools import reduce
-from warnings import warn
-from copy import deepcopy
-from datetime import datetime, timedelta
-
-from six import text_type, string_types
-
-from logilab.mtconverter import xml_escape
-from logilab.common.graph import has_path
-from logilab.common.decorators import cached, cachedproperty
-from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
-from logilab.common.deprecation import deprecated
-from logilab.common.registry import yes
-
-from rql import nodes, utils
-
-from cubicweb import Unauthorized
-from cubicweb.schema import display_name
-from cubicweb.uilib import css_em_num_value, domid
-from cubicweb.utils import make_uid
-from cubicweb.predicates import match_context_prop, partial_relation_possible
-from cubicweb.appobject import AppObject
-from cubicweb.web import RequestError, htmlwidgets
-
-
-def rtype_facet_title(facet):
- if facet.cw_rset:
- ptypes = facet.cw_rset.column_types(0)
- if len(ptypes) == 1:
- return display_name(facet._cw, facet.rtype, form=facet.role,
- context=next(iter(ptypes)))
- 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):
- """ Return the variable whose name is `mainvar`
- or the first variable selected in column 0
- """
- if mainvar is None:
- vref = next(select.selection[0].iget_nodes(nodes.VariableRef))
- 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 list(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
- allow_hide = 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(int(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 = [text_type(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:
- tr = self._cw._
- else:
- tr = text_type
- if self.rql_sort:
- values = [(tr(label), eid) for eid, label in rset]
- else:
- if self.label_vid is None:
- values = [(tr(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, string_types):
- # 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.values():
- 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 = next(utils.rqlvar_maker(defined=self.select.defined_vars,
- aliases=self.select.aliases))
- obj = self.filtered_variable.name
- else:
- subj = self.filtered_variable.name
- obj = next(utils.rqlvar_maker(defined=self.select.defined_vars,
- aliases=self.select.aliases))
- 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:
- tr = self._cw._
- else:
- tr = text_type
- if self.rql_sort:
- return [(tr(value), value) for value, in rset]
- values = [(tr(value), value) for value, in rset]
- return sorted(values, reverse=not self.sortasc)
-
-
-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, string_types):
- 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 = [text_type(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, string_types):
- 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 = next(iter(attrtypes))
- 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 [(text_type(minv), minv), (text_type(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 text_type(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 AbstractRangeRQLPathFacet(RQLPathFacet):
- """
- The :class:`AbstractRangeRQLPathFacet` is the base class for
- RQLPathFacet-type facets allowing the use of RangeWidgets-like
- widgets (such as (:class:`FacetRangeWidget`,
- class:`DateFacetRangeWidget`) on the parent :class:`RQLPathFacet`
- target attribute.
- """
- __abstract__ = True
-
- def vocabulary(self):
- """return vocabulary for this facet, eg a list of (label,
- value)"""
- select = self.select
- select.save_state()
- try:
- filtered_variable = self.filtered_variable
- cleanup_select(select, filtered_variable)
- varmap, restrvar = self.add_path_to_select()
- if self.label_variable:
- attrvar = varmap[self.label_variable]
- else:
- attrvar = restrvar
- # start RangeRQLPathFacet
- minf = nodes.Function('MIN')
- minf.append(nodes.VariableRef(restrvar))
- select.add_selected(minf)
- maxf = nodes.Function('MAX')
- maxf.append(nodes.VariableRef(restrvar))
- 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)
- # end RangeRQLPathFacet
- 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)
- if rset:
- minv, maxv = rset[0]
- return [(text_type(minv), minv), (text_type(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 add_rql_restrictions(self):
- infvalue = self.infvalue()
- supvalue = self.supvalue()
- if infvalue is None or supvalue is None: # nothing sent
- return
- varmap, restrvar = self.add_path_to_select(
- skiplabel=True, skipattrfilter=True)
- restrel = None
- for part in self.path:
- if isinstance(part, string_types):
- part = part.split()
- subject, rtype, object = part
- if object == self.filter_variable:
- restrel = rtype
- assert restrel
- # 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, '>=', restrvar, restrel)
- if supvalue != self.supvalue(max=True):
- self._add_restriction(supvalue, '<=', restrvar, restrel)
-
- def _add_restriction(self, value, operator, restrvar, restrel):
- self.select.add_constant_restriction(restrvar,
- restrel,
- self.formatvalue(value),
- self.target_attr_type, operator)
-
-
-class RangeRQLPathFacet(AbstractRangeRQLPathFacet, RQLPathFacet):
- """
- The :class:`RangeRQLPathFacet` uses the :class:`FacetRangeWidget`
- on the :class:`AbstractRangeRQLPathFacet` target attribute
- """
- pass
-
-
-class DateRangeRQLPathFacet(AbstractRangeRQLPathFacet, DateRangeFacet):
- """
- The :class:`DateRangeRQLPathFacet` uses the
- :class:`DateFacetRangeWidget` on the
- :class:`AbstractRangeRQLPathFacet` target attribute
- """
- pass
-
-
-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"""
- value = self._cw.form.get(self.__regid__)
- if not value: # no value sent for this facet
- return
- exists = nodes.Exists()
- self.select.add_restriction(exists)
- var = self.select.make_variable()
- if self.role == 'subject':
- subj, obj = self.filtered_variable, var
- else:
- subj, obj = var, self.filtered_variable
- exists.add_relation(subj, self.rtype, obj)
-
-
-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)
- else:
- value = int(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'))
- if value == 0:
- comp.append(nodes.variable_ref(attr_var))
- else:
- 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 not val or val & mask])
-
- def possible_values(self):
- return [text_type(val) for label, val in self.vocabulary()]
-
-
-## html widets ################################################################
-_DEFAULT_VOCAB_WIDGET_HEIGHT = 12
-_DEFAULT_FACET_GROUP_HEIGHT = 15
-
-class FacetVocabularyWidget(htmlwidgets.HTMLWidget):
-
- def __init__(self, facet):
- self.facet = facet
- self.items = []
-
- @cachedproperty
- 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
- """
- return css_em_num_value(self.facet._cw.vreg, 'facet_vocabMaxHeight',
- _DEFAULT_VOCAB_WIDGET_HEIGHT)
-
- @cachedproperty
- def height(self):
- """ title, optional and/or dropdown, len(items) or upper limit """
- return (1.5 + # title + small magic constant
- int(self.facet._support_and_compat() +
- min(len(self.items), self.css_overflow_limit)))
-
- @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 = domid(make_uid(self.facet.__regid__))
- w(u'<div id="%s" class="facet">\n' % facetid)
- cssclass = 'facetTitle'
- if self.facet.allow_hide:
- cssclass += ' hideFacetBody'
- w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' %
- (cssclass, xml_escape(self.facet.__regid__), title))
- if self.facet._support_and_compat():
- self._render_and_or(w)
- cssclass = 'facetBody 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(text_type(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' ' * 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 2.5
-
- 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 = 'facetTitle'
- if self.facet.allow_hide:
- cssclass += ' hideFacetBody'
- w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' %
- (cssclass, xml_escape(self.facet.__regid__), title))
- cssclass = 'facetBody'
- if not self.facet.start_unfolded:
- cssclass += ' hidden'
- w(u'<div class="%s">\n' % cssclass)
- w(u'<input name="%s" type="text" value="%s" />\n' % (
- xml_escape(self.facet.__regid__), self.value or u''))
- w(u'</div>\n')
- 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 2.5
-
- 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 rangeFacet">\n' % facetid)
- cssclass = 'facetTitle'
- if facet.allow_hide:
- cssclass += ' hideFacetBody'
- w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' %
- (cssclass, 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 1.5
-
- 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(text_type(self.value))))
- w(u'<div>')
- w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" /> ' % (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')
-
-
-# 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