')
+
+ 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'\n' % facetid)
+ cssclass = 'facetTitle'
+ if self.facet.allow_hide:
+ cssclass += ' hideFacetBody'
+ w(u'
%s
\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'
\n' % cssclass)
+ for value, label, selected in self.items:
+ if value is None:
+ continue
+ self._render_value(w, value, label, selected, overflow)
+ w(u'
\n')
+ w(u'
\n')
+
+ def _render_and_or(self, w):
+ _ = self.facet._cw._
+ w(u"""
+ %s
+ %s
+ """ % (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'\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('%s ' % xml_escape(label))
+ w(padding)
+ w(u'
')
+
+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'\n' % facetid)
+ cssclass = 'facetTitle'
+ if self.facet.allow_hide:
+ cssclass += ' hideFacetBody'
+ w(u'
%s
\n' %
+ (cssclass, xml_escape(self.facet.__regid__), title))
+ cssclass = 'facetBody'
+ if not self.facet.start_unfolded:
+ cssclass += ' hidden'
+ w(u'
\n' % cssclass)
+ w(u' \n' % (
+ xml_escape(self.facet.__regid__), self.value or u''))
+ w(u'
\n')
+ w(u'
\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'\n' % facetid)
+ cssclass = 'facetTitle'
+ if facet.allow_hide:
+ cssclass += ' hideFacetBody'
+ w(u'
%s
\n' %
+ (cssclass, facetname, title))
+ cssclass = 'facetBody'
+ if not self.facet.start_unfolded:
+ cssclass += ' hidden'
+ w(u'
\n' % cssclass)
+ w(u'
-
'
+ % (sliderid, sliderid))
+ w(u'
'
+ % (facetname, self.minvalue))
+ w(u'
'
+ % (facetname, self.maxvalue))
+ w(u'
'
+ % (facetname, self.minvalue))
+ w(u'
'
+ % (facetname, self.maxvalue))
+ w(u'
' % sliderid)
+ w(u'
\n')
+ w(u'
\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'\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'
\n'
+ % (cssclass, xml_escape(text_type(self.value))))
+ w(u'
')
+ w(u'
' % (imgsrc, imgalt))
+ w(u'
%s '
+ % (xml_escape(self.facet.__regid__), title))
+ w(u'
\n')
+ w(u'
\n')
+ w(u'
\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