|
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-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'  %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 |