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 """ |
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' ' * 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 |
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"/> ' % (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 |