common/uilib.py
changeset 4023 eae23c40627a
parent 3418 7b49fa7e942d
child 4252 6c4f109c2b03
equal deleted inserted replaced
4022:934e758a73ef 4023:eae23c40627a
     1 # -*- coding: utf-8 -*-
     1 """pre 3.6 bw compat"""
     2 """user interface libraries
     2 # pylint: disable-msg=W0614,W0401
     3 
     3 from warnings import warn
     4 contains some functions designed to help implementation of cubicweb user interface
     4 warn('moved to cubicweb.uilib', DeprecationWarning, stacklevel=2)
     5 
     5 from cubicweb.uilib import *
     6 :organization: Logilab
       
     7 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
       
     8 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     9 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
       
    10 """
       
    11 __docformat__ = "restructuredtext en"
       
    12 
       
    13 import csv
       
    14 import re
       
    15 from StringIO import StringIO
       
    16 
       
    17 from logilab.mtconverter import xml_escape, html_unescape
       
    18 
       
    19 from cubicweb.utils import ustrftime
       
    20 
       
    21 def rql_for_eid(eid):
       
    22     """return the rql query necessary to fetch entity with the given eid.  This
       
    23     function should only be used to generate link with rql inside, not to give
       
    24     to cursor.execute (in which case you won't benefit from rql cache).
       
    25 
       
    26     :Parameters:
       
    27       - `eid`: the eid of the entity we should search
       
    28     :rtype: str
       
    29     :return: the rql query
       
    30     """
       
    31     return 'Any X WHERE X eid %s' % eid
       
    32 
       
    33 
       
    34 def printable_value(req, attrtype, value, props=None, displaytime=True):
       
    35     """return a displayable value (i.e. unicode string)"""
       
    36     if value is None or attrtype == 'Bytes':
       
    37         return u''
       
    38     if attrtype == 'String':
       
    39         # don't translate empty value if you don't want strange results
       
    40         if props is not None and value and props.get('internationalizable'):
       
    41             return req._(value)
       
    42 
       
    43         return value
       
    44     if attrtype == 'Date':
       
    45         return ustrftime(value, req.property_value('ui.date-format'))
       
    46     if attrtype == 'Time':
       
    47         return ustrftime(value, req.property_value('ui.time-format'))
       
    48     if attrtype == 'Datetime':
       
    49         if displaytime:
       
    50             return ustrftime(value, req.property_value('ui.datetime-format'))
       
    51         return ustrftime(value, req.property_value('ui.date-format'))
       
    52     if attrtype == 'Boolean':
       
    53         if value:
       
    54             return req._('yes')
       
    55         return req._('no')
       
    56     if attrtype == 'Float':
       
    57         value = req.property_value('ui.float-format') % value
       
    58     return unicode(value)
       
    59 
       
    60 
       
    61 # text publishing #############################################################
       
    62 
       
    63 try:
       
    64     from cubicweb.ext.rest import rest_publish # pylint: disable-msg=W0611
       
    65 except ImportError:
       
    66     def rest_publish(entity, data):
       
    67         """default behaviour if docutils was not found"""
       
    68         return xml_escape(data)
       
    69 
       
    70 TAG_PROG = re.compile(r'</?.*?>', re.U)
       
    71 def remove_html_tags(text):
       
    72     """Removes HTML tags from text
       
    73 
       
    74     >>> remove_html_tags('<td>hi <a href="http://www.google.fr">world</a></td>')
       
    75     'hi world'
       
    76     >>>
       
    77     """
       
    78     return TAG_PROG.sub('', text)
       
    79 
       
    80 
       
    81 REF_PROG = re.compile(r"<ref\s+rql=([\'\"])([^\1]*?)\1\s*>([^<]*)</ref>", re.U)
       
    82 def _subst_rql(view, obj):
       
    83     delim, rql, descr = obj.groups()
       
    84     return u'<a href="%s">%s</a>' % (view._cw.build_url(rql=rql), descr)
       
    85 
       
    86 def html_publish(view, text):
       
    87     """replace <ref rql=''> links by <a href="...">"""
       
    88     if not text:
       
    89         return u''
       
    90     return REF_PROG.sub(lambda obj, view=view:_subst_rql(view, obj), text)
       
    91 
       
    92 # fallback implementation, nicer one defined below if lxml is available
       
    93 def soup2xhtml(data, encoding):
       
    94     # normalize line break
       
    95     # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
       
    96     return u'\n'.join(data.splitlines())
       
    97 
       
    98 # fallback implementation, nicer one defined below if lxml> 2.0 is available
       
    99 def safe_cut(text, length):
       
   100     """returns a string of length <length> based on <text>, removing any html
       
   101     tags from given text if cut is necessary."""
       
   102     if text is None:
       
   103         return u''
       
   104     noenttext = html_unescape(text)
       
   105     text_nohtml = remove_html_tags(noenttext)
       
   106     # try to keep html tags if text is short enough
       
   107     if len(text_nohtml) <= length:
       
   108         return text
       
   109     # else if un-tagged text is too long, cut it
       
   110     return xml_escape(text_nohtml[:length] + u'...')
       
   111 
       
   112 fallback_safe_cut = safe_cut
       
   113 
       
   114 
       
   115 try:
       
   116     from lxml import etree
       
   117 except (ImportError, AttributeError):
       
   118     # gae environment: lxml not available
       
   119     pass
       
   120 else:
       
   121 
       
   122     def soup2xhtml(data, encoding):
       
   123         """tidy (at least try) html soup and return the result
       
   124         Note: the function considers a string with no surrounding tag as valid
       
   125               if <div>`data`</div> can be parsed by an XML parser
       
   126         """
       
   127         # normalize line break
       
   128         # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
       
   129         data = u'\n'.join(data.splitlines())
       
   130         # XXX lxml 1.1 support still needed ?
       
   131         xmltree = etree.HTML('<div>%s</div>' % data)
       
   132         # NOTE: lxml 1.1 (etch platforms) doesn't recognize
       
   133         #       the encoding=unicode parameter (lxml 2.0 does), this is
       
   134         #       why we specify an encoding and re-decode to unicode later
       
   135         body = etree.tostring(xmltree[0], encoding=encoding)
       
   136         # remove <body> and </body> and decode to unicode
       
   137         return body[11:-13].decode(encoding)
       
   138 
       
   139     if hasattr(etree.HTML('<div>test</div>'), 'iter'):
       
   140 
       
   141         def safe_cut(text, length):
       
   142             """returns an html document of length <length> based on <text>,
       
   143             and cut is necessary.
       
   144             """
       
   145             if text is None:
       
   146                 return u''
       
   147             dom = etree.HTML(text)
       
   148             curlength = 0
       
   149             add_ellipsis = False
       
   150             for element in dom.iter():
       
   151                 if curlength >= length:
       
   152                     parent = element.getparent()
       
   153                     parent.remove(element)
       
   154                     if curlength == length and (element.text or element.tail):
       
   155                         add_ellipsis = True
       
   156                 else:
       
   157                     if element.text is not None:
       
   158                         element.text = cut(element.text, length - curlength)
       
   159                         curlength += len(element.text)
       
   160                     if element.tail is not None:
       
   161                         if curlength < length:
       
   162                             element.tail = cut(element.tail, length - curlength)
       
   163                             curlength += len(element.tail)
       
   164                         elif curlength == length:
       
   165                             element.tail = '...'
       
   166                         else:
       
   167                             element.tail = ''
       
   168             text = etree.tounicode(dom[0])[6:-7] # remove wrapping <body></body>
       
   169             if add_ellipsis:
       
   170                 return text + u'...'
       
   171             return text
       
   172 
       
   173 def text_cut(text, nbwords=30, gotoperiod=True):
       
   174     """from the given plain text, return a text with at least <nbwords> words,
       
   175     trying to go to the end of the current sentence.
       
   176 
       
   177     :param nbwords: the minimum number of words required
       
   178     :param gotoperiod: specifies if the function should try to go to
       
   179                        the first period after the cut (i.e. finish
       
   180                        the sentence if possible)
       
   181 
       
   182     Note that spaces are normalized.
       
   183     """
       
   184     if text is None:
       
   185         return u''
       
   186     words = text.split()
       
   187     text = u' '.join(words) # normalize spaces
       
   188     textlength = minlength = len(' '.join(words[:nbwords]))
       
   189     if gotoperiod:
       
   190         textlength = text.find('.', minlength) + 1
       
   191         if textlength == 0: # no period found
       
   192             textlength = minlength
       
   193     return text[:textlength]
       
   194 
       
   195 def cut(text, length):
       
   196     """returns a string of a maximum length <length> based on <text>
       
   197     (approximatively, since if text has been  cut, '...' is added to the end of the string,
       
   198     resulting in a string of len <length> + 3)
       
   199     """
       
   200     if text is None:
       
   201         return u''
       
   202     if len(text) <= length:
       
   203         return text
       
   204     # else if un-tagged text is too long, cut it
       
   205     return text[:length] + u'...'
       
   206 
       
   207 
       
   208 
       
   209 # HTML generation helper functions ############################################
       
   210 
       
   211 HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param',
       
   212                               'img', 'area', 'input', 'col'))
       
   213 
       
   214 def sgml_attributes(attrs):
       
   215     return u' '.join(u'%s="%s"' % (attr, xml_escape(unicode(value)))
       
   216                      for attr, value in sorted(attrs.items())
       
   217                      if value is not None)
       
   218 
       
   219 def simple_sgml_tag(tag, content=None, escapecontent=True, **attrs):
       
   220     """generation of a simple sgml tag (eg without children tags) easier
       
   221 
       
   222     content and attri butes will be escaped
       
   223     """
       
   224     value = u'<%s' % tag
       
   225     if attrs:
       
   226         try:
       
   227             attrs['class'] = attrs.pop('klass')
       
   228         except KeyError:
       
   229             pass
       
   230         value += u' ' + sgml_attributes(attrs)
       
   231     if content:
       
   232         if escapecontent:
       
   233             content = xml_escape(unicode(content))
       
   234         value += u'>%s</%s>' % (content, tag)
       
   235     else:
       
   236         if tag in HTML4_EMPTY_TAGS:
       
   237             value += u' />'
       
   238         else:
       
   239             value += u'></%s>' % tag
       
   240     return value
       
   241 
       
   242 def tooltipize(text, tooltip, url=None):
       
   243     """make an HTML tooltip"""
       
   244     url = url or '#'
       
   245     return u'<a href="%s" title="%s">%s</a>' % (url, tooltip, text)
       
   246 
       
   247 def toggle_action(nodeid):
       
   248     """builds a HTML link that uses the js toggleVisibility function"""
       
   249     return u"javascript: toggleVisibility('%s')" % nodeid
       
   250 
       
   251 def toggle_link(nodeid, label):
       
   252     """builds a HTML link that uses the js toggleVisibility function"""
       
   253     return u'<a href="%s">%s</a>' % (toggle_action(nodeid), label)
       
   254 
       
   255 
       
   256 def ureport_as_html(layout):
       
   257     from logilab.common.ureports import HTMLWriter
       
   258     formater = HTMLWriter(True)
       
   259     stream = StringIO() #UStringIO() don't want unicode assertion
       
   260     formater.format(layout, stream)
       
   261     res = stream.getvalue()
       
   262     if isinstance(res, str):
       
   263         res = unicode(res, 'UTF8')
       
   264     return res
       
   265 
       
   266 # traceback formatting ########################################################
       
   267 
       
   268 import traceback
       
   269 
       
   270 def rest_traceback(info, exception):
       
   271     """return a ReST formated traceback"""
       
   272     res = [u'Traceback\n---------\n::\n']
       
   273     for stackentry in traceback.extract_tb(info[2]):
       
   274         res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3]))
       
   275         if stackentry[3]:
       
   276             res.append(u'\t  %s' % stackentry[3].decode('utf-8', 'replace'))
       
   277     res.append(u'\n')
       
   278     try:
       
   279         res.append(u'\t Error: %s\n' % exception)
       
   280     except:
       
   281         pass
       
   282     return u'\n'.join(res)
       
   283 
       
   284 
       
   285 def html_traceback(info, exception, title='',
       
   286                    encoding='ISO-8859-1', body=''):
       
   287     """ return an html formatted traceback from python exception infos.
       
   288     """
       
   289     tcbk = info[2]
       
   290     stacktb = traceback.extract_tb(tcbk)
       
   291     strings = []
       
   292     if body:
       
   293         strings.append(u'<div class="error_body">')
       
   294         # FIXME
       
   295         strings.append(body)
       
   296         strings.append(u'</div>')
       
   297     if title:
       
   298         strings.append(u'<h1 class="error">%s</h1>'% xml_escape(title))
       
   299     try:
       
   300         strings.append(u'<p class="error">%s</p>' % xml_escape(str(exception)).replace("\n","<br />"))
       
   301     except UnicodeError:
       
   302         pass
       
   303     strings.append(u'<div class="error_traceback">')
       
   304     for index, stackentry in enumerate(stacktb):
       
   305         strings.append(u'<b>File</b> <b class="file">%s</b>, <b>line</b> '
       
   306                        u'<b class="line">%s</b>, <b>function</b> '
       
   307                        u'<b class="function">%s</b>:<br/>'%(
       
   308             xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2])))
       
   309         if stackentry[3]:
       
   310             string = xml_escape(stackentry[3]).decode('utf-8', 'replace')
       
   311             strings.append(u'&#160;&#160;%s<br/>\n' % (string))
       
   312         # add locals info for each entry
       
   313         try:
       
   314             local_context = tcbk.tb_frame.f_locals
       
   315             html_info = []
       
   316             chars = 0
       
   317             for name, value in local_context.iteritems():
       
   318                 value = xml_escape(repr(value))
       
   319                 info = u'<span class="name">%s</span>=%s, ' % (name, value)
       
   320                 line_length = len(name) + len(value)
       
   321                 chars += line_length
       
   322                 # 150 is the result of *years* of research ;-) (CSS might be helpful here)
       
   323                 if chars > 150:
       
   324                     info = u'<br/>' + info
       
   325                     chars = line_length
       
   326                 html_info.append(info)
       
   327             boxid = 'ctxlevel%d' % index
       
   328             strings.append(u'[%s]' % toggle_link(boxid, '+'))
       
   329             strings.append(u'<div id="%s" class="pycontext hidden">%s</div>' %
       
   330                            (boxid, ''.join(html_info)))
       
   331             tcbk = tcbk.tb_next
       
   332         except Exception:
       
   333             pass # doesn't really matter if we have no context info
       
   334     strings.append(u'</div>')
       
   335     return '\n'.join(strings)
       
   336 
       
   337 # csv files / unicode support #################################################
       
   338 
       
   339 class UnicodeCSVWriter:
       
   340     """proxies calls to csv.writer.writerow to be able to deal with unicode"""
       
   341 
       
   342     def __init__(self, wfunc, encoding, **kwargs):
       
   343         self.writer = csv.writer(self, **kwargs)
       
   344         self.wfunc = wfunc
       
   345         self.encoding = encoding
       
   346 
       
   347     def write(self, data):
       
   348         self.wfunc(data)
       
   349 
       
   350     def writerow(self, row):
       
   351         csvrow = []
       
   352         for elt in row:
       
   353             if isinstance(elt, unicode):
       
   354                 csvrow.append(elt.encode(self.encoding))
       
   355             else:
       
   356                 csvrow.append(str(elt))
       
   357         self.writer.writerow(csvrow)
       
   358 
       
   359     def writerows(self, rows):
       
   360         for row in rows:
       
   361             self.writerow(row)
       
   362 
       
   363 
       
   364 # some decorators #############################################################
       
   365 
       
   366 class limitsize(object):
       
   367     def __init__(self, maxsize):
       
   368         self.maxsize = maxsize
       
   369 
       
   370     def __call__(self, function):
       
   371         def newfunc(*args, **kwargs):
       
   372             ret = function(*args, **kwargs)
       
   373             if isinstance(ret, basestring):
       
   374                 return ret[:self.maxsize]
       
   375             return ret
       
   376         return newfunc
       
   377 
       
   378 
       
   379 def htmlescape(function):
       
   380     def newfunc(*args, **kwargs):
       
   381         ret = function(*args, **kwargs)
       
   382         assert isinstance(ret, basestring)
       
   383         return xml_escape(ret)
       
   384     return newfunc