--- a/web/views/facets.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,435 +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 facets box and some basic facets"""
-
-__docformat__ = "restructuredtext en"
-from cubicweb import _
-
-from warnings import warn
-
-from logilab.mtconverter import xml_escape
-from logilab.common.decorators import cachedproperty
-from logilab.common.registry import objectify_predicate, yes
-
-from cubicweb import tags
-from cubicweb.predicates import (non_final_entity, multi_lines_rset,
- match_context_prop, relation_possible)
-from cubicweb.utils import json_dumps
-from cubicweb.uilib import css_em_num_value
-from cubicweb.view import AnyRsetView
-from cubicweb.web import component, facet as facetbase
-from cubicweb.web.views.ajaxcontroller import ajaxfunc
-
-def facets(req, rset, context, mainvar=None, **kwargs):
- """return the base rql and a list of widgets for facets applying to the
- given rset/context (cached version of :func:`_facet`)
-
- :param req: A :class:`~cubicweb.req.RequestSessionBase` object
- :param rset: A :class:`~cubicweb.rset.ResultSet`
- :param context: A string that match the ``__regid__`` of a ``FacetFilter``
- :param mainvar: A string that match a select var from the rset
- """
- try:
- cache = req.__rset_facets
- except AttributeError:
- cache = req.__rset_facets = {}
- try:
- return cache[(rset, context, mainvar)]
- except KeyError:
- facets = _facets(req, rset, context, mainvar, **kwargs)
- cache[(rset, context, mainvar)] = facets
- return facets
-
-def _facets(req, rset, context, mainvar, **kwargs):
- """return the base rql and a list of widgets for facets applying to the
- given rset/context
-
- :param req: A :class:`~cubicweb.req.RequestSessionBase` object
- :param rset: A :class:`~cubicweb.rset.ResultSet`
- :param context: A string that match the ``__regid__`` of a ``FacetFilter``
- :param mainvar: A string that match a select var from the rset
- """
- ### initialisation
- # XXX done by selectors, though maybe necessary when rset has been hijacked
- # (e.g. contextview_selector matched)
- origqlst = rset.syntax_tree()
- # union not yet supported
- if len(origqlst.children) != 1:
- req.debug('facette disabled on union request %s', origqlst)
- return None, ()
- rqlst = origqlst.copy()
- select = rqlst.children[0]
- filtered_variable, baserql = facetbase.init_facets(rset, select, mainvar)
- ### Selection
- possible_facets = req.vreg['facets'].poss_visible_objects(
- req, rset=rset, rqlst=origqlst, select=select,
- context=context, filtered_variable=filtered_variable, **kwargs)
- wdgs = [(facet, facet.get_widget()) for facet in possible_facets]
- return baserql, [wdg for facet, wdg in wdgs if wdg is not None]
-
-
-@objectify_predicate
-def contextview_selector(cls, req, rset=None, row=None, col=None, view=None,
- **kwargs):
- if view:
- try:
- getcontext = getattr(view, 'filter_box_context_info')
- except AttributeError:
- return 0
- rset = getcontext()[0]
- if rset is None or rset.rowcount < 2:
- return 0
- wdgs = facets(req, rset, cls.__regid__, view=view)[1]
- return len(wdgs)
- return 0
-
-@objectify_predicate
-def has_facets(cls, req, rset=None, **kwargs):
- if rset is None or rset.rowcount < 2:
- return 0
- wdgs = facets(req, rset, cls.__regid__, **kwargs)[1]
- return len(wdgs)
-
-
-def filter_hiddens(w, baserql, wdgs, **kwargs):
- kwargs['facets'] = ','.join(wdg.facet.__regid__ for wdg in wdgs)
- kwargs['baserql'] = baserql
- for key, val in kwargs.items():
- w(u'<input type="hidden" name="%s" value="%s" />' % (
- key, xml_escape(val)))
-
-
-class FacetFilterMixIn(object):
- """Mixin Class to generate Facet Filter Form
-
- To generate the form, you need to explicitly call the following method:
-
- .. automethod:: generate_form
-
- The most useful function to override is:
-
- .. automethod:: layout_widgets
- """
-
- needs_js = ['cubicweb.ajax.js', 'cubicweb.facets.js']
- needs_css = ['cubicweb.facets.css']
-
- def generate_form(self, w, rset, divid, vid, vidargs=None, mainvar=None,
- paginate=False, cssclass='', hiddens=None, **kwargs):
- """display a form to filter some view's content
-
- :param w: Write function
-
- :param rset: ResultSet to be filtered
-
- :param divid: Dom ID of the div where the rendering of the view is done.
- :type divid: string
-
- :param vid: ID of the view display in the div
- :type vid: string
-
- :param paginate: Is the view paginated?
- :type paginate: boolean
-
- :param cssclass: Additional css classes to put on the form.
- :type cssclass: string
-
- :param hiddens: other hidden parametters to include in the forms.
- :type hiddens: dict from extra keyword argument
- """
- # XXX Facet.context property hijacks an otherwise well-behaved
- # vocabulary with its own notions
- # Hence we whack here to avoid a clash
- kwargs.pop('context', None)
- baserql, wdgs = facets(self._cw, rset, context=self.__regid__,
- mainvar=mainvar, **kwargs)
- assert wdgs
- self._cw.add_js(self.needs_js)
- self._cw.add_css(self.needs_css)
- self._cw.html_headers.define_var('facetLoadingMsg',
- self._cw._('facet-loading-msg'))
- if vidargs is not None:
- warn("[3.14] vidargs is deprecated. Maybe you're using some TableView?",
- DeprecationWarning, stacklevel=2)
- else:
- vidargs = {}
- vidargs = dict((k, v) for k, v in vidargs.items() if v)
- facetargs = xml_escape(json_dumps([divid, vid, paginate, vidargs]))
- w(u'<form id="%sForm" class="%s" method="post" action="" '
- 'cubicweb:facetargs="%s" >' % (divid, cssclass, facetargs))
- w(u'<fieldset>')
- if hiddens is None:
- hiddens = {}
- if mainvar:
- hiddens['mainvar'] = mainvar
- filter_hiddens(w, baserql, wdgs, **hiddens)
- self.layout_widgets(w, self.sorted_widgets(wdgs))
-
- # <Enter> is supposed to submit the form only if there is a single
- # input:text field. However most browsers will submit the form
- # on <Enter> anyway if there is an input:submit field.
- #
- # see: http://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2
- #
- # Firefox 7.0.1 does not submit form on <Enter> if there is more than a
- # input:text field and not input:submit but does it if there is an
- # input:submit.
- #
- # IE 6 or Firefox 2 behave the same way.
- w(u'<input type="submit" class="hidden" />')
- #
- w(u'</fieldset>\n')
- w(u'</form>\n')
-
- def sorted_widgets(self, wdgs):
- """sort widgets: by default sort by widget height, then according to
- widget.order (the original widgets order)
- """
- return sorted(wdgs, key=lambda x: 99 * (not x.facet.start_unfolded) or x.height )
-
- def layout_widgets(self, w, wdgs):
- """layout widgets: by default simply render each of them
- (i.e. succession of <div>)
- """
- for wdg in wdgs:
- wdg.render(w=w)
-
-
-class FilterBox(FacetFilterMixIn, component.CtxComponent):
- """filter results of a query"""
- __regid__ = 'facet.filterbox'
- __select__ = ((non_final_entity() & has_facets())
- | contextview_selector()) # can't use has_facets because of
- # contextview mecanism
- context = 'left' # XXX doesn't support 'incontext', only 'left' or 'right'
- title = _('facet.filters')
- visible = True # functionality provided by the search box by default
- order = 1
-
- bk_linkbox_template = u'<div class="facetTitle">%s</div>'
-
- def render_body(self, w, **kwargs):
- req = self._cw
- rset, vid, divid, paginate = self._get_context()
- assert len(rset) > 1
- if vid is None:
- vid = req.form.get('vid')
- if self.bk_linkbox_template and req.vreg.schema['Bookmark'].has_perm(req, 'add'):
- w(self.bookmark_link(rset))
- w(self.focus_link(rset))
- hiddens = {}
- for param in ('subvid', 'vtitle'):
- if param in req.form:
- hiddens[param] = req.form[param]
- self.generate_form(w, rset, divid, vid, paginate=paginate,
- hiddens=hiddens, **self.cw_extra_kwargs)
-
- def _get_context(self):
- view = self.cw_extra_kwargs.get('view')
- context = getattr(view, 'filter_box_context_info', lambda: None)()
- if context:
- rset, vid, divid, paginate = context
- else:
- rset = self.cw_rset
- vid, divid = None, 'pageContent'
- paginate = view and view.paginable
- return rset, vid, divid, paginate
-
- def bookmark_link(self, rset):
- req = self._cw
- bk_path = u'rql=%s' % req.url_quote(rset.printable_rql())
- if req.form.get('vid'):
- bk_path += u'&vid=%s' % req.url_quote(req.form['vid'])
- bk_path = u'view?' + bk_path
- bk_title = req._('my custom search')
- linkto = u'bookmarked_by:%s:subject' % req.user.eid
- bkcls = req.vreg['etypes'].etype_class('Bookmark')
- bk_add_url = bkcls.cw_create_url(req, path=bk_path, title=bk_title,
- __linkto=linkto)
- bk_base_url = bkcls.cw_create_url(req, title=bk_title, __linkto=linkto)
- bk_link = u'<a cubicweb:target="%s" id="facetBkLink" href="%s">%s</a>' % (
- xml_escape(bk_base_url), xml_escape(bk_add_url),
- req._('bookmark this search'))
- return self.bk_linkbox_template % bk_link
-
- def focus_link(self, rset):
- return self.bk_linkbox_template % tags.a(self._cw._('focus on this selection'),
- href=self._cw.url(), id='focusLink')
-
-class FilterTable(FacetFilterMixIn, AnyRsetView):
- __regid__ = 'facet.filtertable'
- __select__ = has_facets()
- average_perfacet_uncomputable_overhead = .3
-
- def call(self, vid, divid, vidargs=None, cssclass=''):
- hiddens = self.cw_extra_kwargs.setdefault('hiddens', {})
- hiddens['fromformfilter'] = '1'
- self.generate_form(self.w, self.cw_rset, divid, vid, vidargs=vidargs,
- cssclass=cssclass, **self.cw_extra_kwargs)
-
- @cachedproperty
- def per_facet_height_overhead(self):
- return (css_em_num_value(self._cw.vreg, 'facet_MarginBottom', .2) +
- css_em_num_value(self._cw.vreg, 'facet_Padding', .2) +
- self.average_perfacet_uncomputable_overhead)
-
- def layout_widgets(self, w, wdgs):
- """layout widgets: put them in a table where each column should have
- sum(wdg.height) < wdg_stack_size.
- """
- w(u'<div class="filter">\n')
- widget_queue = []
- queue_height = 0
- wdg_stack_size = facetbase._DEFAULT_FACET_GROUP_HEIGHT
- for wdg in wdgs:
- height = wdg.height + self.per_facet_height_overhead
- if queue_height + height <= wdg_stack_size:
- widget_queue.append(wdg)
- queue_height += height
- continue
- w(u'<div class="facetGroup">')
- for queued in widget_queue:
- queued.render(w=w)
- w(u'</div>')
- widget_queue = [wdg]
- queue_height = height
- if widget_queue:
- w(u'<div class="facetGroup">')
- for queued in widget_queue:
- queued.render(w=w)
- w(u'</div>')
- w(u'</div>\n')
-
-# python-ajax remote functions used by facet widgets #########################
-
-@ajaxfunc(output_type='json')
-def filter_build_rql(self, names, values):
- form = self._rebuild_posted_form(names, values)
- self._cw.form = form
- builder = facetbase.FilterRQLBuilder(self._cw)
- return builder.build_rql()
-
-@ajaxfunc(output_type='json')
-def filter_select_content(self, facetids, rql, mainvar):
- # Union unsupported yet
- select = self._cw.vreg.parse(self._cw, rql).children[0]
- filtered_variable = facetbase.get_filtered_variable(select, mainvar)
- facetbase.prepare_select(select, filtered_variable)
- update_map = {}
- for fid in facetids:
- fobj = facetbase.get_facet(self._cw, fid, select, filtered_variable)
- update_map[fid] = fobj.possible_values()
- return update_map
-
-
-
-# facets ######################################################################
-
-class CWSourceFacet(facetbase.RelationFacet):
- __regid__ = 'cw_source-facet'
- rtype = 'cw_source'
- target_attr = 'name'
-
-class CreatedByFacet(facetbase.RelationFacet):
- __regid__ = 'created_by-facet'
- rtype = 'created_by'
- target_attr = 'login'
-
-class InGroupFacet(facetbase.RelationFacet):
- __regid__ = 'in_group-facet'
- rtype = 'in_group'
- target_attr = 'name'
-
-class InStateFacet(facetbase.RelationAttributeFacet):
- __regid__ = 'in_state-facet'
- rtype = 'in_state'
- target_attr = 'name'
-
-
-# inherit from RelationFacet to benefit from its possible_values implementation
-class ETypeFacet(facetbase.RelationFacet):
- __regid__ = 'etype-facet'
- __select__ = yes()
- order = 1
- rtype = 'is'
- target_attr = 'name'
-
- @property
- def title(self):
- return self._cw._('entity type')
-
- def vocabulary(self):
- """return vocabulary for this facet, eg a list of 2-uple (label, value)
- """
- etypes = self.cw_rset.column_types(0)
- return sorted((self._cw._(etype), etype) for etype in etypes)
-
- 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
- self.select.add_type_restriction(self.filtered_variable, value)
-
- 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:
- facetbase.cleanup_select(select, self.filtered_variable)
- etype_var = facetbase.prepare_vocabulary_select(
- select, self.filtered_variable, self.rtype, self.role)
- attrvar = select.make_variable()
- select.add_selected(attrvar)
- select.add_relation(etype_var, 'name', attrvar)
- return [etype for _, etype in self.rqlexec(select.as_string())]
- finally:
- select.recover()
-
-
-class HasTextFacet(facetbase.AbstractFacet):
- __select__ = relation_possible('has_text', 'subject') & match_context_prop()
- __regid__ = 'has_text-facet'
- rtype = 'has_text'
- role = 'subject'
- order = 0
-
- @property
- def wdgclass(self):
- return facetbase.FacetStringWidget
-
- @property
- def title(self):
- return self._cw._('has_text')
-
- def get_widget(self):
- """return the widget instance to use to display this facet
-
- default implentation expects a .vocabulary method on the facet and
- return a combobox displaying this vocabulary
- """
- return self.wdgclass(self)
-
- 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
- self.select.add_constant_restriction(self.filtered_variable, 'has_text', value, 'String')