diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/facet.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/web/facet.py Sat Jan 16 13:48:51 2016 +0100 @@ -0,0 +1,1787 @@ +# 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 . +""" +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 + + +""" % (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'%s ' % (imgsrc, imgalt)) + w(u'' + % (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