uilib.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
--- a/uilib.py	Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,589 +0,0 @@
-# -*- 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 <http://www.gnu.org/licenses/>.
-"""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('<td>hi <a href="http://www.google.fr">world</a></td>')
-    'hi world'
-    >>>
-    """
-    return TAG_PROG.sub('', text)
-
-
-REF_PROG = re.compile(r"<ref\s+rql=([\'\"])([^\1]*?)\1\s*>([^<]*)</ref>", re.U)
-def _subst_rql(view, obj):
-    delim, rql, descr = obj.groups()
-    return u'<a href="%s">%s</a>' % (view._cw.build_url(rql=rql), descr)
-
-def html_publish(view, text):
-    """replace <ref rql=''> links by <a href="...">"""
-    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 <length> based on <text>, 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 </body> and </html> tags, then normalize line break
-    # (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1)
-    data = REM_ROOT_HTML_TAGS.sub('', u'\n'.join(data.splitlines()))
-    xmltree = etree.HTML(CLEANER.clean_html('<div>%s</div>' % data))
-    # NOTE: lxml 2.0 does support encoding='unicode', but last time I (syt)
-    # tried I got weird results (lxml 2.2.8)
-    body = etree.tostring(xmltree[0], encoding=encoding)
-    # remove <body> and </body> and decode to unicode
-    snippet = body[6:-7].decode(encoding)
-    # take care to bad xhtml (for instance starting with </div>) which
-    # may mess with the <div> we added below. Only remove it if it's
-    # still there...
-    if snippet.startswith('<div>') and snippet.endswith('</div>'):
-        snippet = snippet[5:-6]
-    return snippet
-
-    # lxml.Cleaner envelops text elements by internal logic (not accessible)
-    # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
-    # TODO drop attributes in elements
-    # TODO add policy configuration (content only, embedded content, ...)
-    # XXX this is buggy for "<p>text1</p><p>text2</p>"...
-    # XXX drop these two snippets action and follow the lxml behaviour
-    # XXX (tests need to be updated)
-    # if snippet.startswith('<div>') and snippet.endswith('</div>'):
-    #     snippet = snippet[5:-6]
-    # if snippet.startswith('<p>') and snippet.endswith('</p>'):
-    #     snippet = snippet[3:-4]
-    return snippet.decode(encoding)
-
-if hasattr(etree.HTML('<div>test</div>'), 'iter'): # XXX still necessary?
-    # pylint: disable=E0102
-    def safe_cut(text, length):
-        """returns an html document of length <length> based on <text>,
-        and cut is necessary.
-        """
-        if text is None:
-            return u''
-        dom = etree.HTML(text)
-        curlength = 0
-        add_ellipsis = False
-        for element in dom.iter():
-            if curlength >= length:
-                parent = element.getparent()
-                parent.remove(element)
-                if curlength == length and (element.text or element.tail):
-                    add_ellipsis = True
-            else:
-                if element.text is not None:
-                    element.text = cut(element.text, length - curlength)
-                    curlength += len(element.text)
-                if element.tail is not None:
-                    if curlength < length:
-                        element.tail = cut(element.tail, length - curlength)
-                        curlength += len(element.tail)
-                    elif curlength == length:
-                        element.tail = '...'
-                    else:
-                        element.tail = ''
-        text = etree.tounicode(dom[0])[6:-7] # remove wrapping <body></body>
-        if add_ellipsis:
-            return text + u'...'
-        return text
-
-def text_cut(text, nbwords=30, gotoperiod=True):
-    """from the given plain text, return a text with at least <nbwords> words,
-    trying to go to the end of the current sentence.
-
-    :param nbwords: the minimum number of words required
-    :param gotoperiod: specifies if the function should try to go to
-                       the first period after the cut (i.e. finish
-                       the sentence if possible)
-
-    Note that spaces are normalized.
-    """
-    if text is None:
-        return u''
-    words = text.split()
-    text = u' '.join(words) # normalize spaces
-    textlength = minlength = len(' '.join(words[:nbwords]))
-    if gotoperiod:
-        textlength = text.find('.', minlength) + 1
-        if textlength == 0: # no period found
-            textlength = minlength
-    return text[:textlength]
-
-def cut(text, length):
-    """returns a string of a maximum length <length> based on <text>
-    (approximatively, since if text has been  cut, '...' is added to the end of the string,
-    resulting in a string of len <length> + 3)
-    """
-    if text is None:
-        return u''
-    if len(text) <= length:
-        return text
-    # else if un-tagged text is too long, cut it
-    return text[:length] + u'...'
-
-
-
-# HTML generation helper functions ############################################
-
-class _JSId(object):
-    def __init__(self, id, parent=None):
-        self.id = id
-        self.parent = parent
-    def __unicode__(self):
-        if self.parent:
-            return u'%s.%s' % (self.parent, self.id)
-        return text_type(self.id)
-    __str__ = __unicode__ if PY3 else lambda self: self.__unicode__().encode('utf-8')
-    def __getattr__(self, attr):
-        return _JSId(attr, self)
-    def __call__(self, *args):
-        return _JSCallArgs(args, self)
-
-class _JSCallArgs(_JSId):
-    def __init__(self, args, parent=None):
-        assert isinstance(args, tuple)
-        self.args = args
-        self.parent = parent
-    def __unicode__(self):
-        args = []
-        for arg in self.args:
-            args.append(js_dumps(arg))
-        if self.parent:
-            return u'%s(%s)' % (self.parent, ','.join(args))
-        return ','.join(args)
-    __str__ = __unicode__ if PY3 else lambda self: self.__unicode__().encode('utf-8')
-
-class _JS(object):
-    def __getattr__(self, attr):
-        return _JSId(attr)
-
-js = _JS()
-js.__doc__ = """\
-magic object to return strings suitable to call some javascript function with
-the given arguments (which should be correctly typed).
-
->>> str(js.pouet(1, "2"))
-'pouet(1,"2")'
->>> str(js.cw.pouet(1, "2"))
-'cw.pouet(1,"2")'
->>> str(js.cw.pouet(1, "2").pouet(None))
-'cw.pouet(1,"2").pouet(null)'
->>> str(js.cw.pouet(1, JSString("$")).pouet(None))
-'cw.pouet(1,$).pouet(null)'
->>> str(js.cw.pouet(1, {'callback': JSString("cw.cb")}).pouet(None))
-'cw.pouet(1,{callback: cw.cb}).pouet(null)'
-"""
-
-def domid(string):
-    """return a valid DOM id from a string (should also be usable in jQuery
-    search expression...)
-    """
-    return string.replace('.', '_').replace('-', '_')
-
-HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param',
-                              'img', 'area', 'input', 'col'))
-
-def sgml_attributes(attrs):
-    return u' '.join(u'%s="%s"' % (attr, xml_escape(text_type(value)))
-                     for attr, value in sorted(attrs.items())
-                     if value is not None)
-
-def simple_sgml_tag(tag, content=None, escapecontent=True, **attrs):
-    """generation of a simple sgml tag (eg without children tags) easier
-
-    content and attri butes will be escaped
-    """
-    value = u'<%s' % tag
-    if attrs:
-        try:
-            attrs['class'] = attrs.pop('klass')
-        except KeyError:
-            pass
-        value += u' ' + sgml_attributes(attrs)
-    if content:
-        if escapecontent:
-            content = xml_escape(text_type(content))
-        value += u'>%s</%s>' % (content, tag)
-    else:
-        if tag in HTML4_EMPTY_TAGS:
-            value += u' />'
-        else:
-            value += u'></%s>' % tag
-    return value
-
-def tooltipize(text, tooltip, url=None):
-    """make an HTML tooltip"""
-    url = url or '#'
-    return u'<a href="%s" title="%s">%s</a>' % (url, tooltip, text)
-
-def toggle_action(nodeid):
-    """builds a HTML link that uses the js toggleVisibility function"""
-    return u"javascript: toggleVisibility('%s')" % nodeid
-
-def toggle_link(nodeid, label):
-    """builds a HTML link that uses the js toggleVisibility function"""
-    return u'<a href="%s">%s</a>' % (toggle_action(nodeid), label)
-
-
-def ureport_as_html(layout):
-    from logilab.common.ureports import HTMLWriter
-    formater = HTMLWriter(True)
-    stream = StringIO() #UStringIO() don't want unicode assertion
-    formater.format(layout, stream)
-    res = stream.getvalue()
-    if isinstance(res, binary_type):
-        res = res.decode('UTF8')
-    return res
-
-# traceback formatting ########################################################
-
-import traceback
-
-def exc_message(ex, encoding):
-    if PY3:
-        excmsg = str(ex)
-    else:
-        try:
-            excmsg = unicode(ex)
-        except Exception:
-            try:
-                excmsg = unicode(str(ex), encoding, 'replace')
-            except Exception:
-                excmsg = unicode(repr(ex), encoding, 'replace')
-    exctype = ex.__class__.__name__
-    return u'%s: %s' % (exctype, excmsg)
-
-
-def rest_traceback(info, exception):
-    """return a unicode ReST formated traceback"""
-    res = [u'Traceback\n---------\n::\n']
-    for stackentry in traceback.extract_tb(info[2]):
-        res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3]))
-        if stackentry[3]:
-            data = xml_escape(stackentry[3])
-            if PY2:
-                data = data.decode('utf-8', 'replace')
-            res.append(u'\t  %s' % data)
-    res.append(u'\n')
-    try:
-        res.append(u'\t Error: %s\n' % exception)
-    except Exception:
-        pass
-    return u'\n'.join(res)
-
-
-def html_traceback(info, exception, title='',
-                   encoding='ISO-8859-1', body=''):
-    """ return an html formatted traceback from python exception infos.
-    """
-    tcbk = info[2]
-    stacktb = traceback.extract_tb(tcbk)
-    strings = []
-    if body:
-        strings.append(u'<div class="error_body">')
-        # FIXME
-        strings.append(body)
-        strings.append(u'</div>')
-    if title:
-        strings.append(u'<h1 class="error">%s</h1>'% xml_escape(title))
-    try:
-        strings.append(u'<p class="error">%s</p>' % xml_escape(str(exception)).replace("\n","<br />"))
-    except UnicodeError:
-        pass
-    strings.append(u'<div class="error_traceback">')
-    for index, stackentry in enumerate(stacktb):
-        strings.append(u'<b>File</b> <b class="file">%s</b>, <b>line</b> '
-                       u'<b class="line">%s</b>, <b>function</b> '
-                       u'<b class="function">%s</b>:<br/>'%(
-            xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2])))
-        if stackentry[3]:
-            string = xml_escape(stackentry[3])
-            if PY2:
-                string = string.decode('utf-8', 'replace')
-            strings.append(u'&#160;&#160;%s<br/>\n' % (string))
-        # add locals info for each entry
-        try:
-            local_context = tcbk.tb_frame.f_locals
-            html_info = []
-            chars = 0
-            for name, value in local_context.items():
-                value = xml_escape(repr(value))
-                info = u'<span class="name">%s</span>=%s, ' % (name, value)
-                line_length = len(name) + len(value)
-                chars += line_length
-                # 150 is the result of *years* of research ;-) (CSS might be helpful here)
-                if chars > 150:
-                    info = u'<br/>' + info
-                    chars = line_length
-                html_info.append(info)
-            boxid = 'ctxlevel%d' % index
-            strings.append(u'[%s]' % toggle_link(boxid, '+'))
-            strings.append(u'<div id="%s" class="pycontext hidden">%s</div>' %
-                           (boxid, ''.join(html_info)))
-            tcbk = tcbk.tb_next
-        except Exception:
-            pass # doesn't really matter if we have no context info
-    strings.append(u'</div>')
-    return '\n'.join(strings)
-
-# csv files / unicode support #################################################
-
-class UnicodeCSVWriter:
-    """proxies calls to csv.writer.writerow to be able to deal with unicode
-
-    Under Python 3, this code no longer encodes anything."""
-
-    def __init__(self, wfunc, encoding, **kwargs):
-        self.writer = csv.writer(self, **kwargs)
-        self.wfunc = wfunc
-        self.encoding = encoding
-
-    def write(self, data):
-        self.wfunc(data)
-
-    def writerow(self, row):
-        if PY3:
-            self.writer.writerow(row)
-            return
-        csvrow = []
-        for elt in row:
-            if isinstance(elt, text_type):
-                csvrow.append(elt.encode(self.encoding))
-            else:
-                csvrow.append(str(elt))
-        self.writer.writerow(csvrow)
-
-    def writerows(self, rows):
-        for row in rows:
-            self.writerow(row)
-
-
-# some decorators #############################################################
-
-class limitsize(object):
-    def __init__(self, maxsize):
-        self.maxsize = maxsize
-
-    def __call__(self, function):
-        def newfunc(*args, **kwargs):
-            ret = function(*args, **kwargs)
-            if isinstance(ret, string_types):
-                return ret[:self.maxsize]
-            return ret
-        return newfunc
-
-
-def htmlescape(function):
-    def newfunc(*args, **kwargs):
-        ret = function(*args, **kwargs)
-        assert isinstance(ret, string_types)
-        return xml_escape(ret)
-    return newfunc