diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/uilib.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/uilib.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,589 @@
+# -*- coding: utf-8 -*-
+# copyright 2003-2011 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 .
+"""user interface libraries
+
+contains some functions designed to help implementation of cubicweb user
+interface.
+"""
+
+__docformat__ = "restructuredtext en"
+
+import csv
+import re
+from io import StringIO
+
+from six import PY2, PY3, text_type, binary_type, string_types, integer_types
+
+from logilab.mtconverter import xml_escape, html_unescape
+from logilab.common.date import ustrftime
+from logilab.common.deprecation import deprecated
+
+from cubicweb import _
+from cubicweb.utils import js_dumps
+
+
+def rql_for_eid(eid):
+ """return the rql query necessary to fetch entity with the given eid. This
+ function should only be used to generate link with rql inside, not to give
+ to cursor.execute (in which case you won't benefit from rql cache).
+
+ :Parameters:
+ - `eid`: the eid of the entity we should search
+ :rtype: str
+ :return: the rql query
+ """
+ return 'Any X WHERE X eid %s' % eid
+
+def eid_param(name, eid):
+ assert name is not None
+ assert eid is not None
+ return '%s:%s' % (name, eid)
+
+def print_bytes(value, req, props, displaytime=True):
+ return u''
+
+def print_string(value, req, props, displaytime=True):
+ # don't translate empty value if you don't want strange results
+ if props is not None and value and props.get('internationalizable'):
+ return req._(value)
+ return value
+
+def print_int(value, req, props, displaytime=True):
+ return text_type(value)
+
+def print_date(value, req, props, displaytime=True):
+ return ustrftime(value, req.property_value('ui.date-format'))
+
+def print_time(value, req, props, displaytime=True):
+ return ustrftime(value, req.property_value('ui.time-format'))
+
+def print_tztime(value, req, props, displaytime=True):
+ return ustrftime(value, req.property_value('ui.time-format')) + u' UTC'
+
+def print_datetime(value, req, props, displaytime=True):
+ if displaytime:
+ return ustrftime(value, req.property_value('ui.datetime-format'))
+ return ustrftime(value, req.property_value('ui.date-format'))
+
+def print_tzdatetime(value, req, props, displaytime=True):
+ if displaytime:
+ return ustrftime(value, req.property_value('ui.datetime-format')) + u' UTC'
+ return ustrftime(value, req.property_value('ui.date-format'))
+
+_('%d years')
+_('%d months')
+_('%d weeks')
+_('%d days')
+_('%d hours')
+_('%d minutes')
+_('%d seconds')
+
+def print_timedelta(value, req, props, displaytime=True):
+ if isinstance(value, integer_types):
+ # `date - date`, unlike `datetime - datetime` gives an int
+ # (number of days), not a timedelta
+ # XXX should rql be fixed to return Int instead of Interval in
+ # that case? that would be probably the proper fix but we
+ # loose information on the way...
+ value = timedelta(days=value)
+ if value.days > 730 or value.days < -730: # 2 years
+ return req._('%d years') % (value.days // 365)
+ elif value.days > 60 or value.days < -60: # 2 months
+ return req._('%d months') % (value.days // 30)
+ elif value.days > 14 or value.days < -14: # 2 weeks
+ return req._('%d weeks') % (value.days // 7)
+ elif value.days > 2 or value.days < -2:
+ return req._('%d days') % int(value.days)
+ else:
+ minus = 1 if value.days >= 0 else -1
+ if value.seconds > 3600:
+ return req._('%d hours') % (int(value.seconds // 3600) * minus)
+ elif value.seconds >= 120:
+ return req._('%d minutes') % (int(value.seconds // 60) * minus)
+ else:
+ return req._('%d seconds') % (int(value.seconds) * minus)
+
+def print_boolean(value, req, props, displaytime=True):
+ if value:
+ return req._('yes')
+ return req._('no')
+
+def print_float(value, req, props, displaytime=True):
+ return text_type(req.property_value('ui.float-format') % value) # XXX cast needed ?
+
+PRINTERS = {
+ 'Bytes': print_bytes,
+ 'String': print_string,
+ 'Int': print_int,
+ 'BigInt': print_int,
+ 'Date': print_date,
+ 'Time': print_time,
+ 'TZTime': print_tztime,
+ 'Datetime': print_datetime,
+ 'TZDatetime': print_tzdatetime,
+ 'Boolean': print_boolean,
+ 'Float': print_float,
+ 'Decimal': print_float,
+ 'Interval': print_timedelta,
+ }
+
+@deprecated('[3.14] use req.printable_value(attrtype, value, ...)')
+def printable_value(req, attrtype, value, props=None, displaytime=True):
+ return req.printable_value(attrtype, value, props, displaytime)
+
+def css_em_num_value(vreg, propname, default):
+ """ we try to read an 'em' css property
+ if we get another unit we're out of luck and resort to the given default
+ (hence, it is strongly advised not to specify but ems for this css prop)
+ """
+ propvalue = vreg.config.uiprops[propname].lower().strip()
+ if propvalue.endswith('em'):
+ try:
+ return float(propvalue[:-2])
+ except Exception:
+ vreg.warning('css property %s looks malformed (%r)',
+ propname, propvalue)
+ else:
+ vreg.warning('css property %s should use em (currently is %r)',
+ propname, propvalue)
+ return default
+
+# text publishing #############################################################
+
+from cubicweb.ext.markdown import markdown_publish # pylint: disable=W0611
+
+try:
+ from cubicweb.ext.rest import rest_publish # pylint: disable=W0611
+except ImportError:
+ def rest_publish(entity, data):
+ """default behaviour if docutils was not found"""
+ return xml_escape(data)
+
+
+TAG_PROG = re.compile(r'?.*?>', re.U)
+def remove_html_tags(text):
+ """Removes HTML tags from text
+
+ >>> remove_html_tags('
hi world | ')
+ 'hi world'
+ >>>
+ """
+ return TAG_PROG.sub('', text)
+
+
+REF_PROG = re.compile(r"[([^<]*)]", re.U)
+def _subst_rql(view, obj):
+ delim, rql, descr = obj.groups()
+ return u'%s' % (view._cw.build_url(rql=rql), descr)
+
+def html_publish(view, text):
+ """replace [ links by """
+ if not text:
+ return u''
+ return REF_PROG.sub(lambda obj, view=view:_subst_rql(view, obj), text)
+
+# fallback implementation, nicer one defined below if lxml> 2.0 is available
+def safe_cut(text, length):
+ """returns a string of length based on , removing any html
+ tags from given text if cut is necessary."""
+ if text is None:
+ return u''
+ noenttext = html_unescape(text)
+ text_nohtml = remove_html_tags(noenttext)
+ # try to keep html tags if text is short enough
+ if len(text_nohtml) <= length:
+ return text
+ # else if un-tagged text is too long, cut it
+ return xml_escape(text_nohtml[:length] + u'...')
+
+fallback_safe_cut = safe_cut
+
+REM_ROOT_HTML_TAGS = re.compile('(body|html)>', re.U)
+
+from lxml import etree, html
+from lxml.html import clean, defs
+
+ALLOWED_TAGS = (defs.general_block_tags | defs.list_tags | defs.table_tags |
+ defs.phrase_tags | defs.font_style_tags |
+ set(('span', 'a', 'br', 'img', 'map', 'area', 'sub', 'sup', 'canvas'))
+ )
+
+CLEANER = clean.Cleaner(allow_tags=ALLOWED_TAGS, remove_unknown_tags=False,
+ style=True, safe_attrs_only=True,
+ add_nofollow=False,
+ )
+
+def soup2xhtml(data, encoding):
+ """tidy html soup by allowing some element tags and return the result
+ """
+ # remove spurious ]