web/facet.py
changeset 7943 ad0581296e2c
parent 7879 9aae456abab5
child 7953 a37531c8a4a6
equal deleted inserted replaced
7942:d12c21ea4cd4 7943:ad0581296e2c
    48 __docformat__ = "restructuredtext en"
    48 __docformat__ = "restructuredtext en"
    49 _ = unicode
    49 _ = unicode
    50 
    50 
    51 from warnings import warn
    51 from warnings import warn
    52 from copy import deepcopy
    52 from copy import deepcopy
    53 from datetime import date, datetime, timedelta
    53 from datetime import datetime, timedelta
    54 
    54 
    55 from logilab.mtconverter import xml_escape
    55 from logilab.mtconverter import xml_escape
    56 from logilab.common.graph import has_path
    56 from logilab.common.graph import has_path
    57 from logilab.common.decorators import cached
    57 from logilab.common.decorators import cached
    58 from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
    58 from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
    59 from logilab.common.compat import all
    59 from logilab.common.compat import all
    60 from logilab.common.deprecation import deprecated
    60 from logilab.common.deprecation import deprecated
    61 
    61 
    62 from rql import parse, nodes, utils
    62 from rql import nodes, utils
    63 
    63 
    64 from cubicweb import Unauthorized, typed_eid
    64 from cubicweb import Unauthorized, typed_eid
    65 from cubicweb.schema import display_name
    65 from cubicweb.schema import display_name
    66 from cubicweb.utils import make_uid
    66 from cubicweb.utils import make_uid
    67 from cubicweb.selectors import match_context_prop, partial_relation_possible, yes
    67 from cubicweb.selectors import match_context_prop, partial_relation_possible, yes
   467 
   467 
   468     @property
   468     @property
   469     def wdgclass(self):
   469     def wdgclass(self):
   470         return FacetVocabularyWidget
   470         return FacetVocabularyWidget
   471 
   471 
       
   472     def get_selected(self):
       
   473         return frozenset(typed_eid(eid)
       
   474                          for eid in self._cw.list_form_param(self.__regid__))
       
   475 
   472     def get_widget(self):
   476     def get_widget(self):
   473         """Return the widget instance to use to display this facet.
   477         """Return the widget instance to use to display this facet.
   474 
   478 
   475         This implementation expects a .vocabulary method on the facet and
   479         This implementation expects a .vocabulary method on the facet and
   476         return a combobox displaying this vocabulary.
   480         return a combobox displaying this vocabulary.
   477         """
   481         """
   478         vocab = self.vocabulary()
   482         vocab = self.vocabulary()
   479         if len(vocab) <= 1:
   483         if len(vocab) <= 1:
   480             return None
   484             return None
   481         wdg = self.wdgclass(self)
   485         wdg = self.wdgclass(self)
   482         selected = frozenset(typed_eid(eid) for eid in self._cw.list_form_param(self.__regid__))
   486         selected = self.get_selected()
   483         for label, value in vocab:
   487         for label, value in vocab:
   484             if value is None:
   488             wdg.items.append((value, label, value in selected))
   485                 wdg.append(FacetSeparator(label))
       
   486             else:
       
   487                 wdg.append(FacetItem(self._cw, label, value, value in selected))
       
   488         return wdg
   489         return wdg
   489 
   490 
   490     def vocabulary(self):
   491     def vocabulary(self):
   491         """Return vocabulary for this facet, eg a list of 2-uple (label, value).
   492         """Return vocabulary for this facet, eg a list of 2-uple (label, value).
   492         """
   493         """
   495     def possible_values(self):
   496     def possible_values(self):
   496         """Return a list of possible values (as string since it's used to
   497         """Return a list of possible values (as string since it's used to
   497         compare to a form value in javascript) for this facet.
   498         compare to a form value in javascript) for this facet.
   498         """
   499         """
   499         raise NotImplementedError
   500         raise NotImplementedError
   500 
       
   501 
   501 
   502 
   502 
   503 class RelationFacet(VocabularyFacet):
   503 class RelationFacet(VocabularyFacet):
   504     """Base facet to filter some entities according to other entities to which
   504     """Base facet to filter some entities according to other entities to which
   505     they are related. Create concrete facet by inheriting from this class an then
   505     they are related. Create concrete facet by inheriting from this class an then
   709     def support_and(self):
   709     def support_and(self):
   710         return self._search_card('+*')
   710         return self._search_card('+*')
   711 
   711 
   712     # internal utilities #######################################################
   712     # internal utilities #######################################################
   713 
   713 
       
   714     @cached
   714     def _support_and_compat(self):
   715     def _support_and_compat(self):
   715         support = self.support_and
   716         support = self.support_and
   716         if callable(support):
   717         if callable(support):
   717             warn('[3.13] %s.support_and is now a property' % self.__class__,
   718             warn('[3.13] %s.support_and is now a property' % self.__class__,
   718                  DeprecationWarning)
   719                  DeprecationWarning)
  1337         else:
  1338         else:
  1338             self.select.add_relation(var, self.rtype, self.filtered_variable)
  1339             self.select.add_relation(var, self.rtype, self.filtered_variable)
  1339 
  1340 
  1340 
  1341 
  1341 ## html widets ################################################################
  1342 ## html widets ################################################################
  1342 _DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT = 9
  1343 _DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT = 12
  1343 
       
  1344 @cached
       
  1345 def _css_height_to_line_count(vreg):
       
  1346     cssprop = vreg.config.uiprops['facet_overflowedHeight'].lower().strip()
       
  1347     # let's talk a bit ...
       
  1348     # we try to deduce a number of displayed lines from a css property
       
  1349     # there is a linear (rough empiric coefficient == 0.73) relation between
       
  1350     # css _em_ value and line qty
       
  1351     # if we get another unit we're out of luck and resort to one constant
       
  1352     # hence, it is strongly advised not to specify but ems for this css prop
       
  1353     if cssprop.endswith('em'):
       
  1354         try:
       
  1355             return int(cssprop[:-2]) * .73
       
  1356         except Exception:
       
  1357             vreg.warning('css property facet_overflowedHeight looks malformed (%r)',
       
  1358                          cssprop)
       
  1359     return _DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT
       
  1360 
  1344 
  1361 class FacetVocabularyWidget(htmlwidgets.HTMLWidget):
  1345 class FacetVocabularyWidget(htmlwidgets.HTMLWidget):
  1362 
  1346 
  1363     def __init__(self, facet):
  1347     def __init__(self, facet):
  1364         self.facet = facet
  1348         self.facet = facet
  1365         self.items = []
  1349         self.items = []
  1366 
  1350 
       
  1351     @property
       
  1352     @cached
       
  1353     def css_overflow_limit(self):
       
  1354         """ we try to deduce a number of displayed lines from a css property
       
  1355         if we get another unit we're out of luck and resort to one constant
       
  1356         hence, it is strongly advised not to specify but ems for this css prop
       
  1357         """
       
  1358         vreg = self.facet._cw.vreg
       
  1359         cssprop = vreg.config.uiprops['facet_overflowedHeight'].lower().strip()
       
  1360         if cssprop.endswith('em'):
       
  1361             try:
       
  1362                 return int(cssprop[:-2])
       
  1363             except Exception:
       
  1364                 vreg.warning('css property facet_overflowedHeight looks malformed (%r)',
       
  1365                              cssprop)
       
  1366         return _DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT
       
  1367 
       
  1368     @property
  1367     @cached
  1369     @cached
  1368     def height(self):
  1370     def height(self):
  1369         maxheight = _css_height_to_line_count(self.facet._cw.vreg)
  1371         return 1 + min(len(self.items),
  1370         return 1 + min(len(self.items), maxheight) + int(self.facet._support_and_compat())
  1372                        self.css_overflow_limit + int(self.facet._support_and_compat()))
  1371 
  1373 
  1372     def append(self, item):
  1374     @property
  1373         self.items.append(item)
  1375     @cached
       
  1376     def overflows(self):
       
  1377         return len(self.items) >= self.css_overflow_limit
       
  1378 
       
  1379     scrollbar_padding_factor = 4
  1374 
  1380 
  1375     def _render(self):
  1381     def _render(self):
  1376         w = self.w
  1382         w = self.w
  1377         title = xml_escape(self.facet.title)
  1383         title = xml_escape(self.facet.title)
  1378         facetid = make_uid(self.facet.__regid__)
  1384         facetid = make_uid(self.facet.__regid__)
  1379         w(u'<div id="%s" class="facet">\n' % facetid)
  1385         w(u'<div id="%s" class="facet">\n' % facetid)
  1380         w(u'<div class="facetTitle" cubicweb:facetName="%s">%s</div>\n' %
  1386         w(u'<div class="facetTitle" cubicweb:facetName="%s">%s</div>\n' %
  1381           (xml_escape(self.facet.__regid__), title))
  1387           (xml_escape(self.facet.__regid__), title))
  1382         if self.facet._support_and_compat():
  1388         if self.facet._support_and_compat():
  1383             _ = self.facet._cw._
  1389             self._render_and_or(w)
  1384             w(u'''<select name="%s" class="radio facetOperator" title="%s">
  1390         cssclass = 'vocabularyFacet'
  1385   <option value="OR">%s</option>
       
  1386   <option value="AND">%s</option>
       
  1387 </select>''' % (xml_escape(self.facet.__regid__) + '_andor', _('and/or between different values'),
       
  1388                 _('OR'), _('AND')))
       
  1389         cssclass = 'facetBody'
       
  1390         if not self.facet.start_unfolded:
  1391         if not self.facet.start_unfolded:
  1391             cssclass += ' hidden'
  1392             cssclass += ' hidden'
  1392         if len(self.items) > 6:
  1393         overflow = self.overflows
  1393             cssclass += ' overflowed'
  1394         if overflow:
       
  1395             if self.facet._support_and_compat():
       
  1396                 cssclass += ' vocabularyFacetBodyWithLogicalSelector'
       
  1397             else:
       
  1398                 cssclass += ' vocabularyFacetBody'
  1394         w(u'<div class="%s">\n' % cssclass)
  1399         w(u'<div class="%s">\n' % cssclass)
  1395         for item in self.items:
  1400         for value, label, selected in self.items:
  1396             item.render(w=w)
  1401             if value is None:
       
  1402                 continue
       
  1403             self._render_value(w, value, label, selected, overflow)
  1397         w(u'</div>\n')
  1404         w(u'</div>\n')
  1398         w(u'</div>\n')
  1405         w(u'</div>\n')
  1399 
  1406 
       
  1407     def _render_and_or(self, w):
       
  1408         _ = self.facet._cw._
       
  1409         w(u"""<select name='%s' class='radio facetOperator' title='%s'>
       
  1410   <option value='OR'>%s</option>
       
  1411   <option value='AND'>%s</option>
       
  1412 </select>""" % (xml_escape(self.facet.__regid__) + '_andor',
       
  1413                 _('and/or between different values'),
       
  1414                 _('OR'), _('AND')))
       
  1415 
       
  1416     def _render_value(self, w, value, label, selected, overflow):
       
  1417         cssclass = 'facetValue facetCheckBox'
       
  1418         if selected:
       
  1419             cssclass += ' facetValueSelected'
       
  1420         w(u'<div class="%s" cubicweb:value="%s">\n'
       
  1421           % (cssclass, xml_escape(unicode(value))))
       
  1422         # If it is overflowed one must add padding to compensate for the vertical
       
  1423         # scrollbar; given current css values, 4 blanks work perfectly ...
       
  1424         padding = u'&#160;' * self.scrollbar_padding_factor if overflow else u''
       
  1425         w('<span>%s</span>' % xml_escape(label))
       
  1426         w(padding)
       
  1427         w(u'</div>')
  1400 
  1428 
  1401 class FacetStringWidget(htmlwidgets.HTMLWidget):
  1429 class FacetStringWidget(htmlwidgets.HTMLWidget):
  1402     def __init__(self, facet):
  1430     def __init__(self, facet):
  1403         self.facet = facet
  1431         self.facet = facet
  1404         self.value = None
  1432         self.value = None
  1405 
  1433 
       
  1434     @property
  1406     def height(self):
  1435     def height(self):
  1407         return 3
  1436         return 3
  1408 
  1437 
  1409     def _render(self):
  1438     def _render(self):
  1410         w = self.w
  1439         w = self.w
  1445     def __init__(self, facet, minvalue, maxvalue):
  1474     def __init__(self, facet, minvalue, maxvalue):
  1446         self.facet = facet
  1475         self.facet = facet
  1447         self.minvalue = minvalue
  1476         self.minvalue = minvalue
  1448         self.maxvalue = maxvalue
  1477         self.maxvalue = maxvalue
  1449 
  1478 
       
  1479     @property
  1450     def height(self):
  1480     def height(self):
  1451         return 3
  1481         return 3
  1452 
  1482 
  1453     def _render(self):
  1483     def _render(self):
  1454         w = self.w
  1484         w = self.w
  1505                                                    datetime2ticks(maxvalue))
  1535                                                    datetime2ticks(maxvalue))
  1506         fmt = facet._cw.property_value('ui.date-format')
  1536         fmt = facet._cw.property_value('ui.date-format')
  1507         facet._cw.html_headers.define_var('DATE_FMT', fmt)
  1537         facet._cw.html_headers.define_var('DATE_FMT', fmt)
  1508 
  1538 
  1509 
  1539 
  1510 class FacetItem(htmlwidgets.HTMLWidget):
       
  1511 
       
  1512     selected_img = "black-check.png"
       
  1513     unselected_img = "no-check-no-border.png"
       
  1514 
       
  1515     def __init__(self, req, label, value, selected=False):
       
  1516         self._cw = req
       
  1517         self.label = label
       
  1518         self.value = value
       
  1519         self.selected = selected
       
  1520 
       
  1521     def _render(self):
       
  1522         w = self.w
       
  1523         cssclass = 'facetValue facetCheckBox'
       
  1524         if self.selected:
       
  1525             cssclass += ' facetValueSelected'
       
  1526             imgsrc = self._cw.data_url(self.selected_img)
       
  1527             imgalt = self._cw._('selected')
       
  1528         else:
       
  1529             imgsrc = self._cw.data_url(self.unselected_img)
       
  1530             imgalt = self._cw._('not selected')
       
  1531         w(u'<div class="%s" cubicweb:value="%s">\n'
       
  1532           % (cssclass, xml_escape(unicode(self.value))))
       
  1533         w(u'<img src="%s" alt="%s"/>&#160;' % (imgsrc, imgalt))
       
  1534         w(u'<a href="javascript: {}">%s</a>' % xml_escape(self.label))
       
  1535         w(u'</div>')
       
  1536 
       
  1537 
       
  1538 class CheckBoxFacetWidget(htmlwidgets.HTMLWidget):
  1540 class CheckBoxFacetWidget(htmlwidgets.HTMLWidget):
  1539     selected_img = "black-check.png"
  1541     selected_img = "black-check.png"
  1540     unselected_img = "black-uncheck.png"
  1542     unselected_img = "black-uncheck.png"
  1541 
  1543 
  1542     def __init__(self, req, facet, value, selected):
  1544     def __init__(self, req, facet, value, selected):
  1543         self._cw = req
  1545         self._cw = req
  1544         self.facet = facet
  1546         self.facet = facet
  1545         self.value = value
  1547         self.value = value
  1546         self.selected = selected
  1548         self.selected = selected
  1547 
  1549 
       
  1550     @property
  1548     def height(self):
  1551     def height(self):
  1549         return 2
  1552         return 2
  1550 
  1553 
  1551     def _render(self):
  1554     def _render(self):
  1552         w = self.w
  1555         w = self.w
  1561         else:
  1564         else:
  1562             imgsrc = self._cw.data_url(self.unselected_img)
  1565             imgsrc = self._cw.data_url(self.unselected_img)
  1563             imgalt = self._cw._('not selected')
  1566             imgalt = self._cw._('not selected')
  1564         w(u'<div class="%s" cubicweb:value="%s">\n'
  1567         w(u'<div class="%s" cubicweb:value="%s">\n'
  1565           % (cssclass, xml_escape(unicode(self.value))))
  1568           % (cssclass, xml_escape(unicode(self.value))))
  1566         w(u'<div class="facetCheckBoxWidget">')
  1569         w(u'<div>')
  1567         w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" />&#160;' % (imgsrc, imgalt))
  1570         w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" />&#160;' % (imgsrc, imgalt))
  1568         w(u'<label class="facetTitle" cubicweb:facetName="%s"><a href="javascript: {}">%s</a></label>'
  1571         w(u'<label class="facetTitle" cubicweb:facetName="%s">%s</label>'
  1569           % (xml_escape(self.facet.__regid__), title))
  1572           % (xml_escape(self.facet.__regid__), title))
  1570         w(u'</div>\n')
  1573         w(u'</div>\n')
  1571         w(u'</div>\n')
  1574         w(u'</div>\n')
  1572         w(u'</div>\n')
  1575         w(u'</div>\n')
  1573 
  1576