common/uilib.py
changeset 0 b97547f5f1fa
child 159 ff7b0f8dcb3c
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 # -*- coding: utf-8 -*-
       
     2 """user interface libraries
       
     3 
       
     4 contains some functions designed to help implementation of cubicweb user interface
       
     5 
       
     6 :organization: Logilab
       
     7 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     8 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     9 """
       
    10 __docformat__ = "restructuredtext en"
       
    11 
       
    12 import csv
       
    13 import decimal
       
    14 import locale
       
    15 import re
       
    16 from urllib import quote as urlquote
       
    17 from cStringIO import StringIO
       
    18 from xml.parsers.expat import ExpatError
       
    19 
       
    20 import simplejson
       
    21 
       
    22 from mx.DateTime import DateTimeType, DateTimeDeltaType
       
    23 
       
    24 from logilab.common.textutils import unormalize
       
    25 
       
    26 def ustrftime(date, fmt='%Y-%m-%d'):
       
    27     """like strftime, but returns a unicode string instead of an encoded
       
    28     string which may be problematic with localized date.
       
    29     
       
    30     encoding is guessed by locale.getpreferredencoding()
       
    31     """
       
    32     # date format may depend on the locale
       
    33     encoding = locale.getpreferredencoding(do_setlocale=False) or 'UTF-8'
       
    34     return unicode(date.strftime(fmt), encoding)
       
    35 
       
    36 
       
    37 def rql_for_eid(eid):
       
    38     """return the rql query necessary to fetch entity with the given eid.  This
       
    39     function should only be used to generate link with rql inside, not to give
       
    40     to cursor.execute (in which case you won't benefit from rql cache).
       
    41 
       
    42     :Parameters:
       
    43       - `eid`: the eid of the entity we should search
       
    44     :rtype: str
       
    45     :return: the rql query
       
    46     """
       
    47     return 'Any X WHERE X eid %s' % eid
       
    48 
       
    49 
       
    50 def printable_value(req, attrtype, value, props=None, displaytime=True):
       
    51     """return a displayable value (i.e. unicode string)"""
       
    52     if value is None or attrtype == 'Bytes':
       
    53         return u''
       
    54     if attrtype == 'String':
       
    55         # don't translate empty value if you don't want strange results
       
    56         if props is not None and value and props.get('internationalizable'):
       
    57             return req._(value)
       
    58         
       
    59         return value
       
    60     if attrtype == 'Date':
       
    61         return ustrftime(value, req.property_value('ui.date-format'))
       
    62     if attrtype == 'Time':
       
    63         return ustrftime(value, req.property_value('ui.time-format'))
       
    64     if attrtype == 'Datetime':
       
    65         if not displaytime:
       
    66             return ustrftime(value, req.property_value('ui.date-format'))
       
    67         return ustrftime(value, req.property_value('ui.datetime-format'))
       
    68     if attrtype == 'Boolean':
       
    69         if value:
       
    70             return req._('yes')
       
    71         return req._('no')
       
    72     if attrtype == 'Float':
       
    73         value = req.property_value('ui.float-format') % value
       
    74     return unicode(value)
       
    75 
       
    76 
       
    77 # text publishing #############################################################
       
    78 
       
    79 try:
       
    80     from cubicweb.common.rest import rest_publish # pylint: disable-msg=W0611
       
    81 except ImportError:
       
    82     def rest_publish(entity, data):
       
    83         """default behaviour if docutils was not found"""
       
    84         return data
       
    85     
       
    86 TAG_PROG = re.compile(r'</?.*?>', re.U)
       
    87 def remove_html_tags(text):
       
    88     """Removes HTML tags from text
       
    89 
       
    90     >>> remove_html_tags('<td>hi <a href="http://www.google.fr">world</a></td>')
       
    91     'hi world'
       
    92     >>>
       
    93     """
       
    94     return TAG_PROG.sub('', text)
       
    95 
       
    96 
       
    97 REF_PROG = re.compile(r"<ref\s+rql=([\'\"])([^\1]*?)\1\s*>([^<]*)</ref>", re.U)
       
    98 def _subst_rql(view, obj):
       
    99     delim, rql, descr = obj.groups()
       
   100     return u'<a href="%s">%s</a>' % (view.build_url(rql=rql), descr)
       
   101 
       
   102 def html_publish(view, text):
       
   103     """replace <ref rql=''> links by <a href="...">"""
       
   104     if not text:
       
   105         return u''
       
   106     return REF_PROG.sub(lambda obj, view=view:_subst_rql(view, obj), text)
       
   107 
       
   108 try:
       
   109     from lxml import etree
       
   110 except ImportError:
       
   111     # gae environment: lxml not availabel
       
   112     
       
   113     def soup2xhtml(data, encoding):
       
   114         return data
       
   115     
       
   116 else:
       
   117 
       
   118     def soup2xhtml(data, encoding):
       
   119         """tidy (at least try) html soup and return the result
       
   120         Note: the function considers a string with no surrounding tag as valid
       
   121               if <div>`data`</div> can be parsed by an XML parser
       
   122         """
       
   123         xmltree = etree.HTML('<div>%s</div>' % data)
       
   124         # NOTE: lxml 1.1 (etch platforms) doesn't recognize
       
   125         #       the encoding=unicode parameter (lxml 2.0 does), this is
       
   126         #       why we specify an encoding and re-decode to unicode later
       
   127         body = etree.tostring(xmltree[0], encoding=encoding)
       
   128         # remove <body> and </body> and decode to unicode
       
   129         return body[11:-13].decode(encoding)
       
   130 
       
   131     
       
   132 # HTML generation helper functions ############################################
       
   133 
       
   134 from logilab.mtconverter import html_escape
       
   135 
       
   136 def tooltipize(text, tooltip, url=None):
       
   137     """make an HTML tooltip"""
       
   138     url = url or '#'
       
   139     return u'<a href="%s" title="%s">%s</a>' % (url, tooltip, text)
       
   140 
       
   141 def toggle_action(nodeid):
       
   142     """builds a HTML link that uses the js toggleVisibility function"""
       
   143     return u"javascript: toggleVisibility('%s')" % nodeid
       
   144 
       
   145 def toggle_link(nodeid, label):
       
   146     """builds a HTML link that uses the js toggleVisibility function"""
       
   147     return u'<a href="%s">%s</a>' % (toggle_action(nodeid), label)
       
   148 
       
   149 def ajax_replace_url(nodeid, rql, vid=None, swap=False, **extraparams):
       
   150     """builds a replacePageChunk-like url
       
   151     >>> ajax_replace_url('foo', 'Person P')
       
   152     "javascript: replacePageChunk('foo', 'Person%20P');"
       
   153     >>> ajax_replace_url('foo', 'Person P', 'oneline')
       
   154     "javascript: replacePageChunk('foo', 'Person%20P', 'oneline');"
       
   155     >>> ajax_replace_url('foo', 'Person P', 'oneline', name='bar', age=12)
       
   156     "javascript: replacePageChunk('foo', 'Person%20P', 'oneline', {'age':12, 'name':'bar'});"
       
   157     >>> ajax_replace_url('foo', 'Person P', name='bar', age=12)
       
   158     "javascript: replacePageChunk('foo', 'Person%20P', 'null', {'age':12, 'name':'bar'});"    
       
   159     """
       
   160     params = [repr(nodeid), repr(urlquote(rql))]
       
   161     if extraparams and not vid:
       
   162         params.append("'null'")
       
   163     elif vid:
       
   164         params.append(repr(vid))
       
   165     if extraparams:
       
   166         params.append(simplejson.dumps(extraparams))
       
   167     if swap:
       
   168         params.append('true')
       
   169     return "javascript: replacePageChunk(%s);" % ', '.join(params)
       
   170 
       
   171 def safe_cut(text, length):
       
   172     """returns a string of length <length> based on <text>, removing any html
       
   173     tags from given text if cut is necessary.
       
   174     """
       
   175     if text is None:
       
   176         return u''
       
   177     text_nohtml = remove_html_tags(text)
       
   178     # try to keep html tags if text is short enough
       
   179     if len(text_nohtml) <= length:
       
   180         return text
       
   181     # else if un-tagged text is too long, cut it
       
   182     return text_nohtml[:length-3] + u'...'
       
   183 
       
   184 def text_cut(text, nbwords=30):
       
   185     if text is None:
       
   186         return u''
       
   187     minlength = len(' '.join(text.split()[:nbwords]))
       
   188     textlength = text.find('.', minlength) + 1
       
   189     if textlength == 0: # no point found
       
   190         textlength = minlength 
       
   191     return text[:textlength]
       
   192 
       
   193 
       
   194 def cut(text, length):
       
   195     """returns a string of length <length> based on <text>
       
   196     post:
       
   197       len(__return__) <= length
       
   198     """
       
   199     if text is None:
       
   200         return u''
       
   201     if len(text) <= length:
       
   202         return text
       
   203     # else if un-tagged text is too long, cut it
       
   204     return text[:length-3] + u'...'
       
   205 
       
   206 
       
   207 from StringIO import StringIO
       
   208 
       
   209 def ureport_as_html(layout):
       
   210     from logilab.common.ureports import HTMLWriter
       
   211     formater = HTMLWriter(True)
       
   212     stream = StringIO() #UStringIO() don't want unicode assertion
       
   213     formater.format(layout, stream)
       
   214     res = stream.getvalue()
       
   215     if isinstance(res, str):
       
   216         res = unicode(res, 'UTF8')
       
   217     return res
       
   218 
       
   219 def render_HTML_tree(tree, selected_node=None, render_node=None, caption=None):
       
   220     """
       
   221     Generate a pure HTML representation of a tree given as an instance
       
   222     of a logilab.common.tree.Node
       
   223 
       
   224     selected_node is the currently selected node (if any) which will
       
   225     have its surrounding <div> have id="selected" (which default
       
   226     to a bold border libe with the default CSS).
       
   227 
       
   228     render_node is a function that should take a Node content (Node.id)
       
   229     as parameter and should return a string (what will be displayed
       
   230     in the cell).
       
   231 
       
   232     Warning: proper rendering of the generated html code depends on html_tree.css
       
   233     """
       
   234     tree_depth = tree.depth_down()
       
   235     if render_node is None:
       
   236         render_node = str
       
   237 
       
   238     # helper function that build a matrix from the tree, like:
       
   239     # +------+-----------+-----------+
       
   240     # | root | child_1_1 | child_2_1 |
       
   241     # | root | child_1_1 | child_2_2 |
       
   242     # | root | child_1_2 |           |
       
   243     # | root | child_1_3 | child_2_3 |
       
   244     # | root | child_1_3 | child_2_4 |
       
   245     # +------+-----------+-----------+
       
   246     # from:
       
   247     # root -+- child_1_1 -+- child_2_1
       
   248     #       |             |
       
   249     #       |             +- child_2_2
       
   250     #       +- child_1_2
       
   251     #       |
       
   252     #       +- child1_3 -+- child_2_3
       
   253     #                    |
       
   254     #                    +- child_2_2
       
   255     def build_matrix(path, matrix):
       
   256         if path[-1].is_leaf():
       
   257             matrix.append(path[:])
       
   258         else:
       
   259             for child in path[-1].children:
       
   260                 build_matrix(path[:] + [child], matrix)
       
   261         
       
   262     matrix = []
       
   263     build_matrix([tree], matrix)
       
   264 
       
   265     # make all lines in the matrix have the same number of columns
       
   266     for line in matrix:
       
   267         line.extend([None]*(tree_depth-len(line)))
       
   268     for i in range(len(matrix)-1, 0, -1):
       
   269         prev_line, line = matrix[i-1:i+1]
       
   270         for j in range(len(line)):
       
   271             if line[j] == prev_line[j]:
       
   272                 line[j] = None
       
   273 
       
   274     # We build the matrix of link types (between 2 cells on a line of the matrix)
       
   275     # link types are :
       
   276     link_types = {(True,  True,  True ): 1, # T
       
   277                   (False, False, True ): 2, # |
       
   278                   (False, True,  True ): 3, # + (actually, vert. bar with horiz. bar on the right)
       
   279                   (False, True,  False): 4, # L
       
   280                   (True,  True,  False): 5, # -
       
   281                   }
       
   282     links = []
       
   283     for i, line in enumerate(matrix):
       
   284         links.append([])
       
   285         for j in range(tree_depth-1):
       
   286             cell_11 = line[j] is not None
       
   287             cell_12 = line[j+1] is not None
       
   288             cell_21 = line[j+1] is not None and line[j+1].next_sibling() is not None
       
   289             link_type = link_types.get((cell_11, cell_12, cell_21), 0)
       
   290             if link_type == 0 and i > 0 and links[i-1][j] in (1,2,3):
       
   291                 link_type = 2
       
   292             links[-1].append(link_type)
       
   293     
       
   294 
       
   295     # We can now generate the HTML code for the <table> 
       
   296     s = u'<table class="tree">\n'
       
   297     if caption:
       
   298         s += '<caption>%s</caption>\n' % caption
       
   299 
       
   300     for i, link_line in enumerate(links):
       
   301         line = matrix[i]
       
   302 
       
   303         s += '<tr>'
       
   304         for j, link_cell in enumerate(link_line):
       
   305             cell = line[j]
       
   306             if cell:
       
   307                 if cell.id == selected_node:
       
   308                     s += '<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>' % (render_node(cell.id))
       
   309                 else:
       
   310                     s += '<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>' % (render_node(cell.id))
       
   311             else:
       
   312                 s += '<td rowspan="2">&nbsp;</td>'
       
   313             s += '<td class="tree_cell_%d_1">&nbsp;</td>' % link_cell
       
   314             s += '<td class="tree_cell_%d_2">&nbsp;</td>' % link_cell
       
   315                 
       
   316         cell = line[-1]
       
   317         if cell:
       
   318             if cell.id == selected_node:
       
   319                 s += '<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>' % (render_node(cell.id))
       
   320             else:
       
   321                 s += '<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>' % (render_node(cell.id))
       
   322         else:
       
   323             s += '<td rowspan="2">&nbsp;</td>'
       
   324 
       
   325         s += '</tr>\n'
       
   326         if link_line:
       
   327             s += '<tr>'
       
   328             for j, link_cell in enumerate(link_line):
       
   329                 s += '<td class="tree_cell_%d_3">&nbsp;</td>' % link_cell
       
   330                 s += '<td class="tree_cell_%d_4">&nbsp;</td>' % link_cell
       
   331             s += '</tr>\n'
       
   332 
       
   333     s += '</table>'
       
   334     return s
       
   335 
       
   336 
       
   337 
       
   338 # traceback formatting ########################################################
       
   339 
       
   340 import traceback
       
   341 
       
   342 def rest_traceback(info, exception):
       
   343     """return a ReST formated traceback"""
       
   344     res = [u'Traceback\n---------\n::\n']
       
   345     for stackentry in traceback.extract_tb(info[2]):
       
   346         res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3]))
       
   347         if stackentry[3]:
       
   348             res.append(u'\t  %s' % stackentry[3].decode('utf-8', 'replace'))
       
   349     res.append(u'\n')
       
   350     try:
       
   351         res.append(u'\t Error: %s\n' % exception)
       
   352     except:
       
   353         pass
       
   354     return u'\n'.join(res)
       
   355 
       
   356 
       
   357 def html_traceback(info, exception, title='',
       
   358                    encoding='ISO-8859-1', body=''):
       
   359     """ return an html formatted traceback from python exception infos.
       
   360     """
       
   361     tcbk = info[2]
       
   362     stacktb = traceback.extract_tb(tcbk)
       
   363     strings = []
       
   364     if body:
       
   365         strings.append(u'<div class="error_body">')
       
   366         # FIXME
       
   367         strings.append(body)
       
   368         strings.append(u'</div>')
       
   369     if title:
       
   370         strings.append(u'<h1 class="error">%s</h1>'% html_escape(title))
       
   371     try:
       
   372         strings.append(u'<p class="error">%s</p>' % html_escape(str(exception)).replace("\n","<br />"))
       
   373     except UnicodeError:
       
   374         pass
       
   375     strings.append(u'<div class="error_traceback">')
       
   376     for index, stackentry in enumerate(stacktb):
       
   377         strings.append(u'<b>File</b> <b class="file">%s</b>, <b>line</b> '
       
   378                        u'<b class="line">%s</b>, <b>function</b> '
       
   379                        u'<b class="function">%s</b>:<br/>'%(
       
   380             html_escape(stackentry[0]), stackentry[1], html_escape(stackentry[2])))
       
   381         if stackentry[3]:
       
   382             string = html_escape(stackentry[3]).decode('utf-8', 'replace')
       
   383             strings.append(u'&nbsp;&nbsp;%s<br/>\n' % (string))
       
   384         # add locals info for each entry
       
   385         try:
       
   386             local_context = tcbk.tb_frame.f_locals
       
   387             html_info = []
       
   388             chars = 0
       
   389             for name, value in local_context.iteritems():
       
   390                 value = html_escape(repr(value))
       
   391                 info = u'<span class="name">%s</span>=%s, ' % (name, value)
       
   392                 line_length = len(name) + len(value)
       
   393                 chars += line_length
       
   394                 # 150 is the result of *years* of research ;-) (CSS might be helpful here)
       
   395                 if chars > 150:
       
   396                     info = u'<br/>' + info
       
   397                     chars = line_length
       
   398                 html_info.append(info)
       
   399             boxid = 'ctxlevel%d' % index
       
   400             strings.append(u'[%s]' % toggle_link(boxid, '+'))
       
   401             strings.append(u'<div id="%s" class="pycontext hidden">%s</div>' %
       
   402                            (boxid, ''.join(html_info)))
       
   403             tcbk = tcbk.tb_next
       
   404         except Exception:
       
   405             pass # doesn't really matter if we have no context info    
       
   406     strings.append(u'</div>')
       
   407     return '\n'.join(strings)
       
   408 
       
   409 # csv files / unicode support #################################################
       
   410 
       
   411 class UnicodeCSVWriter:
       
   412     """proxies calls to csv.writer.writerow to be able to deal with unicode"""
       
   413     
       
   414     def __init__(self, wfunc, encoding, **kwargs):
       
   415         self.writer = csv.writer(self, **kwargs)
       
   416         self.wfunc = wfunc
       
   417         self.encoding = encoding
       
   418 
       
   419     def write(self, data):
       
   420         self.wfunc(data)
       
   421 
       
   422     def writerow(self, row):
       
   423         csvrow = []
       
   424         for elt in row:
       
   425             if isinstance(elt, unicode):
       
   426                 csvrow.append(elt.encode(self.encoding))
       
   427             else:
       
   428                 csvrow.append(str(elt))
       
   429         self.writer.writerow(csvrow)
       
   430 
       
   431     def writerows(self, rows):
       
   432         for row in rows:
       
   433             self.writerow(row)
       
   434 
       
   435 
       
   436 # some decorators #############################################################
       
   437 
       
   438 class limitsize(object):
       
   439     def __init__(self, maxsize):
       
   440         self.maxsize = maxsize
       
   441 
       
   442     def __call__(self, function):
       
   443         def newfunc(*args, **kwargs):
       
   444             ret = function(*args, **kwargs)
       
   445             if isinstance(ret, basestring):
       
   446                 return ret[:self.maxsize]
       
   447             return ret
       
   448         return newfunc
       
   449 
       
   450 
       
   451 def jsonize(function):
       
   452     import simplejson
       
   453     def newfunc(*args, **kwargs):
       
   454         ret = function(*args, **kwargs)
       
   455         if isinstance(ret, decimal.Decimal):
       
   456             ret = float(ret)
       
   457         elif isinstance(ret, DateTimeType):
       
   458             ret = ret.strftime('%Y-%m-%d %H:%M')
       
   459         elif isinstance(ret, DateTimeDeltaType):
       
   460             ret = ret.seconds
       
   461         try:
       
   462             return simplejson.dumps(ret)
       
   463         except TypeError:
       
   464             return simplejson.dumps(repr(ret))
       
   465     return newfunc
       
   466 
       
   467 
       
   468 def htmlescape(function):
       
   469     def newfunc(*args, **kwargs):
       
   470         ret = function(*args, **kwargs)
       
   471         assert isinstance(ret, basestring)
       
   472         return html_escape(ret)
       
   473     return newfunc