|
1 # -*- coding: utf-8 -*- |
|
2 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
3 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
4 # |
|
5 # This file is part of CubicWeb. |
|
6 # |
|
7 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
8 # terms of the GNU Lesser General Public License as published by the Free |
|
9 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
10 # any later version. |
|
11 # |
|
12 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
14 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
15 # details. |
|
16 # |
|
17 # You should have received a copy of the GNU Lesser General Public License along |
|
18 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
19 """user interface libraries |
|
20 |
|
21 contains some functions designed to help implementation of cubicweb user |
|
22 interface. |
|
23 """ |
|
24 |
|
25 __docformat__ = "restructuredtext en" |
|
26 |
|
27 import csv |
|
28 import re |
|
29 from io import StringIO |
|
30 |
|
31 from six import PY2, PY3, text_type, binary_type, string_types, integer_types |
|
32 |
|
33 from logilab.mtconverter import xml_escape, html_unescape |
|
34 from logilab.common.date import ustrftime |
|
35 from logilab.common.deprecation import deprecated |
|
36 |
|
37 from cubicweb import _ |
|
38 from cubicweb.utils import js_dumps |
|
39 |
|
40 |
|
41 def rql_for_eid(eid): |
|
42 """return the rql query necessary to fetch entity with the given eid. This |
|
43 function should only be used to generate link with rql inside, not to give |
|
44 to cursor.execute (in which case you won't benefit from rql cache). |
|
45 |
|
46 :Parameters: |
|
47 - `eid`: the eid of the entity we should search |
|
48 :rtype: str |
|
49 :return: the rql query |
|
50 """ |
|
51 return 'Any X WHERE X eid %s' % eid |
|
52 |
|
53 def eid_param(name, eid): |
|
54 assert name is not None |
|
55 assert eid is not None |
|
56 return '%s:%s' % (name, eid) |
|
57 |
|
58 def print_bytes(value, req, props, displaytime=True): |
|
59 return u'' |
|
60 |
|
61 def print_string(value, req, props, displaytime=True): |
|
62 # don't translate empty value if you don't want strange results |
|
63 if props is not None and value and props.get('internationalizable'): |
|
64 return req._(value) |
|
65 return value |
|
66 |
|
67 def print_int(value, req, props, displaytime=True): |
|
68 return text_type(value) |
|
69 |
|
70 def print_date(value, req, props, displaytime=True): |
|
71 return ustrftime(value, req.property_value('ui.date-format')) |
|
72 |
|
73 def print_time(value, req, props, displaytime=True): |
|
74 return ustrftime(value, req.property_value('ui.time-format')) |
|
75 |
|
76 def print_tztime(value, req, props, displaytime=True): |
|
77 return ustrftime(value, req.property_value('ui.time-format')) + u' UTC' |
|
78 |
|
79 def print_datetime(value, req, props, displaytime=True): |
|
80 if displaytime: |
|
81 return ustrftime(value, req.property_value('ui.datetime-format')) |
|
82 return ustrftime(value, req.property_value('ui.date-format')) |
|
83 |
|
84 def print_tzdatetime(value, req, props, displaytime=True): |
|
85 if displaytime: |
|
86 return ustrftime(value, req.property_value('ui.datetime-format')) + u' UTC' |
|
87 return ustrftime(value, req.property_value('ui.date-format')) |
|
88 |
|
89 _('%d years') |
|
90 _('%d months') |
|
91 _('%d weeks') |
|
92 _('%d days') |
|
93 _('%d hours') |
|
94 _('%d minutes') |
|
95 _('%d seconds') |
|
96 |
|
97 def print_timedelta(value, req, props, displaytime=True): |
|
98 if isinstance(value, integer_types): |
|
99 # `date - date`, unlike `datetime - datetime` gives an int |
|
100 # (number of days), not a timedelta |
|
101 # XXX should rql be fixed to return Int instead of Interval in |
|
102 # that case? that would be probably the proper fix but we |
|
103 # loose information on the way... |
|
104 value = timedelta(days=value) |
|
105 if value.days > 730 or value.days < -730: # 2 years |
|
106 return req._('%d years') % (value.days // 365) |
|
107 elif value.days > 60 or value.days < -60: # 2 months |
|
108 return req._('%d months') % (value.days // 30) |
|
109 elif value.days > 14 or value.days < -14: # 2 weeks |
|
110 return req._('%d weeks') % (value.days // 7) |
|
111 elif value.days > 2 or value.days < -2: |
|
112 return req._('%d days') % int(value.days) |
|
113 else: |
|
114 minus = 1 if value.days >= 0 else -1 |
|
115 if value.seconds > 3600: |
|
116 return req._('%d hours') % (int(value.seconds // 3600) * minus) |
|
117 elif value.seconds >= 120: |
|
118 return req._('%d minutes') % (int(value.seconds // 60) * minus) |
|
119 else: |
|
120 return req._('%d seconds') % (int(value.seconds) * minus) |
|
121 |
|
122 def print_boolean(value, req, props, displaytime=True): |
|
123 if value: |
|
124 return req._('yes') |
|
125 return req._('no') |
|
126 |
|
127 def print_float(value, req, props, displaytime=True): |
|
128 return text_type(req.property_value('ui.float-format') % value) # XXX cast needed ? |
|
129 |
|
130 PRINTERS = { |
|
131 'Bytes': print_bytes, |
|
132 'String': print_string, |
|
133 'Int': print_int, |
|
134 'BigInt': print_int, |
|
135 'Date': print_date, |
|
136 'Time': print_time, |
|
137 'TZTime': print_tztime, |
|
138 'Datetime': print_datetime, |
|
139 'TZDatetime': print_tzdatetime, |
|
140 'Boolean': print_boolean, |
|
141 'Float': print_float, |
|
142 'Decimal': print_float, |
|
143 'Interval': print_timedelta, |
|
144 } |
|
145 |
|
146 @deprecated('[3.14] use req.printable_value(attrtype, value, ...)') |
|
147 def printable_value(req, attrtype, value, props=None, displaytime=True): |
|
148 return req.printable_value(attrtype, value, props, displaytime) |
|
149 |
|
150 def css_em_num_value(vreg, propname, default): |
|
151 """ we try to read an 'em' css property |
|
152 if we get another unit we're out of luck and resort to the given default |
|
153 (hence, it is strongly advised not to specify but ems for this css prop) |
|
154 """ |
|
155 propvalue = vreg.config.uiprops[propname].lower().strip() |
|
156 if propvalue.endswith('em'): |
|
157 try: |
|
158 return float(propvalue[:-2]) |
|
159 except Exception: |
|
160 vreg.warning('css property %s looks malformed (%r)', |
|
161 propname, propvalue) |
|
162 else: |
|
163 vreg.warning('css property %s should use em (currently is %r)', |
|
164 propname, propvalue) |
|
165 return default |
|
166 |
|
167 # text publishing ############################################################# |
|
168 |
|
169 from cubicweb.ext.markdown import markdown_publish # pylint: disable=W0611 |
|
170 |
|
171 try: |
|
172 from cubicweb.ext.rest import rest_publish # pylint: disable=W0611 |
|
173 except ImportError: |
|
174 def rest_publish(entity, data): |
|
175 """default behaviour if docutils was not found""" |
|
176 return xml_escape(data) |
|
177 |
|
178 |
|
179 TAG_PROG = re.compile(r'</?.*?>', re.U) |
|
180 def remove_html_tags(text): |
|
181 """Removes HTML tags from text |
|
182 |
|
183 >>> remove_html_tags('<td>hi <a href="http://www.google.fr">world</a></td>') |
|
184 'hi world' |
|
185 >>> |
|
186 """ |
|
187 return TAG_PROG.sub('', text) |
|
188 |
|
189 |
|
190 REF_PROG = re.compile(r"<ref\s+rql=([\'\"])([^\1]*?)\1\s*>([^<]*)</ref>", re.U) |
|
191 def _subst_rql(view, obj): |
|
192 delim, rql, descr = obj.groups() |
|
193 return u'<a href="%s">%s</a>' % (view._cw.build_url(rql=rql), descr) |
|
194 |
|
195 def html_publish(view, text): |
|
196 """replace <ref rql=''> links by <a href="...">""" |
|
197 if not text: |
|
198 return u'' |
|
199 return REF_PROG.sub(lambda obj, view=view:_subst_rql(view, obj), text) |
|
200 |
|
201 # fallback implementation, nicer one defined below if lxml> 2.0 is available |
|
202 def safe_cut(text, length): |
|
203 """returns a string of length <length> based on <text>, removing any html |
|
204 tags from given text if cut is necessary.""" |
|
205 if text is None: |
|
206 return u'' |
|
207 noenttext = html_unescape(text) |
|
208 text_nohtml = remove_html_tags(noenttext) |
|
209 # try to keep html tags if text is short enough |
|
210 if len(text_nohtml) <= length: |
|
211 return text |
|
212 # else if un-tagged text is too long, cut it |
|
213 return xml_escape(text_nohtml[:length] + u'...') |
|
214 |
|
215 fallback_safe_cut = safe_cut |
|
216 |
|
217 REM_ROOT_HTML_TAGS = re.compile('</(body|html)>', re.U) |
|
218 |
|
219 from lxml import etree, html |
|
220 from lxml.html import clean, defs |
|
221 |
|
222 ALLOWED_TAGS = (defs.general_block_tags | defs.list_tags | defs.table_tags | |
|
223 defs.phrase_tags | defs.font_style_tags | |
|
224 set(('span', 'a', 'br', 'img', 'map', 'area', 'sub', 'sup', 'canvas')) |
|
225 ) |
|
226 |
|
227 CLEANER = clean.Cleaner(allow_tags=ALLOWED_TAGS, remove_unknown_tags=False, |
|
228 style=True, safe_attrs_only=True, |
|
229 add_nofollow=False, |
|
230 ) |
|
231 |
|
232 def soup2xhtml(data, encoding): |
|
233 """tidy html soup by allowing some element tags and return the result |
|
234 """ |
|
235 # remove spurious </body> and </html> tags, then normalize line break |
|
236 # (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1) |
|
237 data = REM_ROOT_HTML_TAGS.sub('', u'\n'.join(data.splitlines())) |
|
238 xmltree = etree.HTML(CLEANER.clean_html('<div>%s</div>' % data)) |
|
239 # NOTE: lxml 2.0 does support encoding='unicode', but last time I (syt) |
|
240 # tried I got weird results (lxml 2.2.8) |
|
241 body = etree.tostring(xmltree[0], encoding=encoding) |
|
242 # remove <body> and </body> and decode to unicode |
|
243 snippet = body[6:-7].decode(encoding) |
|
244 # take care to bad xhtml (for instance starting with </div>) which |
|
245 # may mess with the <div> we added below. Only remove it if it's |
|
246 # still there... |
|
247 if snippet.startswith('<div>') and snippet.endswith('</div>'): |
|
248 snippet = snippet[5:-6] |
|
249 return snippet |
|
250 |
|
251 # lxml.Cleaner envelops text elements by internal logic (not accessible) |
|
252 # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 |
|
253 # TODO drop attributes in elements |
|
254 # TODO add policy configuration (content only, embedded content, ...) |
|
255 # XXX this is buggy for "<p>text1</p><p>text2</p>"... |
|
256 # XXX drop these two snippets action and follow the lxml behaviour |
|
257 # XXX (tests need to be updated) |
|
258 # if snippet.startswith('<div>') and snippet.endswith('</div>'): |
|
259 # snippet = snippet[5:-6] |
|
260 # if snippet.startswith('<p>') and snippet.endswith('</p>'): |
|
261 # snippet = snippet[3:-4] |
|
262 return snippet.decode(encoding) |
|
263 |
|
264 if hasattr(etree.HTML('<div>test</div>'), 'iter'): # XXX still necessary? |
|
265 # pylint: disable=E0102 |
|
266 def safe_cut(text, length): |
|
267 """returns an html document of length <length> based on <text>, |
|
268 and cut is necessary. |
|
269 """ |
|
270 if text is None: |
|
271 return u'' |
|
272 dom = etree.HTML(text) |
|
273 curlength = 0 |
|
274 add_ellipsis = False |
|
275 for element in dom.iter(): |
|
276 if curlength >= length: |
|
277 parent = element.getparent() |
|
278 parent.remove(element) |
|
279 if curlength == length and (element.text or element.tail): |
|
280 add_ellipsis = True |
|
281 else: |
|
282 if element.text is not None: |
|
283 element.text = cut(element.text, length - curlength) |
|
284 curlength += len(element.text) |
|
285 if element.tail is not None: |
|
286 if curlength < length: |
|
287 element.tail = cut(element.tail, length - curlength) |
|
288 curlength += len(element.tail) |
|
289 elif curlength == length: |
|
290 element.tail = '...' |
|
291 else: |
|
292 element.tail = '' |
|
293 text = etree.tounicode(dom[0])[6:-7] # remove wrapping <body></body> |
|
294 if add_ellipsis: |
|
295 return text + u'...' |
|
296 return text |
|
297 |
|
298 def text_cut(text, nbwords=30, gotoperiod=True): |
|
299 """from the given plain text, return a text with at least <nbwords> words, |
|
300 trying to go to the end of the current sentence. |
|
301 |
|
302 :param nbwords: the minimum number of words required |
|
303 :param gotoperiod: specifies if the function should try to go to |
|
304 the first period after the cut (i.e. finish |
|
305 the sentence if possible) |
|
306 |
|
307 Note that spaces are normalized. |
|
308 """ |
|
309 if text is None: |
|
310 return u'' |
|
311 words = text.split() |
|
312 text = u' '.join(words) # normalize spaces |
|
313 textlength = minlength = len(' '.join(words[:nbwords])) |
|
314 if gotoperiod: |
|
315 textlength = text.find('.', minlength) + 1 |
|
316 if textlength == 0: # no period found |
|
317 textlength = minlength |
|
318 return text[:textlength] |
|
319 |
|
320 def cut(text, length): |
|
321 """returns a string of a maximum length <length> based on <text> |
|
322 (approximatively, since if text has been cut, '...' is added to the end of the string, |
|
323 resulting in a string of len <length> + 3) |
|
324 """ |
|
325 if text is None: |
|
326 return u'' |
|
327 if len(text) <= length: |
|
328 return text |
|
329 # else if un-tagged text is too long, cut it |
|
330 return text[:length] + u'...' |
|
331 |
|
332 |
|
333 |
|
334 # HTML generation helper functions ############################################ |
|
335 |
|
336 class _JSId(object): |
|
337 def __init__(self, id, parent=None): |
|
338 self.id = id |
|
339 self.parent = parent |
|
340 def __unicode__(self): |
|
341 if self.parent: |
|
342 return u'%s.%s' % (self.parent, self.id) |
|
343 return text_type(self.id) |
|
344 __str__ = __unicode__ if PY3 else lambda self: self.__unicode__().encode('utf-8') |
|
345 def __getattr__(self, attr): |
|
346 return _JSId(attr, self) |
|
347 def __call__(self, *args): |
|
348 return _JSCallArgs(args, self) |
|
349 |
|
350 class _JSCallArgs(_JSId): |
|
351 def __init__(self, args, parent=None): |
|
352 assert isinstance(args, tuple) |
|
353 self.args = args |
|
354 self.parent = parent |
|
355 def __unicode__(self): |
|
356 args = [] |
|
357 for arg in self.args: |
|
358 args.append(js_dumps(arg)) |
|
359 if self.parent: |
|
360 return u'%s(%s)' % (self.parent, ','.join(args)) |
|
361 return ','.join(args) |
|
362 __str__ = __unicode__ if PY3 else lambda self: self.__unicode__().encode('utf-8') |
|
363 |
|
364 class _JS(object): |
|
365 def __getattr__(self, attr): |
|
366 return _JSId(attr) |
|
367 |
|
368 js = _JS() |
|
369 js.__doc__ = """\ |
|
370 magic object to return strings suitable to call some javascript function with |
|
371 the given arguments (which should be correctly typed). |
|
372 |
|
373 >>> str(js.pouet(1, "2")) |
|
374 'pouet(1,"2")' |
|
375 >>> str(js.cw.pouet(1, "2")) |
|
376 'cw.pouet(1,"2")' |
|
377 >>> str(js.cw.pouet(1, "2").pouet(None)) |
|
378 'cw.pouet(1,"2").pouet(null)' |
|
379 >>> str(js.cw.pouet(1, JSString("$")).pouet(None)) |
|
380 'cw.pouet(1,$).pouet(null)' |
|
381 >>> str(js.cw.pouet(1, {'callback': JSString("cw.cb")}).pouet(None)) |
|
382 'cw.pouet(1,{callback: cw.cb}).pouet(null)' |
|
383 """ |
|
384 |
|
385 def domid(string): |
|
386 """return a valid DOM id from a string (should also be usable in jQuery |
|
387 search expression...) |
|
388 """ |
|
389 return string.replace('.', '_').replace('-', '_') |
|
390 |
|
391 HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param', |
|
392 'img', 'area', 'input', 'col')) |
|
393 |
|
394 def sgml_attributes(attrs): |
|
395 return u' '.join(u'%s="%s"' % (attr, xml_escape(text_type(value))) |
|
396 for attr, value in sorted(attrs.items()) |
|
397 if value is not None) |
|
398 |
|
399 def simple_sgml_tag(tag, content=None, escapecontent=True, **attrs): |
|
400 """generation of a simple sgml tag (eg without children tags) easier |
|
401 |
|
402 content and attri butes will be escaped |
|
403 """ |
|
404 value = u'<%s' % tag |
|
405 if attrs: |
|
406 try: |
|
407 attrs['class'] = attrs.pop('klass') |
|
408 except KeyError: |
|
409 pass |
|
410 value += u' ' + sgml_attributes(attrs) |
|
411 if content: |
|
412 if escapecontent: |
|
413 content = xml_escape(text_type(content)) |
|
414 value += u'>%s</%s>' % (content, tag) |
|
415 else: |
|
416 if tag in HTML4_EMPTY_TAGS: |
|
417 value += u' />' |
|
418 else: |
|
419 value += u'></%s>' % tag |
|
420 return value |
|
421 |
|
422 def tooltipize(text, tooltip, url=None): |
|
423 """make an HTML tooltip""" |
|
424 url = url or '#' |
|
425 return u'<a href="%s" title="%s">%s</a>' % (url, tooltip, text) |
|
426 |
|
427 def toggle_action(nodeid): |
|
428 """builds a HTML link that uses the js toggleVisibility function""" |
|
429 return u"javascript: toggleVisibility('%s')" % nodeid |
|
430 |
|
431 def toggle_link(nodeid, label): |
|
432 """builds a HTML link that uses the js toggleVisibility function""" |
|
433 return u'<a href="%s">%s</a>' % (toggle_action(nodeid), label) |
|
434 |
|
435 |
|
436 def ureport_as_html(layout): |
|
437 from logilab.common.ureports import HTMLWriter |
|
438 formater = HTMLWriter(True) |
|
439 stream = StringIO() #UStringIO() don't want unicode assertion |
|
440 formater.format(layout, stream) |
|
441 res = stream.getvalue() |
|
442 if isinstance(res, binary_type): |
|
443 res = res.decode('UTF8') |
|
444 return res |
|
445 |
|
446 # traceback formatting ######################################################## |
|
447 |
|
448 import traceback |
|
449 |
|
450 def exc_message(ex, encoding): |
|
451 if PY3: |
|
452 excmsg = str(ex) |
|
453 else: |
|
454 try: |
|
455 excmsg = unicode(ex) |
|
456 except Exception: |
|
457 try: |
|
458 excmsg = unicode(str(ex), encoding, 'replace') |
|
459 except Exception: |
|
460 excmsg = unicode(repr(ex), encoding, 'replace') |
|
461 exctype = ex.__class__.__name__ |
|
462 return u'%s: %s' % (exctype, excmsg) |
|
463 |
|
464 |
|
465 def rest_traceback(info, exception): |
|
466 """return a unicode ReST formated traceback""" |
|
467 res = [u'Traceback\n---------\n::\n'] |
|
468 for stackentry in traceback.extract_tb(info[2]): |
|
469 res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3])) |
|
470 if stackentry[3]: |
|
471 data = xml_escape(stackentry[3]) |
|
472 if PY2: |
|
473 data = data.decode('utf-8', 'replace') |
|
474 res.append(u'\t %s' % data) |
|
475 res.append(u'\n') |
|
476 try: |
|
477 res.append(u'\t Error: %s\n' % exception) |
|
478 except Exception: |
|
479 pass |
|
480 return u'\n'.join(res) |
|
481 |
|
482 |
|
483 def html_traceback(info, exception, title='', |
|
484 encoding='ISO-8859-1', body=''): |
|
485 """ return an html formatted traceback from python exception infos. |
|
486 """ |
|
487 tcbk = info[2] |
|
488 stacktb = traceback.extract_tb(tcbk) |
|
489 strings = [] |
|
490 if body: |
|
491 strings.append(u'<div class="error_body">') |
|
492 # FIXME |
|
493 strings.append(body) |
|
494 strings.append(u'</div>') |
|
495 if title: |
|
496 strings.append(u'<h1 class="error">%s</h1>'% xml_escape(title)) |
|
497 try: |
|
498 strings.append(u'<p class="error">%s</p>' % xml_escape(str(exception)).replace("\n","<br />")) |
|
499 except UnicodeError: |
|
500 pass |
|
501 strings.append(u'<div class="error_traceback">') |
|
502 for index, stackentry in enumerate(stacktb): |
|
503 strings.append(u'<b>File</b> <b class="file">%s</b>, <b>line</b> ' |
|
504 u'<b class="line">%s</b>, <b>function</b> ' |
|
505 u'<b class="function">%s</b>:<br/>'%( |
|
506 xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2]))) |
|
507 if stackentry[3]: |
|
508 string = xml_escape(stackentry[3]) |
|
509 if PY2: |
|
510 string = string.decode('utf-8', 'replace') |
|
511 strings.append(u'  %s<br/>\n' % (string)) |
|
512 # add locals info for each entry |
|
513 try: |
|
514 local_context = tcbk.tb_frame.f_locals |
|
515 html_info = [] |
|
516 chars = 0 |
|
517 for name, value in local_context.items(): |
|
518 value = xml_escape(repr(value)) |
|
519 info = u'<span class="name">%s</span>=%s, ' % (name, value) |
|
520 line_length = len(name) + len(value) |
|
521 chars += line_length |
|
522 # 150 is the result of *years* of research ;-) (CSS might be helpful here) |
|
523 if chars > 150: |
|
524 info = u'<br/>' + info |
|
525 chars = line_length |
|
526 html_info.append(info) |
|
527 boxid = 'ctxlevel%d' % index |
|
528 strings.append(u'[%s]' % toggle_link(boxid, '+')) |
|
529 strings.append(u'<div id="%s" class="pycontext hidden">%s</div>' % |
|
530 (boxid, ''.join(html_info))) |
|
531 tcbk = tcbk.tb_next |
|
532 except Exception: |
|
533 pass # doesn't really matter if we have no context info |
|
534 strings.append(u'</div>') |
|
535 return '\n'.join(strings) |
|
536 |
|
537 # csv files / unicode support ################################################# |
|
538 |
|
539 class UnicodeCSVWriter: |
|
540 """proxies calls to csv.writer.writerow to be able to deal with unicode |
|
541 |
|
542 Under Python 3, this code no longer encodes anything.""" |
|
543 |
|
544 def __init__(self, wfunc, encoding, **kwargs): |
|
545 self.writer = csv.writer(self, **kwargs) |
|
546 self.wfunc = wfunc |
|
547 self.encoding = encoding |
|
548 |
|
549 def write(self, data): |
|
550 self.wfunc(data) |
|
551 |
|
552 def writerow(self, row): |
|
553 if PY3: |
|
554 self.writer.writerow(row) |
|
555 return |
|
556 csvrow = [] |
|
557 for elt in row: |
|
558 if isinstance(elt, text_type): |
|
559 csvrow.append(elt.encode(self.encoding)) |
|
560 else: |
|
561 csvrow.append(str(elt)) |
|
562 self.writer.writerow(csvrow) |
|
563 |
|
564 def writerows(self, rows): |
|
565 for row in rows: |
|
566 self.writerow(row) |
|
567 |
|
568 |
|
569 # some decorators ############################################################# |
|
570 |
|
571 class limitsize(object): |
|
572 def __init__(self, maxsize): |
|
573 self.maxsize = maxsize |
|
574 |
|
575 def __call__(self, function): |
|
576 def newfunc(*args, **kwargs): |
|
577 ret = function(*args, **kwargs) |
|
578 if isinstance(ret, string_types): |
|
579 return ret[:self.maxsize] |
|
580 return ret |
|
581 return newfunc |
|
582 |
|
583 |
|
584 def htmlescape(function): |
|
585 def newfunc(*args, **kwargs): |
|
586 ret = function(*args, **kwargs) |
|
587 assert isinstance(ret, string_types) |
|
588 return xml_escape(ret) |
|
589 return newfunc |