# -*- coding: utf-8 -*-"""user interface librariescontains some functions designed to help implementation of cubicweb user interface:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"importcsvimportrefromurllibimportquoteasurlquotefromStringIOimportStringIOfromlogilab.mtconverterimportxml_escape,html_unescapefromcubicweb.utilsimportustrftimedefrql_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'%eiddefprintable_value(req,attrtype,value,props=None,displaytime=True):"""return a displayable value (i.e. unicode string)"""ifvalueisNoneorattrtype=='Bytes':returnu''ifattrtype=='String':# don't translate empty value if you don't want strange resultsifpropsisnotNoneandvalueandprops.get('internationalizable'):returnreq._(value)returnvalueifattrtype=='Date':returnustrftime(value,req.property_value('ui.date-format'))ifattrtype=='Time':returnustrftime(value,req.property_value('ui.time-format'))ifattrtype=='Datetime':ifnotdisplaytime:returnustrftime(value,req.property_value('ui.date-format'))returnustrftime(value,req.property_value('ui.datetime-format'))ifattrtype=='Boolean':ifvalue:returnreq._('yes')returnreq._('no')ifattrtype=='Float':value=req.property_value('ui.float-format')%valuereturnunicode(value)# text publishing #############################################################try:fromcubicweb.ext.restimportrest_publish# pylint: disable-msg=W0611exceptImportError:defrest_publish(entity,data):"""default behaviour if docutils was not found"""returnxml_escape(data)TAG_PROG=re.compile(r'</?.*?>',re.U)defremove_html_tags(text):"""Removes HTML tags from text >>> remove_html_tags('<td>hi <a href="http://www.google.fr">world</a></td>') 'hi world' >>> """returnTAG_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()returnu'<a href="%s">%s</a>'%(view.build_url(rql=rql),descr)defhtml_publish(view,text):"""replace <ref rql=''> links by <a href="...">"""ifnottext:returnu''returnREF_PROG.sub(lambdaobj,view=view:_subst_rql(view,obj),text)# fallback implementation, nicer one defined below if lxml is availabledefsoup2xhtml(data,encoding):# normalize line break# see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1returnu'\n'.join(data.splitlines())# fallback implementation, nicer one defined below if lxml> 2.0 is availabledefsafe_cut(text,length):"""returns a string of length <length> based on <text>, removing any html tags from given text if cut is necessary."""iftextisNone:returnu''noenttext=html_unescape(text)text_nohtml=remove_html_tags(noenttext)# try to keep html tags if text is short enoughiflen(text_nohtml)<=length:returntext# else if un-tagged text is too long, cut itreturnxml_escape(text_nohtml[:length]+u'...')fallback_safe_cut=safe_cuttry:fromlxmlimportetreeexcept(ImportError,AttributeError):# gae environment: lxml not availabelpasselse:defsoup2xhtml(data,encoding):"""tidy (at least try) html soup and return the result Note: the function considers a string with no surrounding tag as valid if <div>`data`</div> can be parsed by an XML parser """# normalize line break# see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1data=u'\n'.join(data.splitlines())# XXX lxml 1.1 support still needed ?xmltree=etree.HTML('<div>%s</div>'%data)# NOTE: lxml 1.1 (etch platforms) doesn't recognize# the encoding=unicode parameter (lxml 2.0 does), this is# why we specify an encoding and re-decode to unicode laterbody=etree.tostring(xmltree[0],encoding=encoding)# remove <body> and </body> and decode to unicodereturnbody[11:-13].decode(encoding)ifhasattr(etree.HTML('<div>test</div>'),'iter'):defsafe_cut(text,length):"""returns an html document of length <length> based on <text>, and cut is necessary. """iftextisNone:returnu''dom=etree.HTML(text)curlength=0add_ellipsis=Falseforelementindom.iter():ifcurlength>=length:parent=element.getparent()parent.remove(element)ifcurlength==lengthand(element.textorelement.tail):add_ellipsis=Trueelse:ifelement.textisnotNone:element.text=cut(element.text,length-curlength)curlength+=len(element.text)ifelement.tailisnotNone:ifcurlength<length:element.tail=cut(element.tail,length-curlength)curlength+=len(element.tail)elifcurlength==length:element.tail='...'else:element.tail=''text=etree.tounicode(dom[0])[6:-7]# remove wrapping <body></body>ifadd_ellipsis:returntext+u'...'returntextdeftext_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. """iftextisNone:returnu''words=text.split()text=u' '.join(words)# normalize spacestextlength=minlength=len(' '.join(words[:nbwords]))ifgotoperiod:textlength=text.find('.',minlength)+1iftextlength==0:# no period foundtextlength=minlengthreturntext[:textlength]defcut(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) """iftextisNone:returnu''iflen(text)<=length:returntext# else if un-tagged text is too long, cut itreturntext[:length]+u'...'# HTML generation helper functions ############################################HTML4_EMPTY_TAGS=frozenset(('base','meta','link','hr','br','param','img','area','input','col'))defsgml_attributes(attrs):returnu' '.join(u'%s="%s"'%(attr,xml_escape(unicode(value)))forattr,valueinsorted(attrs.items())ifvalueisnotNone)defsimple_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'%tagifattrs:try:attrs['class']=attrs.pop('klass')exceptKeyError:passvalue+=u' '+sgml_attributes(attrs)ifcontent:ifescapecontent:content=xml_escape(unicode(content))value+=u'>%s</%s>'%(content,tag)else:iftaginHTML4_EMPTY_TAGS:value+=u' />'else:value+=u'></%s>'%tagreturnvaluedeftooltipize(text,tooltip,url=None):"""make an HTML tooltip"""url=urlor'#'returnu'<a href="%s" title="%s">%s</a>'%(url,tooltip,text)deftoggle_action(nodeid):"""builds a HTML link that uses the js toggleVisibility function"""returnu"javascript: toggleVisibility('%s')"%nodeiddeftoggle_link(nodeid,label):"""builds a HTML link that uses the js toggleVisibility function"""returnu'<a href="%s">%s</a>'%(toggle_action(nodeid),label)defureport_as_html(layout):fromlogilab.common.ureportsimportHTMLWriterformater=HTMLWriter(True)stream=StringIO()#UStringIO() don't want unicode assertionformater.format(layout,stream)res=stream.getvalue()ifisinstance(res,str):res=unicode(res,'UTF8')returnresdefrender_HTML_tree(tree,selected_node=None,render_node=None,caption=None):""" Generate a pure HTML representation of a tree given as an instance of a logilab.common.tree.Node selected_node is the currently selected node (if any) which will have its surrounding <div> have id="selected" (which default to a bold border libe with the default CSS). render_node is a function that should take a Node content (Node.id) as parameter and should return a string (what will be displayed in the cell). Warning: proper rendering of the generated html code depends on html_tree.css """tree_depth=tree.depth_down()ifrender_nodeisNone:render_node=str# helper function that build a matrix from the tree, like:# +------+-----------+-----------+# | root | child_1_1 | child_2_1 |# | root | child_1_1 | child_2_2 |# | root | child_1_2 | |# | root | child_1_3 | child_2_3 |# | root | child_1_3 | child_2_4 |# +------+-----------+-----------+# from:# root -+- child_1_1 -+- child_2_1# | |# | +- child_2_2# +- child_1_2# |# +- child1_3 -+- child_2_3# |# +- child_2_2defbuild_matrix(path,matrix):ifpath[-1].is_leaf():matrix.append(path[:])else:forchildinpath[-1].children:build_matrix(path[:]+[child],matrix)matrix=[]build_matrix([tree],matrix)# make all lines in the matrix have the same number of columnsforlineinmatrix:line.extend([None]*(tree_depth-len(line)))foriinrange(len(matrix)-1,0,-1):prev_line,line=matrix[i-1:i+1]forjinrange(len(line)):ifline[j]==prev_line[j]:line[j]=None# We build the matrix of link types (between 2 cells on a line of the matrix)# link types are :link_types={(True,True,True):1,# T(False,False,True):2,# |(False,True,True):3,# + (actually, vert. bar with horiz. bar on the right)(False,True,False):4,# L(True,True,False):5,# -}links=[]fori,lineinenumerate(matrix):links.append([])forjinrange(tree_depth-1):cell_11=line[j]isnotNonecell_12=line[j+1]isnotNonecell_21=line[j+1]isnotNoneandline[j+1].next_sibling()isnotNonelink_type=link_types.get((cell_11,cell_12,cell_21),0)iflink_type==0andi>0andlinks[i-1][j]in(1,2,3):link_type=2links[-1].append(link_type)# We can now generate the HTML code for the <table>s=u'<table class="tree">\n'ifcaption:s+='<caption>%s</caption>\n'%captionfori,link_lineinenumerate(links):line=matrix[i]s+='<tr>'forj,link_cellinenumerate(link_line):cell=line[j]ifcell:ifcell.id==selected_node:s+='<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>'%(render_node(cell.id))else:s+='<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>'%(render_node(cell.id))else:s+='<td rowspan="2"> </td>'s+='<td class="tree_cell_%d_1"> </td>'%link_cells+='<td class="tree_cell_%d_2"> </td>'%link_cellcell=line[-1]ifcell:ifcell.id==selected_node:s+='<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>'%(render_node(cell.id))else:s+='<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>'%(render_node(cell.id))else:s+='<td rowspan="2"> </td>'s+='</tr>\n'iflink_line:s+='<tr>'forj,link_cellinenumerate(link_line):s+='<td class="tree_cell_%d_3"> </td>'%link_cells+='<td class="tree_cell_%d_4"> </td>'%link_cells+='</tr>\n's+='</table>'returns# traceback formatting ########################################################importtracebackdefrest_traceback(info,exception):"""return a ReST formated traceback"""res=[u'Traceback\n---------\n::\n']forstackentryintraceback.extract_tb(info[2]):res.append(u'\tFile %s, line %s, function %s'%tuple(stackentry[:3]))ifstackentry[3]:res.append(u'\t%s'%stackentry[3].decode('utf-8','replace'))res.append(u'\n')try:res.append(u'\t Error: %s\n'%exception)except:passreturnu'\n'.join(res)defhtml_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=[]ifbody:strings.append(u'<div class="error_body">')# FIXMEstrings.append(body)strings.append(u'</div>')iftitle: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 />"))exceptUnicodeError:passstrings.append(u'<div class="error_traceback">')forindex,stackentryinenumerate(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])))ifstackentry[3]:string=xml_escape(stackentry[3]).decode('utf-8','replace')strings.append(u' %s<br/>\n'%(string))# add locals info for each entrytry:local_context=tcbk.tb_frame.f_localshtml_info=[]chars=0forname,valueinlocal_context.iteritems():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)ifchars>150:info=u'<br/>'+infochars=line_lengthhtml_info.append(info)boxid='ctxlevel%d'%indexstrings.append(u'[%s]'%toggle_link(boxid,'+'))strings.append(u'<div id="%s" class="pycontext hidden">%s</div>'%(boxid,''.join(html_info)))tcbk=tcbk.tb_nextexceptException:pass# doesn't really matter if we have no context infostrings.append(u'</div>')return'\n'.join(strings)# csv files / unicode support #################################################classUnicodeCSVWriter:"""proxies calls to csv.writer.writerow to be able to deal with unicode"""def__init__(self,wfunc,encoding,**kwargs):self.writer=csv.writer(self,**kwargs)self.wfunc=wfuncself.encoding=encodingdefwrite(self,data):self.wfunc(data)defwriterow(self,row):csvrow=[]foreltinrow:ifisinstance(elt,unicode):csvrow.append(elt.encode(self.encoding))else:csvrow.append(str(elt))self.writer.writerow(csvrow)defwriterows(self,rows):forrowinrows:self.writerow(row)# some decorators #############################################################classlimitsize(object):def__init__(self,maxsize):self.maxsize=maxsizedef__call__(self,function):defnewfunc(*args,**kwargs):ret=function(*args,**kwargs)ifisinstance(ret,basestring):returnret[:self.maxsize]returnretreturnnewfuncdefhtmlescape(function):defnewfunc(*args,**kwargs):ret=function(*args,**kwargs)assertisinstance(ret,basestring)returnxml_escape(ret)returnnewfunc