1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | """ |
---|
5 | | This file is part of the web2py Web Framework |
---|
6 | | Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> |
---|
7 | | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) |
---|
8 | |
---|
9 | Template helpers |
---|
10 | -------------------------------------------- |
---|
11 | """ |
---|
12 | |
---|
13 | import cgi |
---|
14 | import os |
---|
15 | import re |
---|
16 | import copy |
---|
17 | import types |
---|
18 | import urllib |
---|
19 | import base64 |
---|
20 | import itertools |
---|
21 | from pydal._compat import PY2, reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, \ |
---|
22 | urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type, long |
---|
23 | from yatl import sanitizer |
---|
24 | import marshal |
---|
25 | |
---|
26 | from gluon import decoder |
---|
27 | from gluon.storage import Storage |
---|
28 | from gluon.utils import web2py_uuid, compare |
---|
29 | from gluon.highlight import highlight |
---|
30 | from gluon.validators import simple_hash |
---|
31 | |
---|
32 | |
---|
33 | def local_html_escape(data, quote=False): |
---|
34 | """ |
---|
35 | Works with bytes. |
---|
36 | Replace special characters "&", "<" and ">" to HTML-safe sequences. |
---|
37 | If the optional flag quote is true (the default), the quotation mark |
---|
38 | characters, both double quote (") and single quote (') characters are also |
---|
39 | translated. |
---|
40 | """ |
---|
41 | if PY2: |
---|
42 | import cgi |
---|
43 | data = cgi.escape(data, quote) |
---|
44 | return data.replace("'", "'") if quote else data |
---|
45 | else: |
---|
46 | import html |
---|
47 | if isinstance(data, str): |
---|
48 | return html.escape(data, quote=quote) |
---|
49 | data = data.replace(b"&", b"&") # Must be done first! |
---|
50 | data = data.replace(b"<", b"<") |
---|
51 | data = data.replace(b">", b">") |
---|
52 | if quote: |
---|
53 | data = data.replace(b'"', b""") |
---|
54 | data = data.replace(b'\'', b"'") |
---|
55 | return data |
---|
56 | |
---|
57 | regex_crlf = re.compile('\r|\n') |
---|
58 | |
---|
59 | join = ''.join |
---|
60 | |
---|
61 | # name2codepoint is incomplete respect to xhtml (and xml): 'apos' is missing. |
---|
62 | entitydefs = dict([(k_v[0], to_bytes(unichr(k_v[1]))) for k_v in iteritems(name2codepoint)]) |
---|
63 | entitydefs.setdefault('apos', to_bytes("'")) |
---|
64 | |
---|
65 | |
---|
66 | __all__ = [ |
---|
67 | 'A', |
---|
68 | 'ASSIGNJS', |
---|
69 | 'B', |
---|
70 | 'BEAUTIFY', |
---|
71 | 'BODY', |
---|
72 | 'BR', |
---|
73 | 'BUTTON', |
---|
74 | 'CENTER', |
---|
75 | 'CAT', |
---|
76 | 'CODE', |
---|
77 | 'COL', |
---|
78 | 'COLGROUP', |
---|
79 | 'DIV', |
---|
80 | 'EM', |
---|
81 | 'EMBED', |
---|
82 | 'FIELDSET', |
---|
83 | 'FORM', |
---|
84 | 'H1', |
---|
85 | 'H2', |
---|
86 | 'H3', |
---|
87 | 'H4', |
---|
88 | 'H5', |
---|
89 | 'H6', |
---|
90 | 'HEAD', |
---|
91 | 'HR', |
---|
92 | 'HTML', |
---|
93 | 'I', |
---|
94 | 'IFRAME', |
---|
95 | 'IMG', |
---|
96 | 'INPUT', |
---|
97 | 'LABEL', |
---|
98 | 'LEGEND', |
---|
99 | 'LI', |
---|
100 | 'LINK', |
---|
101 | 'OL', |
---|
102 | 'UL', |
---|
103 | 'MARKMIN', |
---|
104 | 'MENU', |
---|
105 | 'META', |
---|
106 | 'OBJECT', |
---|
107 | 'ON', |
---|
108 | 'OPTION', |
---|
109 | 'P', |
---|
110 | 'PRE', |
---|
111 | 'SCRIPT', |
---|
112 | 'OPTGROUP', |
---|
113 | 'SELECT', |
---|
114 | 'SPAN', |
---|
115 | 'STRONG', |
---|
116 | 'STYLE', |
---|
117 | 'TABLE', |
---|
118 | 'TAG', |
---|
119 | 'TD', |
---|
120 | 'TEXTAREA', |
---|
121 | 'TH', |
---|
122 | 'THEAD', |
---|
123 | 'TBODY', |
---|
124 | 'TFOOT', |
---|
125 | 'TITLE', |
---|
126 | 'TR', |
---|
127 | 'TT', |
---|
128 | 'URL', |
---|
129 | 'XHTML', |
---|
130 | 'XML', |
---|
131 | 'xmlescape', |
---|
132 | 'embed64', |
---|
133 | ] |
---|
134 | |
---|
135 | DEFAULT_PASSWORD_DISPLAY = '*' * 8 |
---|
136 | |
---|
137 | |
---|
138 | def xmlescape(data, quote=True): |
---|
139 | """ |
---|
140 | Returns an escaped string of the provided data |
---|
141 | |
---|
142 | Args: |
---|
143 | data: the data to be escaped |
---|
144 | quote: optional (default False) |
---|
145 | """ |
---|
146 | |
---|
147 | # first try the xml function |
---|
148 | if hasattr(data, 'xml') and callable(data.xml): |
---|
149 | return to_bytes(data.xml()) |
---|
150 | |
---|
151 | if not(isinstance(data, (text_type, bytes))): |
---|
152 | # i.e., integers |
---|
153 | data = str(data) |
---|
154 | data = to_bytes(data, 'utf8', 'xmlcharrefreplace') |
---|
155 | |
---|
156 | # ... and do the escaping |
---|
157 | data = local_html_escape(data, quote) |
---|
158 | return data |
---|
159 | |
---|
160 | |
---|
161 | def call_as_list(f, *a, **b): |
---|
162 | if not isinstance(f, (list, tuple)): |
---|
163 | f = [f] |
---|
164 | for item in f: |
---|
165 | item(*a, **b) |
---|
166 | |
---|
167 | |
---|
168 | def truncate_string(text, length, dots='...'): |
---|
169 | text = to_unicode(text) |
---|
170 | if len(text) > length: |
---|
171 | text = to_native(text[:length - len(dots)]) + dots |
---|
172 | return text |
---|
173 | |
---|
174 | |
---|
175 | def URL(a=None, |
---|
176 | c=None, |
---|
177 | f=None, |
---|
178 | r=None, |
---|
179 | args=None, |
---|
180 | vars=None, |
---|
181 | anchor='', |
---|
182 | extension=None, |
---|
183 | env=None, |
---|
184 | hmac_key=None, |
---|
185 | hash_vars=True, |
---|
186 | salt=None, |
---|
187 | user_signature=None, |
---|
188 | scheme=None, |
---|
189 | host=None, |
---|
190 | port=None, |
---|
191 | encode_embedded_slash=False, |
---|
192 | url_encode=True, |
---|
193 | language=None |
---|
194 | ): |
---|
195 | """ |
---|
196 | generates a url '/a/c/f' corresponding to application a, controller c |
---|
197 | and function f. If r=request is passed, a, c, f are set, respectively, |
---|
198 | to r.application, r.controller, r.function. |
---|
199 | |
---|
200 | The more typical usage is: |
---|
201 | |
---|
202 | URL('index') |
---|
203 | |
---|
204 | that generates a url for the index function |
---|
205 | within the present application and controller. |
---|
206 | |
---|
207 | Args: |
---|
208 | a: application (default to current if r is given) |
---|
209 | c: controller (default to current if r is given) |
---|
210 | f: function (default to current if r is given) |
---|
211 | r: request (optional) |
---|
212 | args: any arguments (optional). Additional "path" elements |
---|
213 | vars: any variables (optional). Querystring elements |
---|
214 | anchor: anchorname, without # (optional) |
---|
215 | extension: force an extension |
---|
216 | hmac_key: key to use when generating hmac signature (optional) |
---|
217 | hash_vars: which of the vars to include in our hmac signature |
---|
218 | True (default) - hash all vars, False - hash none of the vars, |
---|
219 | iterable - hash only the included vars ['key1','key2'] |
---|
220 | salt: salt hashing with this string |
---|
221 | user_signature: signs automatically the URL in such way that only the |
---|
222 | user can access the URL (use with `URL.verify` or |
---|
223 | `auth.requires_signature()`) |
---|
224 | scheme: URI scheme (True, 'http' or 'https', etc); forces absolute URL (optional) |
---|
225 | host: string to force absolute URL with host (True means http_host) |
---|
226 | port: optional port number (forces absolute URL) |
---|
227 | encode_embedded_slash: encode slash characters included in args |
---|
228 | url_encode: encode characters included in vars |
---|
229 | |
---|
230 | Raises: |
---|
231 | SyntaxError: when no application, controller or function is available |
---|
232 | or when a CRLF is found in the generated url |
---|
233 | |
---|
234 | Examples: |
---|
235 | |
---|
236 | >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], |
---|
237 | ... vars={'p':1, 'q':2}, anchor='1')) |
---|
238 | '/a/c/f/x/y/z?p=1&q=2#1' |
---|
239 | |
---|
240 | >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], |
---|
241 | ... vars={'p':(1,3), 'q':2}, anchor='1')) |
---|
242 | '/a/c/f/x/y/z?p=1&p=3&q=2#1' |
---|
243 | |
---|
244 | >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], |
---|
245 | ... vars={'p':(3,1), 'q':2}, anchor='1')) |
---|
246 | '/a/c/f/x/y/z?p=3&p=1&q=2#1' |
---|
247 | |
---|
248 | >>> str(URL(a='a', c='c', f='f', anchor='1+2')) |
---|
249 | '/a/c/f#1%2B2' |
---|
250 | |
---|
251 | >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], |
---|
252 | ... vars={'p':(1,3), 'q':2}, anchor='1', hmac_key='key')) |
---|
253 | '/a/c/f/x/y/z?p=1&p=3&q=2&_signature=a32530f0d0caa80964bb92aad2bedf8a4486a31f#1' |
---|
254 | |
---|
255 | >>> str(URL(a='a', c='c', f='f', args=['w/x', 'y/z'])) |
---|
256 | '/a/c/f/w/x/y/z' |
---|
257 | |
---|
258 | >>> str(URL(a='a', c='c', f='f', args=['w/x', 'y/z'], encode_embedded_slash=True)) |
---|
259 | '/a/c/f/w%2Fx/y%2Fz' |
---|
260 | |
---|
261 | >>> str(URL(a='a', c='c', f='f', args=['%(id)d'], url_encode=False)) |
---|
262 | '/a/c/f/%(id)d' |
---|
263 | |
---|
264 | >>> str(URL(a='a', c='c', f='f', args=['%(id)d'], url_encode=True)) |
---|
265 | '/a/c/f/%25%28id%29d' |
---|
266 | |
---|
267 | >>> str(URL(a='a', c='c', f='f', vars={'id' : '%(id)d' }, url_encode=False)) |
---|
268 | '/a/c/f?id=%(id)d' |
---|
269 | |
---|
270 | >>> str(URL(a='a', c='c', f='f', vars={'id' : '%(id)d' }, url_encode=True)) |
---|
271 | '/a/c/f?id=%25%28id%29d' |
---|
272 | |
---|
273 | >>> str(URL(a='a', c='c', f='f', anchor='%(id)d', url_encode=False)) |
---|
274 | '/a/c/f#%(id)d' |
---|
275 | |
---|
276 | >>> str(URL(a='a', c='c', f='f', anchor='%(id)d', url_encode=True)) |
---|
277 | '/a/c/f#%25%28id%29d' |
---|
278 | """ |
---|
279 | |
---|
280 | from gluon.rewrite import url_out # done here in case used not-in web2py |
---|
281 | |
---|
282 | if args in (None, []): |
---|
283 | args = [] |
---|
284 | vars = vars or {} |
---|
285 | application = None |
---|
286 | controller = None |
---|
287 | function = None |
---|
288 | |
---|
289 | if not isinstance(args, (list, tuple)): |
---|
290 | args = [args] |
---|
291 | |
---|
292 | if not r: |
---|
293 | if a and not c and not f: |
---|
294 | (f, a, c) = (a, c, f) |
---|
295 | elif a and c and not f: |
---|
296 | (c, f, a) = (a, c, f) |
---|
297 | from gluon.globals import current |
---|
298 | if hasattr(current, 'request'): |
---|
299 | r = current.request |
---|
300 | |
---|
301 | if r: |
---|
302 | application = r.application |
---|
303 | controller = r.controller |
---|
304 | function = r.function |
---|
305 | env = r.env |
---|
306 | if extension is None and r.extension != 'html': |
---|
307 | extension = r.extension |
---|
308 | if a: |
---|
309 | application = a |
---|
310 | if c: |
---|
311 | controller = c |
---|
312 | if f: |
---|
313 | if not isinstance(f, str): |
---|
314 | if hasattr(f, '__name__'): |
---|
315 | function = f.__name__ |
---|
316 | else: |
---|
317 | raise SyntaxError( |
---|
318 | 'when calling URL, function or function name required') |
---|
319 | elif '/' in f: |
---|
320 | if f.startswith("/"): |
---|
321 | f = f[1:] |
---|
322 | items = f.split('/') |
---|
323 | function = f = items[0] |
---|
324 | args = items[1:] + args |
---|
325 | else: |
---|
326 | function = f |
---|
327 | |
---|
328 | # if the url gets a static resource, don't force extension |
---|
329 | if controller == 'static': |
---|
330 | extension = None |
---|
331 | # add static version to url |
---|
332 | from gluon.globals import current |
---|
333 | if hasattr(current, 'response'): |
---|
334 | response = current.response |
---|
335 | if response.static_version and response.static_version_urls: |
---|
336 | args = [function] + args |
---|
337 | function = '_' + str(response.static_version) |
---|
338 | |
---|
339 | if '.' in function: |
---|
340 | function, extension = function.rsplit('.', 1) |
---|
341 | |
---|
342 | function2 = '%s.%s' % (function, extension or 'html') |
---|
343 | |
---|
344 | if not (application and controller and function): |
---|
345 | raise SyntaxError('not enough information to build the url (%s %s %s)' % (application, controller, function)) |
---|
346 | |
---|
347 | if args: |
---|
348 | if url_encode: |
---|
349 | if encode_embedded_slash: |
---|
350 | other = '/' + '/'.join([urllib_quote(str(x), '') for x in args]) |
---|
351 | else: |
---|
352 | other = args and urllib_quote('/' + '/'.join([str(x) for x in args])) |
---|
353 | else: |
---|
354 | other = args and ('/' + '/'.join([str(x) for x in args])) |
---|
355 | else: |
---|
356 | other = '' |
---|
357 | |
---|
358 | if other.endswith('/'): |
---|
359 | other += '/' # add trailing slash to make last trailing empty arg explicit |
---|
360 | |
---|
361 | list_vars = [] |
---|
362 | for (key, vals) in sorted(vars.items()): |
---|
363 | if key == '_signature': |
---|
364 | continue |
---|
365 | if not isinstance(vals, (list, tuple)): |
---|
366 | vals = [vals] |
---|
367 | for val in vals: |
---|
368 | list_vars.append((key, val)) |
---|
369 | |
---|
370 | if user_signature: |
---|
371 | from gluon.globals import current |
---|
372 | if current.session.auth: |
---|
373 | hmac_key = current.session.auth.hmac_key |
---|
374 | |
---|
375 | if hmac_key: |
---|
376 | # generate an hmac signature of the vars & args so can later |
---|
377 | # verify the user hasn't messed with anything |
---|
378 | |
---|
379 | h_args = '/%s/%s/%s%s' % (application, controller, function2, other) |
---|
380 | |
---|
381 | # how many of the vars should we include in our hash? |
---|
382 | if hash_vars is True: # include them all |
---|
383 | h_vars = list_vars |
---|
384 | elif hash_vars is False: # include none of them |
---|
385 | h_vars = '' |
---|
386 | else: # include just those specified |
---|
387 | if hash_vars and not isinstance(hash_vars, (list, tuple)): |
---|
388 | hash_vars = [hash_vars] |
---|
389 | h_vars = [(k, v) for (k, v) in list_vars if k in hash_vars] |
---|
390 | |
---|
391 | # re-assembling the same way during hash authentication |
---|
392 | message = h_args + '?' + urlencode(sorted(h_vars)) |
---|
393 | sig = simple_hash( |
---|
394 | message, hmac_key or '', salt or '', digest_alg='sha1') |
---|
395 | # add the signature into vars |
---|
396 | list_vars.append(('_signature', sig)) |
---|
397 | |
---|
398 | if list_vars: |
---|
399 | if url_encode: |
---|
400 | other += '?%s' % urlencode(list_vars) |
---|
401 | else: |
---|
402 | other += '?%s' % '&'.join(['%s=%s' % var[:2] for var in list_vars]) |
---|
403 | if anchor: |
---|
404 | if url_encode: |
---|
405 | other += '#' + urllib_quote(str(anchor)) |
---|
406 | else: |
---|
407 | other += '#' + (str(anchor)) |
---|
408 | if extension: |
---|
409 | function += '.' + extension |
---|
410 | |
---|
411 | if regex_crlf.search(join([application, controller, function, other])): |
---|
412 | raise SyntaxError('CRLF Injection Detected') |
---|
413 | |
---|
414 | url = url_out(r, env, application, controller, function, |
---|
415 | args, other, scheme, host, port, language=language) |
---|
416 | return url |
---|
417 | |
---|
418 | |
---|
419 | def verifyURL(request, hmac_key=None, hash_vars=True, salt=None, user_signature=None): |
---|
420 | """ |
---|
421 | Verifies that a request's args & vars have not been tampered with by the user |
---|
422 | |
---|
423 | :param request: web2py's request object |
---|
424 | :param hmac_key: the key to authenticate with, must be the same one previously |
---|
425 | used when calling URL() |
---|
426 | :param hash_vars: which vars to include in our hashing. (Optional) |
---|
427 | Only uses the 1st value currently |
---|
428 | True (or undefined) means all, False none, |
---|
429 | an iterable just the specified keys |
---|
430 | |
---|
431 | do not call directly. Use instead: |
---|
432 | |
---|
433 | URL.verify(hmac_key='...') |
---|
434 | |
---|
435 | the key has to match the one used to generate the URL. |
---|
436 | |
---|
437 | >>> r = Storage() |
---|
438 | >>> gv = Storage(p=(1,3),q=2,_signature='a32530f0d0caa80964bb92aad2bedf8a4486a31f') |
---|
439 | >>> r.update(dict(application='a', controller='c', function='f', extension='html')) |
---|
440 | >>> r['args'] = ['x', 'y', 'z'] |
---|
441 | >>> r['get_vars'] = gv |
---|
442 | >>> verifyURL(r, 'key') |
---|
443 | True |
---|
444 | >>> verifyURL(r, 'kay') |
---|
445 | False |
---|
446 | >>> r.get_vars.p = (3, 1) |
---|
447 | >>> verifyURL(r, 'key') |
---|
448 | True |
---|
449 | >>> r.get_vars.p = (3, 2) |
---|
450 | >>> verifyURL(r, 'key') |
---|
451 | False |
---|
452 | |
---|
453 | """ |
---|
454 | |
---|
455 | if '_signature' not in request.get_vars: |
---|
456 | return False # no signature in the request URL |
---|
457 | |
---|
458 | # check if user_signature requires |
---|
459 | if user_signature: |
---|
460 | from gluon.globals import current |
---|
461 | if not current.session or not current.session.auth: |
---|
462 | return False |
---|
463 | hmac_key = current.session.auth.hmac_key |
---|
464 | if not hmac_key: |
---|
465 | return False |
---|
466 | |
---|
467 | # get our sig from request.get_vars for later comparison |
---|
468 | original_sig = request.get_vars._signature |
---|
469 | |
---|
470 | # now generate a new hmac for the remaining args & vars |
---|
471 | vars, args = request.get_vars, request.args |
---|
472 | |
---|
473 | # remove the signature var since it was not part of our signed message |
---|
474 | request.get_vars.pop('_signature') |
---|
475 | |
---|
476 | # join all the args & vars into one long string |
---|
477 | |
---|
478 | # always include all of the args |
---|
479 | other = args and urllib_quote('/' + '/'.join([str(x) for x in args])) or '' |
---|
480 | h_args = '/%s/%s/%s.%s%s' % (request.application, |
---|
481 | request.controller, |
---|
482 | request.function, |
---|
483 | request.extension, |
---|
484 | other) |
---|
485 | |
---|
486 | # but only include those vars specified (allows more flexibility for use with |
---|
487 | # forms or ajax) |
---|
488 | |
---|
489 | list_vars = [] |
---|
490 | for (key, vals) in sorted(vars.items()): |
---|
491 | if not isinstance(vals, (list, tuple)): |
---|
492 | vals = [vals] |
---|
493 | for val in vals: |
---|
494 | list_vars.append((key, val)) |
---|
495 | |
---|
496 | # which of the vars are to be included? |
---|
497 | if hash_vars is True: # include them all |
---|
498 | h_vars = list_vars |
---|
499 | elif hash_vars is False: # include none of them |
---|
500 | h_vars = '' |
---|
501 | else: # include just those specified |
---|
502 | # wrap in a try - if the desired vars have been removed it'll fail |
---|
503 | try: |
---|
504 | if hash_vars and not isinstance(hash_vars, (list, tuple)): |
---|
505 | hash_vars = [hash_vars] |
---|
506 | h_vars = [(k, v) for (k, v) in list_vars if k in hash_vars] |
---|
507 | except: |
---|
508 | # user has removed one of our vars! Immediate fail |
---|
509 | return False |
---|
510 | # build the full message string with both args & vars |
---|
511 | message = h_args + '?' + urlencode(sorted(h_vars)) |
---|
512 | |
---|
513 | # hash with the hmac_key provided |
---|
514 | sig = simple_hash(message, str(hmac_key), salt or '', digest_alg='sha1') |
---|
515 | |
---|
516 | # put _signature back in get_vars just in case a second call to URL.verify is performed |
---|
517 | # (otherwise it'll immediately return false) |
---|
518 | request.get_vars['_signature'] = original_sig |
---|
519 | |
---|
520 | # return whether or not the signature in the request matched the one we just generated |
---|
521 | # (I.E. was the message the same as the one we originally signed) |
---|
522 | |
---|
523 | return compare(original_sig, sig) |
---|
524 | |
---|
525 | URL.verify = verifyURL |
---|
526 | |
---|
527 | ON = True |
---|
528 | |
---|
529 | |
---|
530 | class XmlComponent(object): |
---|
531 | """ |
---|
532 | Abstract root for all Html components |
---|
533 | """ |
---|
534 | |
---|
535 | # TODO: move some DIV methods to here |
---|
536 | |
---|
537 | def xml(self): |
---|
538 | raise NotImplementedError |
---|
539 | |
---|
540 | def __mul__(self, n): |
---|
541 | return CAT(*[self for i in range(n)]) |
---|
542 | |
---|
543 | def __add__(self, other): |
---|
544 | if isinstance(self, CAT): |
---|
545 | components = self.components |
---|
546 | else: |
---|
547 | components = [self] |
---|
548 | if isinstance(other, CAT): |
---|
549 | components += other.components |
---|
550 | else: |
---|
551 | components += [other] |
---|
552 | return CAT(*components) |
---|
553 | |
---|
554 | def add_class(self, name): |
---|
555 | """ |
---|
556 | add a class to _class attribute |
---|
557 | """ |
---|
558 | c = self['_class'] |
---|
559 | classes = (set(c.split()) if c else set()) | set(name.split()) |
---|
560 | self['_class'] = ' '.join(classes) if classes else None |
---|
561 | return self |
---|
562 | |
---|
563 | def remove_class(self, name): |
---|
564 | """ |
---|
565 | remove a class from _class attribute |
---|
566 | """ |
---|
567 | c = self['_class'] |
---|
568 | classes = (set(c.split()) if c else set()) - set(name.split()) |
---|
569 | self['_class'] = ' '.join(classes) if classes else None |
---|
570 | return self |
---|
571 | |
---|
572 | |
---|
573 | class XML(XmlComponent): |
---|
574 | """ |
---|
575 | use it to wrap a string that contains XML/HTML so that it will not be |
---|
576 | escaped by the template |
---|
577 | |
---|
578 | Examples: |
---|
579 | |
---|
580 | >>> XML('<h1>Hello</h1>').xml() |
---|
581 | '<h1>Hello</h1>' |
---|
582 | """ |
---|
583 | |
---|
584 | def __init__( |
---|
585 | self, |
---|
586 | text, |
---|
587 | sanitize=False, |
---|
588 | permitted_tags=[ |
---|
589 | 'a', |
---|
590 | 'b', |
---|
591 | 'blockquote', |
---|
592 | 'br/', |
---|
593 | 'i', |
---|
594 | 'li', |
---|
595 | 'ol', |
---|
596 | 'ul', |
---|
597 | 'p', |
---|
598 | 'cite', |
---|
599 | 'code', |
---|
600 | 'pre', |
---|
601 | 'img/', |
---|
602 | 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', |
---|
603 | 'table', 'tr', 'td', 'div', |
---|
604 | 'strong', 'span', |
---|
605 | ], |
---|
606 | allowed_attributes={ |
---|
607 | 'a': ['href', 'title', 'target'], |
---|
608 | 'img': ['src', 'alt'], |
---|
609 | 'blockquote': ['type'], |
---|
610 | 'td': ['colspan'], |
---|
611 | }, |
---|
612 | ): |
---|
613 | """ |
---|
614 | Args: |
---|
615 | text: the XML text |
---|
616 | sanitize: sanitize text using the permitted tags and allowed |
---|
617 | attributes (default False) |
---|
618 | permitted_tags: list of permitted tags (default: simple list of |
---|
619 | tags) |
---|
620 | allowed_attributes: dictionary of allowed attributed (default |
---|
621 | for A, IMG and BlockQuote). |
---|
622 | The key is the tag; the value is a list of allowed attributes. |
---|
623 | """ |
---|
624 | if isinstance(text, unicodeT): |
---|
625 | text = to_native(text.encode('utf8', 'xmlcharrefreplace')) |
---|
626 | if sanitize: |
---|
627 | text = sanitizer.sanitize(text, permitted_tags, allowed_attributes) |
---|
628 | elif isinstance(text, bytes): |
---|
629 | text = to_native(text) |
---|
630 | elif not isinstance(text, str): |
---|
631 | text = str(text) |
---|
632 | self.text = text |
---|
633 | |
---|
634 | def xml(self): |
---|
635 | return to_bytes(self.text) |
---|
636 | |
---|
637 | def __str__(self): |
---|
638 | return self.text |
---|
639 | |
---|
640 | __repr__ = __str__ |
---|
641 | |
---|
642 | def __add__(self, other): |
---|
643 | return '%s%s' % (self, other) |
---|
644 | |
---|
645 | def __radd__(self, other): |
---|
646 | return '%s%s' % (other, self) |
---|
647 | |
---|
648 | def __cmp__(self, other): |
---|
649 | return cmp(str(self), str(other)) |
---|
650 | |
---|
651 | def __hash__(self): |
---|
652 | return hash(str(self)) |
---|
653 | |
---|
654 | # why was this here? Break unpickling in sessions |
---|
655 | # def __getattr__(self, name): |
---|
656 | # return getattr(str(self), name) |
---|
657 | |
---|
658 | def __getitem__(self, i): |
---|
659 | return str(self)[i] |
---|
660 | |
---|
661 | def __getslice__(self, i, j): |
---|
662 | return str(self)[i:j] |
---|
663 | |
---|
664 | def __iter__(self): |
---|
665 | for c in str(self): |
---|
666 | yield c |
---|
667 | |
---|
668 | def __len__(self): |
---|
669 | return len(str(self)) |
---|
670 | |
---|
671 | def flatten(self, render=None): |
---|
672 | """ |
---|
673 | returns the text stored by the XML object rendered |
---|
674 | by the `render` function |
---|
675 | """ |
---|
676 | if render: |
---|
677 | return render(self.text, None, {}) |
---|
678 | return self.text |
---|
679 | |
---|
680 | def elements(self, *args, **kargs): |
---|
681 | """ |
---|
682 | to be considered experimental since the behavior of this method |
---|
683 | is questionable |
---|
684 | another option could be `TAG(self.text).elements(*args, **kwargs)` |
---|
685 | """ |
---|
686 | return [] |
---|
687 | |
---|
688 | # ## important to allow safe session.flash=T(....) |
---|
689 | |
---|
690 | |
---|
691 | def XML_unpickle(data): |
---|
692 | return XML(marshal.loads(data)) |
---|
693 | |
---|
694 | |
---|
695 | def XML_pickle(data): |
---|
696 | return XML_unpickle, (marshal.dumps(str(data)),) |
---|
697 | copyreg.pickle(XML, XML_pickle, XML_unpickle) |
---|
698 | |
---|
699 | |
---|
700 | @implements_bool |
---|
701 | class DIV(XmlComponent): |
---|
702 | """ |
---|
703 | HTML helper, for easy generating and manipulating a DOM structure. |
---|
704 | Little or no validation is done. |
---|
705 | |
---|
706 | Behaves like a dictionary regarding updating of attributes. |
---|
707 | Behaves like a list regarding inserting/appending components. |
---|
708 | |
---|
709 | Examples: |
---|
710 | |
---|
711 | >>> DIV('hello', 'world', _style='color:red;').xml() |
---|
712 | '<div style=\"color:red;\">helloworld</div>' |
---|
713 | |
---|
714 | All other HTML helpers are derived from `DIV`. |
---|
715 | |
---|
716 | `_something="value"` attributes are transparently translated into |
---|
717 | `something="value"` HTML attributes |
---|
718 | """ |
---|
719 | |
---|
720 | # name of the tag, subclasses should update this |
---|
721 | # tags ending with a '/' denote classes that cannot |
---|
722 | # contain components |
---|
723 | tag = 'div' |
---|
724 | |
---|
725 | def __init__(self, *components, **attributes): |
---|
726 | """ |
---|
727 | Args: |
---|
728 | components: any components that should be nested in this element |
---|
729 | attributes: any attributes you want to give to this element |
---|
730 | |
---|
731 | Raises: |
---|
732 | SyntaxError: when a stand alone tag receives components |
---|
733 | """ |
---|
734 | |
---|
735 | if self.tag[-1:] == '/' and components: |
---|
736 | raise SyntaxError('<%s> tags cannot have components' |
---|
737 | % self.tag) |
---|
738 | if len(components) == 1 and isinstance(components[0], (list, tuple)): |
---|
739 | self.components = list(components[0]) |
---|
740 | else: |
---|
741 | self.components = list(components) |
---|
742 | self.attributes = attributes |
---|
743 | self._fixup() |
---|
744 | # converts special attributes in components attributes |
---|
745 | self.parent = None |
---|
746 | for c in self.components: |
---|
747 | self._setnode(c) |
---|
748 | self._postprocessing() |
---|
749 | |
---|
750 | def update(self, **kargs): |
---|
751 | """ |
---|
752 | dictionary like updating of the tag attributes |
---|
753 | """ |
---|
754 | |
---|
755 | for (key, value) in iteritems(kargs): |
---|
756 | self[key] = value |
---|
757 | return self |
---|
758 | |
---|
759 | def append(self, value): |
---|
760 | """ |
---|
761 | list style appending of components |
---|
762 | |
---|
763 | Examples: |
---|
764 | |
---|
765 | >>> a=DIV() |
---|
766 | >>> a.append(SPAN('x')) |
---|
767 | >>> print(a) |
---|
768 | <div><span>x</span></div> |
---|
769 | """ |
---|
770 | self._setnode(value) |
---|
771 | ret = self.components.append(value) |
---|
772 | self._fixup() |
---|
773 | return ret |
---|
774 | |
---|
775 | def insert(self, i, value): |
---|
776 | """ |
---|
777 | List-style inserting of components |
---|
778 | |
---|
779 | Examples: |
---|
780 | |
---|
781 | >>> a=DIV() |
---|
782 | >>> a.insert(0, SPAN('x')) |
---|
783 | >>> print(a) |
---|
784 | <div><span>x</span></div> |
---|
785 | """ |
---|
786 | self._setnode(value) |
---|
787 | ret = self.components.insert(i, value) |
---|
788 | self._fixup() |
---|
789 | return ret |
---|
790 | |
---|
791 | def __getitem__(self, i): |
---|
792 | """ |
---|
793 | Gets attribute with name 'i' or component #i. |
---|
794 | If attribute 'i' is not found returns None |
---|
795 | |
---|
796 | Args: |
---|
797 | i: index. If i is a string: the name of the attribute |
---|
798 | otherwise references to number of the component |
---|
799 | """ |
---|
800 | |
---|
801 | if isinstance(i, str): |
---|
802 | try: |
---|
803 | return self.attributes[i] |
---|
804 | except KeyError: |
---|
805 | return None |
---|
806 | else: |
---|
807 | return self.components[i] |
---|
808 | |
---|
809 | def get(self, i): |
---|
810 | return self.attributes.get(i) |
---|
811 | |
---|
812 | def __setitem__(self, i, value): |
---|
813 | """ |
---|
814 | Sets attribute with name 'i' or component #i. |
---|
815 | |
---|
816 | Args: |
---|
817 | i: index. If i is a string: the name of the attribute |
---|
818 | otherwise references to number of the component |
---|
819 | value: the new value |
---|
820 | """ |
---|
821 | self._setnode(value) |
---|
822 | if isinstance(i, (str, unicodeT)): |
---|
823 | self.attributes[i] = value |
---|
824 | else: |
---|
825 | self.components[i] = value |
---|
826 | |
---|
827 | def __delitem__(self, i): |
---|
828 | """ |
---|
829 | Deletes attribute with name 'i' or component #i. |
---|
830 | |
---|
831 | Args: |
---|
832 | i: index. If i is a string: the name of the attribute |
---|
833 | otherwise references to number of the component |
---|
834 | """ |
---|
835 | |
---|
836 | if isinstance(i, str): |
---|
837 | del self.attributes[i] |
---|
838 | else: |
---|
839 | del self.components[i] |
---|
840 | |
---|
841 | def __len__(self): |
---|
842 | """ |
---|
843 | Returns the number of included components |
---|
844 | """ |
---|
845 | return len(self.components) |
---|
846 | |
---|
847 | def __bool__(self): |
---|
848 | """ |
---|
849 | Always returns True |
---|
850 | """ |
---|
851 | return True |
---|
852 | |
---|
853 | def _fixup(self): |
---|
854 | """ |
---|
855 | Handling of provided components. |
---|
856 | |
---|
857 | Nothing to fixup yet. May be overridden by subclasses, |
---|
858 | eg for wrapping some components in another component or blocking them. |
---|
859 | """ |
---|
860 | return |
---|
861 | |
---|
862 | def _wrap_components(self, allowed_parents, |
---|
863 | wrap_parent=None, |
---|
864 | wrap_lambda=None): |
---|
865 | """ |
---|
866 | helper for _fixup. Checks if a component is in allowed_parents, |
---|
867 | otherwise wraps it in wrap_parent |
---|
868 | |
---|
869 | Args: |
---|
870 | allowed_parents: (tuple) classes that the component should be an |
---|
871 | instance of |
---|
872 | wrap_parent: the class to wrap the component in, if needed |
---|
873 | wrap_lambda: lambda to use for wrapping, if needed |
---|
874 | |
---|
875 | """ |
---|
876 | components = [] |
---|
877 | for c in self.components: |
---|
878 | if isinstance(c, (allowed_parents, CAT)): |
---|
879 | pass |
---|
880 | elif wrap_lambda: |
---|
881 | c = wrap_lambda(c) |
---|
882 | else: |
---|
883 | c = wrap_parent(c) |
---|
884 | if isinstance(c, DIV): |
---|
885 | c.parent = self |
---|
886 | components.append(c) |
---|
887 | self.components = components |
---|
888 | |
---|
889 | def _postprocessing(self): |
---|
890 | """ |
---|
891 | Handling of attributes (normally the ones not prefixed with '_'). |
---|
892 | |
---|
893 | Nothing to postprocess yet. May be overridden by subclasses |
---|
894 | """ |
---|
895 | return |
---|
896 | |
---|
897 | def _traverse(self, status, hideerror=False): |
---|
898 | # TODO: docstring |
---|
899 | newstatus = status |
---|
900 | for c in self.components: |
---|
901 | if hasattr(c, '_traverse') and callable(c._traverse): |
---|
902 | c.vars = self.vars |
---|
903 | c.request_vars = self.request_vars |
---|
904 | c.errors = self.errors |
---|
905 | c.latest = self.latest |
---|
906 | c.session = self.session |
---|
907 | c.formname = self.formname |
---|
908 | if not c.attributes.get('hideerror'): |
---|
909 | c['hideerror'] = hideerror or self.attributes.get('hideerror') |
---|
910 | newstatus = c._traverse(status, hideerror) and newstatus |
---|
911 | |
---|
912 | # for input, textarea, select, option |
---|
913 | # deal with 'value' and 'validation' |
---|
914 | |
---|
915 | name = self['_name'] |
---|
916 | if newstatus: |
---|
917 | newstatus = self._validate() |
---|
918 | self._postprocessing() |
---|
919 | elif 'old_value' in self.attributes: |
---|
920 | self['value'] = self['old_value'] |
---|
921 | self._postprocessing() |
---|
922 | elif name and name in self.vars: |
---|
923 | self['value'] = self.vars[name] |
---|
924 | self._postprocessing() |
---|
925 | if name: |
---|
926 | self.latest[name] = self['value'] |
---|
927 | return newstatus |
---|
928 | |
---|
929 | def _validate(self): |
---|
930 | """ |
---|
931 | nothing to validate yet. May be overridden by subclasses |
---|
932 | """ |
---|
933 | return True |
---|
934 | |
---|
935 | def _setnode(self, value): |
---|
936 | if isinstance(value, DIV): |
---|
937 | value.parent = self |
---|
938 | |
---|
939 | def _xml(self): |
---|
940 | """ |
---|
941 | Helper for xml generation. Returns separately: |
---|
942 | - the component attributes |
---|
943 | - the generated xml of the inner components |
---|
944 | |
---|
945 | Component attributes start with an underscore ('_') and |
---|
946 | do not have a False or None value. The underscore is removed. |
---|
947 | A value of True is replaced with the attribute name. |
---|
948 | |
---|
949 | Returns: |
---|
950 | tuple: (attributes, components) |
---|
951 | """ |
---|
952 | |
---|
953 | # get the attributes for this component |
---|
954 | # (they start with '_', others may have special meanings) |
---|
955 | attr = [] |
---|
956 | for key, value in iteritems(self.attributes): |
---|
957 | if key[:1] != '_': |
---|
958 | continue |
---|
959 | name = key[1:] |
---|
960 | if value is True: |
---|
961 | value = name |
---|
962 | elif value is False or value is None: |
---|
963 | continue |
---|
964 | attr.append((name, value)) |
---|
965 | data = self.attributes.get('data', {}) |
---|
966 | for key, value in iteritems(data): |
---|
967 | name = 'data-' + key |
---|
968 | value = data[key] |
---|
969 | attr.append((name, value)) |
---|
970 | attr.sort() |
---|
971 | fa = b'' |
---|
972 | for name, value in attr: |
---|
973 | fa += (b' %s="%s"') % (to_bytes(name), xmlescape(value, True)) |
---|
974 | |
---|
975 | # get the xml for the inner components |
---|
976 | co = b''.join([xmlescape(component) for component in self.components]) |
---|
977 | return (fa, co) |
---|
978 | |
---|
979 | def xml(self): |
---|
980 | """ |
---|
981 | generates the xml for this component. |
---|
982 | """ |
---|
983 | |
---|
984 | (fa, co) = self._xml() |
---|
985 | |
---|
986 | if not self.tag: |
---|
987 | return co |
---|
988 | |
---|
989 | tagname = to_bytes(self.tag) |
---|
990 | if tagname[-1:] == b'/': |
---|
991 | # <tag [attributes] /> |
---|
992 | return b'<%s%s />' % (tagname[:-1], fa) |
---|
993 | |
---|
994 | # else: <tag [attributes]> inner components xml </tag> |
---|
995 | xml_tag = b'<%s%s>%s</%s>' % (tagname, fa, co, tagname) |
---|
996 | return xml_tag |
---|
997 | |
---|
998 | def __str__(self): |
---|
999 | """ |
---|
1000 | str(COMPONENT) returns COMPONENT.xml() |
---|
1001 | """ |
---|
1002 | # In PY3 __str__ cannot return bytes (TypeError: __str__ returned non-string (type bytes)) |
---|
1003 | return to_native(self.xml()) |
---|
1004 | |
---|
1005 | def flatten(self, render=None): |
---|
1006 | """ |
---|
1007 | Returns the text stored by the DIV object rendered by the render function |
---|
1008 | the render function must take text, tagname, and attributes |
---|
1009 | `render=None` is equivalent to `render=lambda text, tag, attr: text` |
---|
1010 | |
---|
1011 | Examples: |
---|
1012 | |
---|
1013 | >>> markdown = lambda text, tag=None, attributes={}: \ |
---|
1014 | {None: re.sub('\s+',' ',text), \ |
---|
1015 | 'h1':'#'+text+'\\n\\n', \ |
---|
1016 | 'p':text+'\\n'}.get(tag,text) |
---|
1017 | >>> a=TAG('<h1>Header</h1><p>this is a test</p>') |
---|
1018 | >>> a.flatten(markdown) |
---|
1019 | '#Header\\n\\nthis is a test\\n' |
---|
1020 | """ |
---|
1021 | |
---|
1022 | text = '' |
---|
1023 | for c in self.components: |
---|
1024 | if isinstance(c, XmlComponent): |
---|
1025 | s = c.flatten(render) |
---|
1026 | elif render: |
---|
1027 | s = render(to_native(c)) |
---|
1028 | else: |
---|
1029 | s = to_native(c) |
---|
1030 | text += s |
---|
1031 | if render: |
---|
1032 | text = render(text, self.tag, self.attributes) |
---|
1033 | return text |
---|
1034 | |
---|
1035 | regex_tag = re.compile('^[\w\-\:]+') |
---|
1036 | regex_id = re.compile('#([\w\-]+)') |
---|
1037 | regex_class = re.compile('\.([\w\-]+)') |
---|
1038 | regex_attr = re.compile('\[([\w\-\:]+)=(.*?)\]') |
---|
1039 | |
---|
1040 | def elements(self, *args, **kargs): |
---|
1041 | """ |
---|
1042 | Find all components that match the supplied attribute dictionary, |
---|
1043 | or None if nothing could be found |
---|
1044 | |
---|
1045 | All components of the components are searched. |
---|
1046 | |
---|
1047 | Examples: |
---|
1048 | |
---|
1049 | >>> a = DIV(DIV(SPAN('x'),3,DIV(SPAN('y')))) |
---|
1050 | >>> for c in a.elements('span', first_only=True): c[0]='z' |
---|
1051 | >>> print(a) |
---|
1052 | <div><div><span>z</span>3<div><span>y</span></div></div></div> |
---|
1053 | >>> for c in a.elements('span'): c[0]='z' |
---|
1054 | >>> print(a) |
---|
1055 | <div><div><span>z</span>3<div><span>z</span></div></div></div> |
---|
1056 | |
---|
1057 | It also supports a syntax compatible with jQuery |
---|
1058 | |
---|
1059 | Examples: |
---|
1060 | |
---|
1061 | >>> a=TAG('<div><span><a id="1-1" u:v=$>hello</a></span><p class="this is a test">world</p></div>') |
---|
1062 | >>> for e in a.elements('div a#1-1, p.is'): print(e.flatten()) |
---|
1063 | hello |
---|
1064 | world |
---|
1065 | >>> for e in a.elements('#1-1'): print(e.flatten()) |
---|
1066 | hello |
---|
1067 | >>> a.elements('a[u:v=$]')[0].xml() |
---|
1068 | '<a id="1-1" u:v="$">hello</a>' |
---|
1069 | >>> a=FORM( INPUT(_type='text'), SELECT(list(range(1))), TEXTAREA() ) |
---|
1070 | >>> for c in a.elements('input, select, textarea'): c['_disabled'] = 'disabled' |
---|
1071 | >>> a.xml() |
---|
1072 | '<form action="#" enctype="multipart/form-data" method="post"><input disabled="disabled" type="text" /><select disabled="disabled"><option value="0">0</option></select><textarea cols="40" disabled="disabled" rows="10"></textarea></form>' |
---|
1073 | |
---|
1074 | Elements that are matched can also be replaced or removed by specifying |
---|
1075 | a "replace" argument (note, a list of the original matching elements |
---|
1076 | is still returned as usual). |
---|
1077 | |
---|
1078 | Examples: |
---|
1079 | |
---|
1080 | >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='abc'), SPAN('z', _class='abc')))) |
---|
1081 | >>> b = a.elements('span.abc', replace=P('x', _class='xyz')) |
---|
1082 | >>> print(a) # We should .xml() here instead of print |
---|
1083 | <div><div><p class="xyz">x</p><div><p class="xyz">x</p><p class="xyz">x</p></div></div></div> |
---|
1084 | |
---|
1085 | "replace" can be a callable, which will be passed the original element and |
---|
1086 | should return a new element to replace it. |
---|
1087 | |
---|
1088 | Examples: |
---|
1089 | |
---|
1090 | >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='abc'), SPAN('z', _class='abc')))) |
---|
1091 | >>> b = a.elements('span.abc', replace=lambda el: P(el[0], _class='xyz')) |
---|
1092 | >>> print(a) |
---|
1093 | <div><div><p class="xyz">x</p><div><p class="xyz">y</p><p class="xyz">z</p></div></div></div> |
---|
1094 | |
---|
1095 | If replace=None, matching elements will be removed completely. |
---|
1096 | |
---|
1097 | Examples: |
---|
1098 | |
---|
1099 | >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='abc'), SPAN('z', _class='abc')))) |
---|
1100 | >>> b = a.elements('span', find='y', replace=None) |
---|
1101 | >>> print(a) |
---|
1102 | <div><div><span class="abc">x</span><div><span class="abc">z</span></div></div></div> |
---|
1103 | |
---|
1104 | If a "find_text" argument is specified, elements will be searched for text |
---|
1105 | components that match find_text, and any matching text components will be |
---|
1106 | replaced (find_text is ignored if "replace" is not also specified). |
---|
1107 | Like the "find" argument, "find_text" can be a string or a compiled regex. |
---|
1108 | |
---|
1109 | Examples: |
---|
1110 | |
---|
1111 | >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='abc'), SPAN('z', _class='abc')))) |
---|
1112 | >>> b = a.elements(find_text=re.compile('x|y|z'), replace='hello') |
---|
1113 | >>> print(a) |
---|
1114 | <div><div><span class="abc">hello</span><div><span class="abc">hello</span><span class="abc">hello</span></div></div></div> |
---|
1115 | |
---|
1116 | If other attributes are specified along with find_text, then only components |
---|
1117 | that match the specified attributes will be searched for find_text. |
---|
1118 | |
---|
1119 | Examples: |
---|
1120 | |
---|
1121 | >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='efg'), SPAN('z', _class='abc')))) |
---|
1122 | >>> b = a.elements('span.efg', find_text=re.compile('x|y|z'), replace='hello') |
---|
1123 | >>> print(a) |
---|
1124 | <div><div><span class="abc">x</span><div><span class="efg">hello</span><span class="abc">z</span></div></div></div> |
---|
1125 | """ |
---|
1126 | if len(args) == 1: |
---|
1127 | args = [a.strip() for a in args[0].split(',')] |
---|
1128 | if len(args) > 1: |
---|
1129 | subset = [self.elements(a, **kargs) for a in args] |
---|
1130 | return reduce(lambda a, b: a + b, subset, []) |
---|
1131 | elif len(args) == 1: |
---|
1132 | items = args[0].split() |
---|
1133 | if len(items) > 1: |
---|
1134 | subset = [a.elements(' '.join( |
---|
1135 | items[1:]), **kargs) for a in self.elements(items[0])] |
---|
1136 | return reduce(lambda a, b: a + b, subset, []) |
---|
1137 | else: |
---|
1138 | item = items[0] |
---|
1139 | if '#' in item or '.' in item or '[' in item: |
---|
1140 | match_tag = self.regex_tag.search(item) |
---|
1141 | match_id = self.regex_id.search(item) |
---|
1142 | match_class = self.regex_class.search(item) |
---|
1143 | match_attr = self.regex_attr.finditer(item) |
---|
1144 | args = [] |
---|
1145 | if match_tag: |
---|
1146 | args = [match_tag.group()] |
---|
1147 | if match_id: |
---|
1148 | kargs['_id'] = match_id.group(1) |
---|
1149 | if match_class: |
---|
1150 | kargs['_class'] = re.compile('(?<!\w)%s(?!\w)' % |
---|
1151 | match_class.group(1).replace('-', '\\-').replace(':', '\\:')) |
---|
1152 | for item in match_attr: |
---|
1153 | kargs['_' + item.group(1)] = item.group(2) |
---|
1154 | return self.elements(*args, **kargs) |
---|
1155 | # make a copy of the components |
---|
1156 | matches = [] |
---|
1157 | # check if the component has an attribute with the same |
---|
1158 | # value as provided |
---|
1159 | tag = to_native(getattr(self, 'tag')).replace('/', '') |
---|
1160 | check = not (args and tag not in args) |
---|
1161 | for (key, value) in iteritems(kargs): |
---|
1162 | if key not in ['first_only', 'replace', 'find_text']: |
---|
1163 | if isinstance(value, (str, int)): |
---|
1164 | if str(self[key]) != str(value): |
---|
1165 | check = False |
---|
1166 | elif key in self.attributes: |
---|
1167 | if not value.search(str(self[key])): |
---|
1168 | check = False |
---|
1169 | else: |
---|
1170 | check = False |
---|
1171 | if 'find' in kargs: |
---|
1172 | find = kargs['find'] |
---|
1173 | is_regex = not isinstance(find, (str, int)) |
---|
1174 | for c in self.components: |
---|
1175 | if (isinstance(c, str) and ((is_regex and find.search(c)) or |
---|
1176 | (str(find) in c))): |
---|
1177 | check = True |
---|
1178 | # if found, return the component |
---|
1179 | if check: |
---|
1180 | matches.append(self) |
---|
1181 | |
---|
1182 | first_only = kargs.get('first_only', False) |
---|
1183 | replace = kargs.get('replace', False) |
---|
1184 | find_text = replace is not False and kargs.get('find_text', False) |
---|
1185 | is_regex = not isinstance(find_text, (str, int, bool)) |
---|
1186 | find_components = not (check and first_only) |
---|
1187 | |
---|
1188 | def replace_component(i): |
---|
1189 | if replace is None: |
---|
1190 | del self[i] |
---|
1191 | return i |
---|
1192 | else: |
---|
1193 | self[i] = replace(self[i]) if callable(replace) else replace |
---|
1194 | return i + 1 |
---|
1195 | # loop the components |
---|
1196 | if find_text or find_components: |
---|
1197 | i = 0 |
---|
1198 | while i < len(self.components): |
---|
1199 | c = self[i] |
---|
1200 | j = i + 1 |
---|
1201 | if check and find_text and isinstance(c, str) and \ |
---|
1202 | ((is_regex and find_text.search(c)) or (str(find_text) in c)): |
---|
1203 | j = replace_component(i) |
---|
1204 | elif find_components and isinstance(c, XmlComponent): |
---|
1205 | child_matches = c.elements(*args, **kargs) |
---|
1206 | if len(child_matches): |
---|
1207 | if not find_text and replace is not False and child_matches[0] is c: |
---|
1208 | j = replace_component(i) |
---|
1209 | if first_only: |
---|
1210 | return child_matches |
---|
1211 | matches.extend(child_matches) |
---|
1212 | i = j |
---|
1213 | return matches |
---|
1214 | |
---|
1215 | def element(self, *args, **kargs): |
---|
1216 | """ |
---|
1217 | Finds the first component that matches the supplied attribute dictionary, |
---|
1218 | or None if nothing could be found |
---|
1219 | |
---|
1220 | Also the components of the components are searched. |
---|
1221 | """ |
---|
1222 | kargs['first_only'] = True |
---|
1223 | elements = self.elements(*args, **kargs) |
---|
1224 | if not elements: |
---|
1225 | # we found nothing |
---|
1226 | return None |
---|
1227 | return elements[0] |
---|
1228 | |
---|
1229 | def siblings(self, *args, **kargs): |
---|
1230 | """ |
---|
1231 | Finds all sibling components that match the supplied argument list |
---|
1232 | and attribute dictionary, or None if nothing could be found |
---|
1233 | """ |
---|
1234 | sibs = [s for s in self.parent.components if not s == self] |
---|
1235 | matches = [] |
---|
1236 | first_only = False |
---|
1237 | if 'first_only' in kargs: |
---|
1238 | first_only = kargs.pop('first_only') |
---|
1239 | for c in sibs: |
---|
1240 | try: |
---|
1241 | check = True |
---|
1242 | tag = getattr(c, 'tag').replace("/", "") |
---|
1243 | if args and tag not in args: |
---|
1244 | check = False |
---|
1245 | for (key, value) in iteritems(kargs): |
---|
1246 | if c[key] != value: |
---|
1247 | check = False |
---|
1248 | if check: |
---|
1249 | matches.append(c) |
---|
1250 | if first_only: |
---|
1251 | break |
---|
1252 | except: |
---|
1253 | pass |
---|
1254 | return matches |
---|
1255 | |
---|
1256 | def sibling(self, *args, **kargs): |
---|
1257 | """ |
---|
1258 | Finds the first sibling component that match the supplied argument list |
---|
1259 | and attribute dictionary, or None if nothing could be found |
---|
1260 | """ |
---|
1261 | kargs['first_only'] = True |
---|
1262 | sibs = self.siblings(*args, **kargs) |
---|
1263 | if not sibs: |
---|
1264 | return None |
---|
1265 | return sibs[0] |
---|
1266 | |
---|
1267 | |
---|
1268 | class CAT(DIV): |
---|
1269 | |
---|
1270 | tag = '' |
---|
1271 | |
---|
1272 | |
---|
1273 | def TAG_unpickler(data): |
---|
1274 | return pickle.loads(data) |
---|
1275 | |
---|
1276 | |
---|
1277 | def TAG_pickler(data): |
---|
1278 | d = DIV() |
---|
1279 | d.__dict__ = data.__dict__ |
---|
1280 | marshal_dump = pickle.dumps(d, pickle.HIGHEST_PROTOCOL) |
---|
1281 | return (TAG_unpickler, (marshal_dump,)) |
---|
1282 | |
---|
1283 | |
---|
1284 | class __tag_div__(DIV): |
---|
1285 | def __init__(self, name, *a, **b): |
---|
1286 | DIV.__init__(self, *a, **b) |
---|
1287 | self.tag = name |
---|
1288 | |
---|
1289 | copyreg.pickle(__tag_div__, TAG_pickler, TAG_unpickler) |
---|
1290 | |
---|
1291 | |
---|
1292 | class __TAG__(XmlComponent): |
---|
1293 | |
---|
1294 | """ |
---|
1295 | TAG factory |
---|
1296 | |
---|
1297 | Examples: |
---|
1298 | |
---|
1299 | >>> print(TAG.first(TAG.second('test'), _key = 3)) |
---|
1300 | <first key=\"3\"><second>test</second></first> |
---|
1301 | |
---|
1302 | """ |
---|
1303 | |
---|
1304 | def __getitem__(self, name): |
---|
1305 | return self.__getattr__(name) |
---|
1306 | |
---|
1307 | def __getattr__(self, name): |
---|
1308 | if name[-1:] == '_': |
---|
1309 | name = name[:-1] + '/' |
---|
1310 | return lambda *a, **b: __tag_div__(name, *a, **b) |
---|
1311 | |
---|
1312 | def __call__(self, html): |
---|
1313 | return web2pyHTMLParser(decoder.decoder(html)).tree |
---|
1314 | |
---|
1315 | TAG = __TAG__() |
---|
1316 | |
---|
1317 | |
---|
1318 | class HTML(DIV): |
---|
1319 | """ |
---|
1320 | There are four predefined document type definitions. |
---|
1321 | They can be specified in the 'doctype' parameter: |
---|
1322 | |
---|
1323 | - 'strict' enables strict doctype |
---|
1324 | - 'transitional' enables transitional doctype (default) |
---|
1325 | - 'frameset' enables frameset doctype |
---|
1326 | - 'html5' enables HTML 5 doctype |
---|
1327 | - any other string will be treated as user's own doctype |
---|
1328 | |
---|
1329 | 'lang' parameter specifies the language of the document. |
---|
1330 | Defaults to 'en'. |
---|
1331 | |
---|
1332 | See also `DIV` |
---|
1333 | """ |
---|
1334 | |
---|
1335 | tag = b'html' |
---|
1336 | |
---|
1337 | strict = b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n' |
---|
1338 | transitional = \ |
---|
1339 | b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n' |
---|
1340 | frameset = \ |
---|
1341 | b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">\n' |
---|
1342 | html5 = b'<!DOCTYPE HTML>\n' |
---|
1343 | |
---|
1344 | def xml(self): |
---|
1345 | lang = self['lang'] |
---|
1346 | if not lang: |
---|
1347 | lang = 'en' |
---|
1348 | self.attributes['_lang'] = lang |
---|
1349 | doctype = self['doctype'] |
---|
1350 | if doctype is None: |
---|
1351 | doctype = self.transitional |
---|
1352 | elif doctype == 'strict': |
---|
1353 | doctype = self.strict |
---|
1354 | elif doctype == 'transitional': |
---|
1355 | doctype = self.transitional |
---|
1356 | elif doctype == 'frameset': |
---|
1357 | doctype = self.frameset |
---|
1358 | elif doctype == 'html5': |
---|
1359 | doctype = self.html5 |
---|
1360 | elif doctype == '': |
---|
1361 | doctype = b'' |
---|
1362 | else: |
---|
1363 | doctype = b'%s\n' % to_bytes(doctype) |
---|
1364 | (fa, co) = self._xml() |
---|
1365 | |
---|
1366 | return b'%s<%s%s>%s</%s>' % (doctype, self.tag, fa, co, self.tag) |
---|
1367 | |
---|
1368 | |
---|
1369 | class XHTML(DIV): |
---|
1370 | """ |
---|
1371 | This is XHTML version of the HTML helper. |
---|
1372 | |
---|
1373 | There are three predefined document type definitions. |
---|
1374 | They can be specified in the 'doctype' parameter: |
---|
1375 | |
---|
1376 | - 'strict' enables strict doctype |
---|
1377 | - 'transitional' enables transitional doctype (default) |
---|
1378 | - 'frameset' enables frameset doctype |
---|
1379 | - any other string will be treated as user's own doctype |
---|
1380 | |
---|
1381 | 'lang' parameter specifies the language of the document and the xml document. |
---|
1382 | Defaults to 'en'. |
---|
1383 | |
---|
1384 | 'xmlns' parameter specifies the xml namespace. |
---|
1385 | Defaults to 'http://www.w3.org/1999/xhtml'. |
---|
1386 | |
---|
1387 | See also `DIV` |
---|
1388 | """ |
---|
1389 | |
---|
1390 | tag = b'html' |
---|
1391 | |
---|
1392 | strict = b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n' |
---|
1393 | transitional = b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n' |
---|
1394 | frameset = b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">\n' |
---|
1395 | xmlns = 'http://www.w3.org/1999/xhtml' |
---|
1396 | |
---|
1397 | def xml(self): |
---|
1398 | xmlns = self['xmlns'] |
---|
1399 | if xmlns: |
---|
1400 | self.attributes['_xmlns'] = xmlns |
---|
1401 | else: |
---|
1402 | self.attributes['_xmlns'] = self.xmlns |
---|
1403 | lang = self['lang'] |
---|
1404 | if not lang: |
---|
1405 | lang = 'en' |
---|
1406 | self.attributes['_lang'] = lang |
---|
1407 | self.attributes['_xml:lang'] = lang |
---|
1408 | doctype = self['doctype'] |
---|
1409 | if doctype: |
---|
1410 | if doctype == 'strict': |
---|
1411 | doctype = self.strict |
---|
1412 | elif doctype == 'transitional': |
---|
1413 | doctype = self.transitional |
---|
1414 | elif doctype == 'frameset': |
---|
1415 | doctype = self.frameset |
---|
1416 | else: |
---|
1417 | doctype = b'%s\n' % to_bytes(doctype) |
---|
1418 | else: |
---|
1419 | doctype = self.transitional |
---|
1420 | (fa, co) = self._xml() |
---|
1421 | return b'%s<%s%s>%s</%s>' % (doctype, self.tag, fa, co, self.tag) |
---|
1422 | |
---|
1423 | |
---|
1424 | class HEAD(DIV): |
---|
1425 | |
---|
1426 | tag = 'head' |
---|
1427 | |
---|
1428 | |
---|
1429 | class TITLE(DIV): |
---|
1430 | |
---|
1431 | tag = 'title' |
---|
1432 | |
---|
1433 | |
---|
1434 | class META(DIV): |
---|
1435 | |
---|
1436 | tag = 'meta/' |
---|
1437 | |
---|
1438 | |
---|
1439 | class LINK(DIV): |
---|
1440 | |
---|
1441 | tag = 'link/' |
---|
1442 | |
---|
1443 | |
---|
1444 | class SCRIPT(DIV): |
---|
1445 | |
---|
1446 | tag = b'script' |
---|
1447 | tagname = to_bytes(tag) |
---|
1448 | |
---|
1449 | def xml(self): |
---|
1450 | (fa, co) = self._xml() |
---|
1451 | fa = to_bytes(fa) |
---|
1452 | # no escaping of subcomponents |
---|
1453 | co = b'\n'.join([to_bytes(component) for component in |
---|
1454 | self.components]) |
---|
1455 | if co: |
---|
1456 | # <script [attributes]><!--//--><![CDATA[//><!-- |
---|
1457 | # script body |
---|
1458 | # //--><!]]></script> |
---|
1459 | # return '<%s%s><!--//--><![CDATA[//><!--\n%s\n//--><!]]></%s>' % (self.tag, fa, co, self.tag) |
---|
1460 | return b'<%s%s><!--\n%s\n//--></%s>' % (self.tagname, fa, co, self.tagname) |
---|
1461 | else: |
---|
1462 | return DIV.xml(self) |
---|
1463 | |
---|
1464 | |
---|
1465 | class STYLE(DIV): |
---|
1466 | |
---|
1467 | tag = 'style' |
---|
1468 | tagname = to_bytes(tag) |
---|
1469 | |
---|
1470 | def xml(self): |
---|
1471 | (fa, co) = self._xml() |
---|
1472 | fa = to_bytes(fa) |
---|
1473 | # no escaping of subcomponents |
---|
1474 | co = b'\n'.join([to_bytes(component) for component in |
---|
1475 | self.components]) |
---|
1476 | if co: |
---|
1477 | # <style [attributes]><!--/*--><![CDATA[/*><!--*/ |
---|
1478 | # style body |
---|
1479 | # /*]]>*/--></style> |
---|
1480 | return b'<%s%s><!--/*--><![CDATA[/*><!--*/\n%s\n/*]]>*/--></%s>' % (self.tagname, fa, co, self.tagname) |
---|
1481 | else: |
---|
1482 | return DIV.xml(self) |
---|
1483 | |
---|
1484 | |
---|
1485 | class IMG(DIV): |
---|
1486 | |
---|
1487 | tag = 'img/' |
---|
1488 | |
---|
1489 | |
---|
1490 | class SPAN(DIV): |
---|
1491 | |
---|
1492 | tag = 'span' |
---|
1493 | |
---|
1494 | |
---|
1495 | class BODY(DIV): |
---|
1496 | |
---|
1497 | tag = 'body' |
---|
1498 | |
---|
1499 | |
---|
1500 | class H1(DIV): |
---|
1501 | |
---|
1502 | tag = 'h1' |
---|
1503 | |
---|
1504 | |
---|
1505 | class H2(DIV): |
---|
1506 | |
---|
1507 | tag = 'h2' |
---|
1508 | |
---|
1509 | |
---|
1510 | class H3(DIV): |
---|
1511 | |
---|
1512 | tag = 'h3' |
---|
1513 | |
---|
1514 | |
---|
1515 | class H4(DIV): |
---|
1516 | |
---|
1517 | tag = 'h4' |
---|
1518 | |
---|
1519 | |
---|
1520 | class H5(DIV): |
---|
1521 | |
---|
1522 | tag = 'h5' |
---|
1523 | |
---|
1524 | |
---|
1525 | class H6(DIV): |
---|
1526 | |
---|
1527 | tag = 'h6' |
---|
1528 | |
---|
1529 | |
---|
1530 | class P(DIV): |
---|
1531 | """ |
---|
1532 | Will replace ``\\n`` by ``<br />`` if the `cr2br` attribute is provided. |
---|
1533 | |
---|
1534 | see also `DIV` |
---|
1535 | """ |
---|
1536 | |
---|
1537 | tag = 'p' |
---|
1538 | |
---|
1539 | def xml(self): |
---|
1540 | text = DIV.xml(self) |
---|
1541 | if self['cr2br']: |
---|
1542 | text = text.replace(b'\n', b'<br />') |
---|
1543 | return text |
---|
1544 | |
---|
1545 | |
---|
1546 | class STRONG(DIV): |
---|
1547 | |
---|
1548 | tag = 'strong' |
---|
1549 | |
---|
1550 | |
---|
1551 | class B(DIV): |
---|
1552 | |
---|
1553 | tag = 'b' |
---|
1554 | |
---|
1555 | |
---|
1556 | class BR(DIV): |
---|
1557 | |
---|
1558 | tag = 'br/' |
---|
1559 | |
---|
1560 | |
---|
1561 | class HR(DIV): |
---|
1562 | |
---|
1563 | tag = 'hr/' |
---|
1564 | |
---|
1565 | |
---|
1566 | class A(DIV): |
---|
1567 | """ |
---|
1568 | Generates an A() link. |
---|
1569 | A() in web2py is really important and with the included web2py.js |
---|
1570 | allows lots of Ajax interactions in the page |
---|
1571 | |
---|
1572 | On top of "usual" `_attributes`, it takes |
---|
1573 | |
---|
1574 | Args: |
---|
1575 | callback: an url to call but not redirect to |
---|
1576 | cid: if you want to load the _href into an element of the page (component) |
---|
1577 | pass its id (without the #) here |
---|
1578 | delete: element to delete after calling callback |
---|
1579 | target: same thing as cid |
---|
1580 | confirm: text to display upon a callback with a delete |
---|
1581 | noconfirm: don't display alert upon a callback with delete |
---|
1582 | |
---|
1583 | """ |
---|
1584 | |
---|
1585 | tag = 'a' |
---|
1586 | |
---|
1587 | def xml(self): |
---|
1588 | if not self.components and self['_href']: |
---|
1589 | self.append(self['_href']) |
---|
1590 | disable_needed = ['callback', 'cid', 'delete', 'component', 'target'] |
---|
1591 | disable_needed = any((self[attr] for attr in disable_needed)) |
---|
1592 | if disable_needed: |
---|
1593 | self['_data-w2p_disable_with'] = self['_disable_with'] or 'default' |
---|
1594 | self['_disable_with'] = None |
---|
1595 | if self['callback'] and not self['_id']: |
---|
1596 | self['_id'] = web2py_uuid() |
---|
1597 | if self['delete']: |
---|
1598 | self['_data-w2p_remove'] = self['delete'] |
---|
1599 | if self['target']: |
---|
1600 | if self['target'] == '<self>': |
---|
1601 | self['target'] = self['_id'] |
---|
1602 | self['_data-w2p_target'] = self['target'] |
---|
1603 | if self['component']: |
---|
1604 | self['_data-w2p_method'] = 'GET' |
---|
1605 | self['_href'] = self['component'] |
---|
1606 | elif self['callback']: |
---|
1607 | self['_data-w2p_method'] = 'POST' |
---|
1608 | self['_href'] = self['callback'] |
---|
1609 | if self['delete'] and not self['noconfirm']: |
---|
1610 | if not self['confirm']: |
---|
1611 | self['_data-w2p_confirm'] = 'default' |
---|
1612 | else: |
---|
1613 | self['_data-w2p_confirm'] = self['confirm'] |
---|
1614 | elif self['cid']: |
---|
1615 | self['_data-w2p_method'] = 'GET' |
---|
1616 | self['_data-w2p_target'] = self['cid'] |
---|
1617 | if self['pre_call']: |
---|
1618 | self['_data-w2p_pre_call'] = self['pre_call'] |
---|
1619 | return DIV.xml(self) |
---|
1620 | |
---|
1621 | |
---|
1622 | class BUTTON(DIV): |
---|
1623 | |
---|
1624 | tag = 'button' |
---|
1625 | |
---|
1626 | |
---|
1627 | class EM(DIV): |
---|
1628 | |
---|
1629 | tag = 'em' |
---|
1630 | |
---|
1631 | |
---|
1632 | class EMBED(DIV): |
---|
1633 | |
---|
1634 | tag = 'embed/' |
---|
1635 | |
---|
1636 | |
---|
1637 | class TT(DIV): |
---|
1638 | |
---|
1639 | tag = 'tt' |
---|
1640 | |
---|
1641 | |
---|
1642 | class PRE(DIV): |
---|
1643 | |
---|
1644 | tag = 'pre' |
---|
1645 | |
---|
1646 | |
---|
1647 | class CENTER(DIV): |
---|
1648 | |
---|
1649 | tag = 'center' |
---|
1650 | |
---|
1651 | |
---|
1652 | class CODE(DIV): |
---|
1653 | |
---|
1654 | """ |
---|
1655 | Displays code in HTML with syntax highlighting. |
---|
1656 | |
---|
1657 | Args: |
---|
1658 | language: indicates the language, otherwise PYTHON is assumed |
---|
1659 | link: can provide a link |
---|
1660 | styles: for styles |
---|
1661 | |
---|
1662 | Examples: |
---|
1663 | |
---|
1664 | {{=CODE(\"print('hello world')\", language='python', link=None, |
---|
1665 | counter=1, styles={}, highlight_line=None)}} |
---|
1666 | |
---|
1667 | |
---|
1668 | supported languages are |
---|
1669 | |
---|
1670 | "python", "html_plain", "c", "cpp", "web2py", "html" |
---|
1671 | |
---|
1672 | The "html" language interprets {{ and }} tags as "web2py" code, |
---|
1673 | "html_plain" doesn't. |
---|
1674 | |
---|
1675 | if a link='/examples/global/vars/' is provided web2py keywords are linked to |
---|
1676 | the online docs. |
---|
1677 | |
---|
1678 | the counter is used for line numbering, counter can be None or a prompt |
---|
1679 | string. |
---|
1680 | """ |
---|
1681 | |
---|
1682 | def xml(self): |
---|
1683 | language = self['language'] or 'PYTHON' |
---|
1684 | link = self['link'] |
---|
1685 | counter = self.attributes.get('counter', 1) |
---|
1686 | highlight_line = self.attributes.get('highlight_line', None) |
---|
1687 | context_lines = self.attributes.get('context_lines', None) |
---|
1688 | styles = self['styles'] or {} |
---|
1689 | return highlight( |
---|
1690 | join(self.components), |
---|
1691 | language=language, |
---|
1692 | link=link, |
---|
1693 | counter=counter, |
---|
1694 | styles=styles, |
---|
1695 | attributes=self.attributes, |
---|
1696 | highlight_line=highlight_line, |
---|
1697 | context_lines=context_lines, |
---|
1698 | ) |
---|
1699 | |
---|
1700 | |
---|
1701 | class LABEL(DIV): |
---|
1702 | |
---|
1703 | tag = 'label' |
---|
1704 | |
---|
1705 | |
---|
1706 | class LI(DIV): |
---|
1707 | |
---|
1708 | tag = 'li' |
---|
1709 | |
---|
1710 | |
---|
1711 | class UL(DIV): |
---|
1712 | """ |
---|
1713 | UL Component. |
---|
1714 | |
---|
1715 | If subcomponents are not LI-components they will be wrapped in a LI |
---|
1716 | |
---|
1717 | """ |
---|
1718 | |
---|
1719 | tag = 'ul' |
---|
1720 | |
---|
1721 | def _fixup(self): |
---|
1722 | self._wrap_components(LI, LI) |
---|
1723 | |
---|
1724 | |
---|
1725 | class OL(UL): |
---|
1726 | |
---|
1727 | tag = 'ol' |
---|
1728 | |
---|
1729 | |
---|
1730 | class TD(DIV): |
---|
1731 | |
---|
1732 | tag = 'td' |
---|
1733 | |
---|
1734 | |
---|
1735 | class TH(DIV): |
---|
1736 | |
---|
1737 | tag = 'th' |
---|
1738 | |
---|
1739 | |
---|
1740 | class TR(DIV): |
---|
1741 | """ |
---|
1742 | TR Component. |
---|
1743 | |
---|
1744 | If subcomponents are not TD/TH-components they will be wrapped in a TD |
---|
1745 | |
---|
1746 | """ |
---|
1747 | |
---|
1748 | tag = 'tr' |
---|
1749 | |
---|
1750 | def _fixup(self): |
---|
1751 | self._wrap_components((TD, TH), TD) |
---|
1752 | |
---|
1753 | |
---|
1754 | class __TRHEAD__(DIV): |
---|
1755 | """ |
---|
1756 | __TRHEAD__ Component, internal only |
---|
1757 | |
---|
1758 | If subcomponents are not TD/TH-components they will be wrapped in a TH |
---|
1759 | |
---|
1760 | """ |
---|
1761 | |
---|
1762 | tag = 'tr' |
---|
1763 | |
---|
1764 | def _fixup(self): |
---|
1765 | self._wrap_components((TD, TH), TH) |
---|
1766 | |
---|
1767 | |
---|
1768 | class THEAD(DIV): |
---|
1769 | |
---|
1770 | tag = 'thead' |
---|
1771 | |
---|
1772 | def _fixup(self): |
---|
1773 | self._wrap_components((__TRHEAD__, TR), __TRHEAD__) |
---|
1774 | |
---|
1775 | |
---|
1776 | class TBODY(DIV): |
---|
1777 | |
---|
1778 | tag = 'tbody' |
---|
1779 | |
---|
1780 | def _fixup(self): |
---|
1781 | self._wrap_components(TR, TR) |
---|
1782 | |
---|
1783 | |
---|
1784 | class TFOOT(DIV): |
---|
1785 | |
---|
1786 | tag = 'tfoot' |
---|
1787 | |
---|
1788 | def _fixup(self): |
---|
1789 | self._wrap_components(TR, TR) |
---|
1790 | |
---|
1791 | |
---|
1792 | class COL(DIV): |
---|
1793 | |
---|
1794 | tag = 'col/' |
---|
1795 | |
---|
1796 | |
---|
1797 | class COLGROUP(DIV): |
---|
1798 | |
---|
1799 | tag = 'colgroup' |
---|
1800 | |
---|
1801 | |
---|
1802 | class TABLE(DIV): |
---|
1803 | """ |
---|
1804 | TABLE Component. |
---|
1805 | |
---|
1806 | If subcomponents are not TR/TBODY/THEAD/TFOOT-components |
---|
1807 | they will be wrapped in a TR |
---|
1808 | |
---|
1809 | """ |
---|
1810 | |
---|
1811 | tag = 'table' |
---|
1812 | |
---|
1813 | def _fixup(self): |
---|
1814 | self._wrap_components((TR, TBODY, THEAD, TFOOT, COL, COLGROUP), TR) |
---|
1815 | |
---|
1816 | |
---|
1817 | class I(DIV): |
---|
1818 | |
---|
1819 | tag = 'i' |
---|
1820 | |
---|
1821 | |
---|
1822 | class IFRAME(DIV): |
---|
1823 | |
---|
1824 | tag = 'iframe' |
---|
1825 | |
---|
1826 | |
---|
1827 | class INPUT(DIV): |
---|
1828 | |
---|
1829 | """ |
---|
1830 | INPUT Component |
---|
1831 | |
---|
1832 | Takes two special attributes value= and requires=. |
---|
1833 | |
---|
1834 | Args: |
---|
1835 | value: used to pass the initial value for the input field. |
---|
1836 | value differs from _value because it works for checkboxes, radio, |
---|
1837 | textarea and select/option too. |
---|
1838 | For a checkbox value should be '' or 'on'. |
---|
1839 | For a radio or select/option value should be the _value |
---|
1840 | of the checked/selected item. |
---|
1841 | |
---|
1842 | requires: should be None, or a validator or a list of validators |
---|
1843 | for the value of the field. |
---|
1844 | |
---|
1845 | Examples: |
---|
1846 | |
---|
1847 | >>> INPUT(_type='text', _name='name', value='Max').xml() |
---|
1848 | '<input name=\"name\" type=\"text\" value=\"Max\" />' |
---|
1849 | |
---|
1850 | >>> INPUT(_type='checkbox', _name='checkbox', value='on').xml() |
---|
1851 | '<input checked=\"checked\" name=\"checkbox\" type=\"checkbox\" value=\"on\" />' |
---|
1852 | |
---|
1853 | >>> INPUT(_type='radio', _name='radio', _value='yes', value='yes').xml() |
---|
1854 | '<input checked=\"checked\" name=\"radio\" type=\"radio\" value=\"yes\" />' |
---|
1855 | |
---|
1856 | >>> INPUT(_type='radio', _name='radio', _value='no', value='yes').xml() |
---|
1857 | '<input name=\"radio\" type=\"radio\" value=\"no\" />' |
---|
1858 | |
---|
1859 | |
---|
1860 | """ |
---|
1861 | |
---|
1862 | tag = 'input/' |
---|
1863 | |
---|
1864 | def _validate(self): |
---|
1865 | |
---|
1866 | # # this only changes value, not _value |
---|
1867 | |
---|
1868 | name = self['_name'] |
---|
1869 | if name is None or name == '': |
---|
1870 | return True |
---|
1871 | name = str(name) |
---|
1872 | request_vars_get = self.request_vars.get |
---|
1873 | if self['_type'] != 'checkbox': |
---|
1874 | self['old_value'] = self['value'] or self['_value'] or '' |
---|
1875 | value = request_vars_get(name, '') |
---|
1876 | self['value'] = value if not hasattr(value, 'file') else None |
---|
1877 | else: |
---|
1878 | self['old_value'] = self['value'] or False |
---|
1879 | value = request_vars_get(name) |
---|
1880 | if isinstance(value, (tuple, list)): |
---|
1881 | self['value'] = self['_value'] in value |
---|
1882 | else: |
---|
1883 | self['value'] = self['_value'] == value |
---|
1884 | requires = self['requires'] |
---|
1885 | if requires: |
---|
1886 | if not isinstance(requires, (list, tuple)): |
---|
1887 | requires = [requires] |
---|
1888 | for k, validator in enumerate(requires): |
---|
1889 | try: |
---|
1890 | (value, errors) = validator(value) |
---|
1891 | except: |
---|
1892 | import traceback |
---|
1893 | print(traceback.format_exc()) |
---|
1894 | msg = "Validation error, field:%s %s" % (name, validator) |
---|
1895 | raise Exception(msg) |
---|
1896 | if errors is not None: |
---|
1897 | self.vars[name] = value |
---|
1898 | self.errors[name] = errors |
---|
1899 | break |
---|
1900 | if name not in self.errors: |
---|
1901 | self.vars[name] = value |
---|
1902 | return True |
---|
1903 | return False |
---|
1904 | |
---|
1905 | def _postprocessing(self): |
---|
1906 | t = self['_type'] |
---|
1907 | if not t: |
---|
1908 | t = self['_type'] = 'text' |
---|
1909 | t = t.lower() |
---|
1910 | value = self['value'] |
---|
1911 | if self['_value'] is None or isinstance(self['_value'], cgi.FieldStorage): |
---|
1912 | _value = None |
---|
1913 | else: |
---|
1914 | _value = str(self['_value']) |
---|
1915 | if '_checked' in self.attributes and 'value' not in self.attributes: |
---|
1916 | pass |
---|
1917 | elif t == 'checkbox': |
---|
1918 | if not _value: |
---|
1919 | _value = self['_value'] = 'on' |
---|
1920 | if not value: |
---|
1921 | value = [] |
---|
1922 | elif value is True: |
---|
1923 | value = [_value] |
---|
1924 | elif not isinstance(value, (list, tuple)): |
---|
1925 | value = str(value).split('|') |
---|
1926 | self['_checked'] = _value in value and 'checked' or None |
---|
1927 | elif t == 'radio': |
---|
1928 | if str(value) == str(_value): |
---|
1929 | self['_checked'] = 'checked' |
---|
1930 | else: |
---|
1931 | self['_checked'] = None |
---|
1932 | elif t == 'password' and value != DEFAULT_PASSWORD_DISPLAY: |
---|
1933 | self['value'] = '' |
---|
1934 | elif not t == 'submit': |
---|
1935 | if value is None: |
---|
1936 | self['value'] = _value |
---|
1937 | elif not isinstance(value, list): |
---|
1938 | self['_value'] = value |
---|
1939 | |
---|
1940 | def xml(self): |
---|
1941 | name = self.attributes.get('_name', None) |
---|
1942 | if name and hasattr(self, 'errors') \ |
---|
1943 | and self.errors.get(name, None) \ |
---|
1944 | and self['hideerror'] is not True: |
---|
1945 | self['_class'] = (self['_class'] and self['_class'] + ' ' or '') + 'invalidinput' |
---|
1946 | return DIV.xml(self) + DIV( |
---|
1947 | DIV( |
---|
1948 | self.errors[name], _class='error', |
---|
1949 | errors=None, _id='%s__error' % name), |
---|
1950 | _class='error_wrapper').xml() |
---|
1951 | else: |
---|
1952 | if self['_class'] and self['_class'].endswith('invalidinput'): |
---|
1953 | self['_class'] = self['_class'][:-12] |
---|
1954 | if self['_class'] == '': |
---|
1955 | self['_class'] = None |
---|
1956 | return DIV.xml(self) |
---|
1957 | |
---|
1958 | |
---|
1959 | class TEXTAREA(INPUT): |
---|
1960 | |
---|
1961 | """ |
---|
1962 | Examples:: |
---|
1963 | |
---|
1964 | TEXTAREA(_name='sometext', value='blah ' * 100, requires=IS_NOT_EMPTY()) |
---|
1965 | |
---|
1966 | 'blah blah blah ...' will be the content of the textarea field. |
---|
1967 | |
---|
1968 | """ |
---|
1969 | |
---|
1970 | tag = 'textarea' |
---|
1971 | |
---|
1972 | def _postprocessing(self): |
---|
1973 | if '_rows' not in self.attributes: |
---|
1974 | self['_rows'] = 10 |
---|
1975 | if '_cols' not in self.attributes: |
---|
1976 | self['_cols'] = 40 |
---|
1977 | if self['value'] is not None: |
---|
1978 | self.components = [self['value']] |
---|
1979 | elif self.components: |
---|
1980 | self['value'] = self.components[0] |
---|
1981 | |
---|
1982 | |
---|
1983 | class OPTION(DIV): |
---|
1984 | |
---|
1985 | tag = 'option' |
---|
1986 | |
---|
1987 | def _fixup(self): |
---|
1988 | if '_value' not in self.attributes: |
---|
1989 | self.attributes['_value'] = str(self.components[0]) |
---|
1990 | |
---|
1991 | |
---|
1992 | class OBJECT(DIV): |
---|
1993 | |
---|
1994 | tag = 'object' |
---|
1995 | |
---|
1996 | |
---|
1997 | class OPTGROUP(DIV): |
---|
1998 | |
---|
1999 | tag = 'optgroup' |
---|
2000 | |
---|
2001 | def _fixup(self): |
---|
2002 | components = [] |
---|
2003 | for c in self.components: |
---|
2004 | if isinstance(c, OPTION): |
---|
2005 | components.append(c) |
---|
2006 | else: |
---|
2007 | components.append(OPTION(c, _value=str(c))) |
---|
2008 | self.components = components |
---|
2009 | |
---|
2010 | |
---|
2011 | class SELECT(INPUT): |
---|
2012 | """ |
---|
2013 | Examples: |
---|
2014 | |
---|
2015 | >>> from validators import IS_IN_SET |
---|
2016 | >>> SELECT('yes', 'no', _name='selector', value='yes', |
---|
2017 | ... requires=IS_IN_SET(['yes', 'no'])).xml() |
---|
2018 | '<select name=\"selector\"><option selected=\"selected\" value=\"yes\">yes</option><option value=\"no\">no</option></select>' |
---|
2019 | |
---|
2020 | """ |
---|
2021 | |
---|
2022 | tag = 'select' |
---|
2023 | |
---|
2024 | def _fixup(self): |
---|
2025 | components = [] |
---|
2026 | for c in self.components: |
---|
2027 | if isinstance(c, (OPTION, OPTGROUP)): |
---|
2028 | components.append(c) |
---|
2029 | else: |
---|
2030 | components.append(OPTION(c, _value=str(c))) |
---|
2031 | self.components = components |
---|
2032 | |
---|
2033 | def _postprocessing(self): |
---|
2034 | component_list = [] |
---|
2035 | for c in self.components: |
---|
2036 | if isinstance(c, OPTGROUP): |
---|
2037 | component_list.append(c.components) |
---|
2038 | else: |
---|
2039 | component_list.append([c]) |
---|
2040 | options = itertools.chain(*component_list) |
---|
2041 | |
---|
2042 | value = self['value'] |
---|
2043 | if value is not None: |
---|
2044 | if not self['_multiple']: |
---|
2045 | for c in options: # my patch |
---|
2046 | if (value is not None) and (str(c['_value']) == str(value)): |
---|
2047 | c['_selected'] = 'selected' |
---|
2048 | else: |
---|
2049 | c['_selected'] = None |
---|
2050 | else: |
---|
2051 | if isinstance(value, (list, tuple)): |
---|
2052 | values = [str(item) for item in value] |
---|
2053 | else: |
---|
2054 | values = [str(value)] |
---|
2055 | for c in options: # my patch |
---|
2056 | if (value is not None) and (str(c['_value']) in values): |
---|
2057 | c['_selected'] = 'selected' |
---|
2058 | else: |
---|
2059 | c['_selected'] = None |
---|
2060 | |
---|
2061 | |
---|
2062 | class FIELDSET(DIV): |
---|
2063 | |
---|
2064 | tag = 'fieldset' |
---|
2065 | |
---|
2066 | |
---|
2067 | class LEGEND(DIV): |
---|
2068 | |
---|
2069 | tag = 'legend' |
---|
2070 | |
---|
2071 | |
---|
2072 | class FORM(DIV): |
---|
2073 | |
---|
2074 | """ |
---|
2075 | Examples: |
---|
2076 | |
---|
2077 | >>> from validators import IS_NOT_EMPTY |
---|
2078 | >>> form=FORM(INPUT(_name="test", requires=IS_NOT_EMPTY())) |
---|
2079 | >>> form.xml() |
---|
2080 | '<form action=\"#\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"test\" type=\"text\" /></form>' |
---|
2081 | |
---|
2082 | |
---|
2083 | a FORM is container for INPUT, TEXTAREA, SELECT and other helpers |
---|
2084 | |
---|
2085 | form has one important method:: |
---|
2086 | |
---|
2087 | form.accepts(request.vars, session) |
---|
2088 | |
---|
2089 | if form is accepted (and all validators pass) form.vars contains the |
---|
2090 | accepted vars, otherwise form.errors contains the errors. |
---|
2091 | in case of errors the form is modified to present the errors to the user. |
---|
2092 | """ |
---|
2093 | |
---|
2094 | tag = 'form' |
---|
2095 | |
---|
2096 | def __init__(self, *components, **attributes): |
---|
2097 | DIV.__init__(self, *components, **attributes) |
---|
2098 | self.vars = Storage() |
---|
2099 | self.errors = Storage() |
---|
2100 | self.latest = Storage() |
---|
2101 | self.accepted = None # none for not submitted |
---|
2102 | |
---|
2103 | def assert_status(self, status, request_vars): |
---|
2104 | return status |
---|
2105 | |
---|
2106 | def accepts(self, |
---|
2107 | request_vars, |
---|
2108 | session=None, |
---|
2109 | formname='default', |
---|
2110 | keepvalues=False, |
---|
2111 | onvalidation=None, |
---|
2112 | hideerror=False, |
---|
2113 | **kwargs |
---|
2114 | ): |
---|
2115 | """ |
---|
2116 | kwargs is not used but allows to specify the same interface for FORM and SQLFORM |
---|
2117 | """ |
---|
2118 | if request_vars.__class__.__name__ == 'Request': |
---|
2119 | request_vars = request_vars.post_vars |
---|
2120 | self.errors.clear() |
---|
2121 | self.request_vars = Storage() |
---|
2122 | self.request_vars.update(request_vars) |
---|
2123 | self.session = session |
---|
2124 | self.formname = formname |
---|
2125 | self.keepvalues = keepvalues |
---|
2126 | |
---|
2127 | # if this tag is a form and we are in accepting mode (status=True) |
---|
2128 | # check formname and formkey |
---|
2129 | |
---|
2130 | status = True |
---|
2131 | changed = False |
---|
2132 | request_vars = self.request_vars |
---|
2133 | if session is not None: |
---|
2134 | formkey = request_vars._formkey |
---|
2135 | keyname = '_formkey[%s]' % formname |
---|
2136 | formkeys = list(session.get(keyname, [])) |
---|
2137 | # check if user tampering with form and void CSRF |
---|
2138 | if not (formkey and formkeys and formkey in formkeys): |
---|
2139 | status = False |
---|
2140 | else: |
---|
2141 | session[keyname].remove(formkey) |
---|
2142 | if formname != request_vars._formname: |
---|
2143 | status = False |
---|
2144 | if status and session: |
---|
2145 | # check if editing a record that has been modified by the server |
---|
2146 | if hasattr(self, 'record_hash') and self.record_hash != formkey.split(':')[0]: |
---|
2147 | status = False |
---|
2148 | self.record_changed = changed = True |
---|
2149 | status = self._traverse(status, hideerror) |
---|
2150 | status = self.assert_status(status, request_vars) |
---|
2151 | if onvalidation: |
---|
2152 | if isinstance(onvalidation, dict): |
---|
2153 | onsuccess = onvalidation.get('onsuccess', None) |
---|
2154 | onfailure = onvalidation.get('onfailure', None) |
---|
2155 | onchange = onvalidation.get('onchange', None) |
---|
2156 | if [k for k in onvalidation if k not in ('onsuccess', 'onfailure', 'onchange')]: |
---|
2157 | raise RuntimeError('Invalid key in onvalidate dict') |
---|
2158 | if onsuccess and status: |
---|
2159 | call_as_list(onsuccess, self) |
---|
2160 | if onfailure and request_vars and not status: |
---|
2161 | call_as_list(onfailure, self) |
---|
2162 | status = len(self.errors) == 0 |
---|
2163 | if changed: |
---|
2164 | if onchange and self.record_changed and \ |
---|
2165 | self.detect_record_change: |
---|
2166 | call_as_list(onchange, self) |
---|
2167 | elif status: |
---|
2168 | call_as_list(onvalidation, self) |
---|
2169 | if self.errors: |
---|
2170 | status = False |
---|
2171 | if session is not None: |
---|
2172 | if hasattr(self, 'record_hash'): |
---|
2173 | formkey = self.record_hash + ':' + web2py_uuid() |
---|
2174 | else: |
---|
2175 | formkey = web2py_uuid() |
---|
2176 | self.formkey = formkey |
---|
2177 | keyname = '_formkey[%s]' % formname |
---|
2178 | session[keyname] = list(session.get(keyname, []))[-9:] + [formkey] |
---|
2179 | if status and not keepvalues: |
---|
2180 | self._traverse(False, hideerror) |
---|
2181 | self.accepted = status |
---|
2182 | return status |
---|
2183 | |
---|
2184 | def _postprocessing(self): |
---|
2185 | if '_action' not in self.attributes: |
---|
2186 | self['_action'] = '#' |
---|
2187 | if '_method' not in self.attributes: |
---|
2188 | self['_method'] = 'post' |
---|
2189 | if '_enctype' not in self.attributes: |
---|
2190 | self['_enctype'] = 'multipart/form-data' |
---|
2191 | |
---|
2192 | def hidden_fields(self): |
---|
2193 | c = [] |
---|
2194 | attr = self.attributes.get('hidden', {}) |
---|
2195 | if 'hidden' in self.attributes: |
---|
2196 | c = [INPUT(_type='hidden', _name=key, _value=value) for (key, value) in iteritems(attr)] |
---|
2197 | if hasattr(self, 'formkey') and self.formkey: |
---|
2198 | c.append(INPUT(_type='hidden', _name='_formkey', _value=self.formkey)) |
---|
2199 | if hasattr(self, 'formname') and self.formname: |
---|
2200 | c.append(INPUT(_type='hidden', _name='_formname', _value=self.formname)) |
---|
2201 | return DIV(c, _style="display:none;") |
---|
2202 | |
---|
2203 | def xml(self): |
---|
2204 | newform = FORM(*self.components, **self.attributes) |
---|
2205 | hidden_fields = self.hidden_fields() |
---|
2206 | if hidden_fields.components: |
---|
2207 | newform.append(hidden_fields) |
---|
2208 | return DIV.xml(newform) |
---|
2209 | |
---|
2210 | def validate(self, **kwargs): |
---|
2211 | """ |
---|
2212 | This function validates the form, |
---|
2213 | you can use it instead of directly form.accepts. |
---|
2214 | |
---|
2215 | Usage: |
---|
2216 | In controller:: |
---|
2217 | |
---|
2218 | def action(): |
---|
2219 | form=FORM(INPUT(_name=\"test\", requires=IS_NOT_EMPTY())) |
---|
2220 | form.validate() #you can pass some args here - see below |
---|
2221 | return dict(form=form) |
---|
2222 | |
---|
2223 | This can receive a bunch of arguments |
---|
2224 | |
---|
2225 | onsuccess = 'flash' - will show message_onsuccess in response.flash |
---|
2226 | None - will do nothing |
---|
2227 | can be a function (lambda form: pass) |
---|
2228 | onfailure = 'flash' - will show message_onfailure in response.flash |
---|
2229 | None - will do nothing |
---|
2230 | can be a function (lambda form: pass) |
---|
2231 | onchange = 'flash' - will show message_onchange in response.flash |
---|
2232 | None - will do nothing |
---|
2233 | can be a function (lambda form: pass) |
---|
2234 | |
---|
2235 | message_onsuccess |
---|
2236 | message_onfailure |
---|
2237 | message_onchange |
---|
2238 | next = where to redirect in case of success |
---|
2239 | any other kwargs will be passed for form.accepts(...) |
---|
2240 | """ |
---|
2241 | from gluon.globals import current |
---|
2242 | from gluon.http import redirect |
---|
2243 | kwargs['request_vars'] = kwargs.get( |
---|
2244 | 'request_vars', current.request.post_vars) |
---|
2245 | kwargs['session'] = kwargs.get('session', current.session) |
---|
2246 | kwargs['dbio'] = kwargs.get('dbio', False) # necessary for SQLHTML forms |
---|
2247 | |
---|
2248 | onsuccess = kwargs.get('onsuccess', 'flash') |
---|
2249 | onfailure = kwargs.get('onfailure', 'flash') |
---|
2250 | onchange = kwargs.get('onchange', 'flash') |
---|
2251 | message_onsuccess = kwargs.get('message_onsuccess', |
---|
2252 | current.T("Success!")) |
---|
2253 | message_onfailure = kwargs.get('message_onfailure', |
---|
2254 | current.T("Errors in form, please check it out.")) |
---|
2255 | message_onchange = kwargs.get('message_onchange', |
---|
2256 | current.T("Form consecutive submissions not allowed. " + |
---|
2257 | "Try re-submitting or refreshing the form page.")) |
---|
2258 | next = kwargs.get('next', None) |
---|
2259 | for key in ('message_onsuccess', 'message_onfailure', 'onsuccess', |
---|
2260 | 'onfailure', 'next', 'message_onchange', 'onchange'): |
---|
2261 | if key in kwargs: |
---|
2262 | del kwargs[key] |
---|
2263 | |
---|
2264 | if self.accepts(**kwargs): |
---|
2265 | if onsuccess == 'flash': |
---|
2266 | if next: |
---|
2267 | current.session.flash = message_onsuccess |
---|
2268 | else: |
---|
2269 | current.response.flash = message_onsuccess |
---|
2270 | elif callable(onsuccess): |
---|
2271 | onsuccess(self) |
---|
2272 | if next: |
---|
2273 | if self.vars: |
---|
2274 | for key, value in iteritems(self.vars): |
---|
2275 | next = next.replace('[%s]' % key, |
---|
2276 | urllib_quote(str(value))) |
---|
2277 | if not next.startswith('/'): |
---|
2278 | next = URL(next) |
---|
2279 | redirect(next) |
---|
2280 | return True |
---|
2281 | elif self.errors: |
---|
2282 | if onfailure == 'flash': |
---|
2283 | current.response.flash = message_onfailure |
---|
2284 | elif callable(onfailure): |
---|
2285 | onfailure(self) |
---|
2286 | return False |
---|
2287 | elif hasattr(self, "record_changed"): |
---|
2288 | if self.record_changed and self.detect_record_change: |
---|
2289 | if onchange == 'flash': |
---|
2290 | current.response.flash = message_onchange |
---|
2291 | elif callable(onchange): |
---|
2292 | onchange(self) |
---|
2293 | return False |
---|
2294 | |
---|
2295 | def process(self, **kwargs): |
---|
2296 | """ |
---|
2297 | Perform the .validate() method but returns the form |
---|
2298 | |
---|
2299 | Usage in controllers:: |
---|
2300 | |
---|
2301 | # directly on return |
---|
2302 | def action(): |
---|
2303 | #some code here |
---|
2304 | return dict(form=FORM(...).process(...)) |
---|
2305 | |
---|
2306 | You can use it with FORM, SQLFORM or FORM based plugins:: |
---|
2307 | |
---|
2308 | # response.flash messages |
---|
2309 | def action(): |
---|
2310 | form = SQLFORM(db.table).process(message_onsuccess='Sucess!') |
---|
2311 | return dict(form=form) |
---|
2312 | |
---|
2313 | # callback function |
---|
2314 | # callback receives True or False as first arg, and a list of args. |
---|
2315 | def my_callback(status, msg): |
---|
2316 | response.flash = "Success! "+msg if status else "Errors occured" |
---|
2317 | |
---|
2318 | # after argument can be 'flash' to response.flash messages |
---|
2319 | # or a function name to use as callback or None to do nothing. |
---|
2320 | def action(): |
---|
2321 | return dict(form=SQLFORM(db.table).process(onsuccess=my_callback) |
---|
2322 | |
---|
2323 | |
---|
2324 | """ |
---|
2325 | kwargs['dbio'] = kwargs.get('dbio', True) # necessary for SQLHTML forms |
---|
2326 | self.validate(**kwargs) |
---|
2327 | return self |
---|
2328 | |
---|
2329 | REDIRECT_JS = "window.location='%s';return false" |
---|
2330 | |
---|
2331 | def add_button(self, value, url, _class=None): |
---|
2332 | submit = self.element(_type='submit') |
---|
2333 | _class = "%s w2p-form-button" % _class if _class else "w2p-form-button" |
---|
2334 | submit.parent.append( |
---|
2335 | TAG['button'](value, _class=_class, |
---|
2336 | _onclick=url if url.startswith('javascript:') else |
---|
2337 | self.REDIRECT_JS % url)) |
---|
2338 | |
---|
2339 | @staticmethod |
---|
2340 | def confirm(text='OK', buttons=None, hidden=None): |
---|
2341 | if not buttons: |
---|
2342 | buttons = {} |
---|
2343 | if not hidden: |
---|
2344 | hidden = {} |
---|
2345 | inputs = [INPUT(_type='button', |
---|
2346 | _value=name, |
---|
2347 | _onclick=FORM.REDIRECT_JS % link) |
---|
2348 | for name, link in iteritems(buttons)] |
---|
2349 | inputs += [INPUT(_type='hidden', |
---|
2350 | _name=name, |
---|
2351 | _value=value) |
---|
2352 | for name, value in iteritems(hidden)] |
---|
2353 | form = FORM(INPUT(_type='submit', _value=text), *inputs) |
---|
2354 | form.process() |
---|
2355 | return form |
---|
2356 | |
---|
2357 | def as_dict(self, flat=False, sanitize=True): |
---|
2358 | """EXPERIMENTAL |
---|
2359 | |
---|
2360 | Sanitize is naive. It should catch any unsafe value |
---|
2361 | for client retrieval. |
---|
2362 | """ |
---|
2363 | SERIALIZABLE = (int, float, bool, basestring, long, |
---|
2364 | set, list, dict, tuple, Storage, type(None)) |
---|
2365 | UNSAFE = ("PASSWORD", "CRYPT") |
---|
2366 | d = self.__dict__ |
---|
2367 | |
---|
2368 | def sanitizer(obj): |
---|
2369 | if isinstance(obj, dict): |
---|
2370 | for k in obj.keys(): |
---|
2371 | if any([unsafe in str(k).upper() for unsafe in UNSAFE]): |
---|
2372 | # erease unsafe pair |
---|
2373 | obj.pop(k) |
---|
2374 | else: |
---|
2375 | # not implemented |
---|
2376 | pass |
---|
2377 | return obj |
---|
2378 | |
---|
2379 | def flatten(obj): |
---|
2380 | if isinstance(obj, (dict, Storage)): |
---|
2381 | newobj = obj.copy() |
---|
2382 | else: |
---|
2383 | newobj = obj |
---|
2384 | if sanitize: |
---|
2385 | newobj = sanitizer(newobj) |
---|
2386 | if flat: |
---|
2387 | if type(obj) in SERIALIZABLE: |
---|
2388 | if isinstance(newobj, (dict, Storage)): |
---|
2389 | for k in newobj: |
---|
2390 | newk = flatten(k) |
---|
2391 | newobj[newk] = flatten(newobj[k]) |
---|
2392 | if k != newk: |
---|
2393 | newobj.pop(k) |
---|
2394 | return newobj |
---|
2395 | elif isinstance(newobj, (list, tuple, set)): |
---|
2396 | return [flatten(item) for item in newobj] |
---|
2397 | else: |
---|
2398 | return newobj |
---|
2399 | else: |
---|
2400 | return str(newobj) |
---|
2401 | else: |
---|
2402 | return newobj |
---|
2403 | return flatten(d) |
---|
2404 | |
---|
2405 | def as_json(self, sanitize=True): |
---|
2406 | d = self.as_dict(flat=True, sanitize=sanitize) |
---|
2407 | from gluon.serializers import json |
---|
2408 | return json(d) |
---|
2409 | |
---|
2410 | def as_yaml(self, sanitize=True): |
---|
2411 | d = self.as_dict(flat=True, sanitize=sanitize) |
---|
2412 | from gluon.serializers import yaml |
---|
2413 | return yaml(d) |
---|
2414 | |
---|
2415 | def as_xml(self, sanitize=True): |
---|
2416 | d = self.as_dict(flat=True, sanitize=sanitize) |
---|
2417 | from gluon.serializers import xml |
---|
2418 | return xml(d) |
---|
2419 | |
---|
2420 | |
---|
2421 | class BEAUTIFY(DIV): |
---|
2422 | """ |
---|
2423 | Turns any list, dictionary, etc into decent looking html. |
---|
2424 | |
---|
2425 | Two special attributes are |
---|
2426 | |
---|
2427 | - sorted: a function that takes the dict and returned sorted keys |
---|
2428 | - keyfilter: a function that takes a key and returns its representation or |
---|
2429 | None if the key is to be skipped. |
---|
2430 | By default key[:1]=='_' is skipped. |
---|
2431 | |
---|
2432 | Examples: |
---|
2433 | |
---|
2434 | >>> BEAUTIFY(['a', 'b', {'hello': 'world'}]).xml() |
---|
2435 | '<div><table><tr><td><div>a</div></td></tr><tr><td><div>b</div></td></tr><tr><td><div><table><tr><td style="font-weight:bold;vertical-align:top;">hello</td><td style="vertical-align:top;">:</td><td><div>world</div></td></tr></table></div></td></tr></table></div>' |
---|
2436 | |
---|
2437 | """ |
---|
2438 | |
---|
2439 | tag = 'div' |
---|
2440 | |
---|
2441 | @staticmethod |
---|
2442 | def no_underscore(key): |
---|
2443 | if key[:1] == '_': |
---|
2444 | return None |
---|
2445 | return key |
---|
2446 | |
---|
2447 | def __init__(self, component, **attributes): |
---|
2448 | self.components = [component] |
---|
2449 | self.attributes = attributes |
---|
2450 | sorter = attributes.get('sorted', sorted) |
---|
2451 | keyfilter = attributes.get('keyfilter', BEAUTIFY.no_underscore) |
---|
2452 | components = [] |
---|
2453 | attributes = copy.copy(self.attributes) |
---|
2454 | level = attributes['level'] = attributes.get('level', 6) - 1 |
---|
2455 | if '_class' in attributes: |
---|
2456 | attributes['_class'] += 'i' |
---|
2457 | if level == 0: |
---|
2458 | return |
---|
2459 | for c in self.components: |
---|
2460 | if hasattr(c, 'value') and not callable(c.value) and not isinstance(c, cgi.FieldStorage): |
---|
2461 | if c.value: |
---|
2462 | components.append(c.value) |
---|
2463 | if hasattr(c, 'xml') and callable(c.xml): |
---|
2464 | components.append(c) |
---|
2465 | continue |
---|
2466 | elif hasattr(c, 'keys') and callable(c.keys): |
---|
2467 | rows = [] |
---|
2468 | try: |
---|
2469 | keys = (sorter and sorter(c)) or c |
---|
2470 | for key in keys: |
---|
2471 | if isinstance(key, (str, unicodeT)) and keyfilter: |
---|
2472 | filtered_key = keyfilter(key) |
---|
2473 | else: |
---|
2474 | filtered_key = str(key) |
---|
2475 | if filtered_key is None: |
---|
2476 | continue |
---|
2477 | value = c[key] |
---|
2478 | if isinstance(value, types.LambdaType): |
---|
2479 | continue |
---|
2480 | rows.append( |
---|
2481 | TR( |
---|
2482 | TD(filtered_key, _style='font-weight:bold;vertical-align:top;'), |
---|
2483 | TD(':', _style='vertical-align:top;'), |
---|
2484 | TD(BEAUTIFY(value, **attributes)))) |
---|
2485 | components.append(TABLE(*rows, **attributes)) |
---|
2486 | continue |
---|
2487 | except: |
---|
2488 | pass |
---|
2489 | if isinstance(c, str): |
---|
2490 | components.append(str(c)) |
---|
2491 | elif isinstance(c, unicodeT): |
---|
2492 | components.append(c.encode('utf8')) |
---|
2493 | elif isinstance(c, (list, tuple)): |
---|
2494 | items = [TR(TD(BEAUTIFY(item, **attributes))) |
---|
2495 | for item in c] |
---|
2496 | components.append(TABLE(*items, **attributes)) |
---|
2497 | elif isinstance(c, cgi.FieldStorage): |
---|
2498 | components.append('FieldStorage object') |
---|
2499 | else: |
---|
2500 | components.append(repr(c)) |
---|
2501 | self.components = components |
---|
2502 | |
---|
2503 | |
---|
2504 | class MENU(DIV): |
---|
2505 | """ |
---|
2506 | Used to build menus |
---|
2507 | |
---|
2508 | Args: |
---|
2509 | _class: defaults to 'web2py-menu web2py-menu-vertical' |
---|
2510 | ul_class: defaults to 'web2py-menu-vertical' |
---|
2511 | li_class: defaults to 'web2py-menu-expand' |
---|
2512 | li_first: defaults to 'web2py-menu-first' |
---|
2513 | li_last: defaults to 'web2py-menu-last' |
---|
2514 | |
---|
2515 | Use like:: |
---|
2516 | |
---|
2517 | menu = MENU([['name', False, URL(...), [submenu]], ...]) |
---|
2518 | {{=menu}} |
---|
2519 | |
---|
2520 | """ |
---|
2521 | |
---|
2522 | tag = 'ul' |
---|
2523 | |
---|
2524 | def __init__(self, data, **args): |
---|
2525 | self.data = data |
---|
2526 | self.attributes = args |
---|
2527 | self.components = [] |
---|
2528 | if '_class' not in self.attributes: |
---|
2529 | self['_class'] = 'web2py-menu web2py-menu-vertical' |
---|
2530 | if 'ul_class' not in self.attributes: |
---|
2531 | self['ul_class'] = 'web2py-menu-vertical' |
---|
2532 | if 'li_class' not in self.attributes: |
---|
2533 | self['li_class'] = 'web2py-menu-expand' |
---|
2534 | if 'li_first' not in self.attributes: |
---|
2535 | self['li_first'] = 'web2py-menu-first' |
---|
2536 | if 'li_last' not in self.attributes: |
---|
2537 | self['li_last'] = 'web2py-menu-last' |
---|
2538 | if 'li_active' not in self.attributes: |
---|
2539 | self['li_active'] = 'web2py-menu-active' |
---|
2540 | if 'mobile' not in self.attributes: |
---|
2541 | self['mobile'] = False |
---|
2542 | |
---|
2543 | def serialize(self, data, level=0): |
---|
2544 | if level == 0: |
---|
2545 | ul = UL(**self.attributes) |
---|
2546 | else: |
---|
2547 | ul = UL(_class=self['ul_class']) |
---|
2548 | for item in data: |
---|
2549 | if isinstance(item, LI): |
---|
2550 | ul.append(item) |
---|
2551 | else: |
---|
2552 | (name, active, link) = item[:3] |
---|
2553 | if isinstance(link, DIV): |
---|
2554 | li = LI(link) |
---|
2555 | elif 'no_link_url' in self.attributes and self['no_link_url'] == link: |
---|
2556 | li = LI(DIV(name)) |
---|
2557 | elif isinstance(link, dict): |
---|
2558 | li = LI(A(name, **link)) |
---|
2559 | elif link: |
---|
2560 | li = LI(A(name, _href=link)) |
---|
2561 | elif not link and isinstance(name, A): |
---|
2562 | li = LI(name) |
---|
2563 | else: |
---|
2564 | li = LI(A(name, _href='#', |
---|
2565 | _onclick='javascript:void(0);return false;')) |
---|
2566 | if level == 0 and item == data[0]: |
---|
2567 | li['_class'] = self['li_first'] |
---|
2568 | elif level == 0 and item == data[-1]: |
---|
2569 | li['_class'] = self['li_last'] |
---|
2570 | if len(item) > 3 and item[3]: |
---|
2571 | li['_class'] = self['li_class'] |
---|
2572 | li.append(self.serialize(item[3], level + 1)) |
---|
2573 | if active or ('active_url' in self.attributes and self['active_url'] == link): |
---|
2574 | if li['_class']: |
---|
2575 | li['_class'] = li['_class'] + ' ' + self['li_active'] |
---|
2576 | else: |
---|
2577 | li['_class'] = self['li_active'] |
---|
2578 | if len(item) <= 4 or item[4] is True: |
---|
2579 | ul.append(li) |
---|
2580 | return ul |
---|
2581 | |
---|
2582 | def serialize_mobile(self, data, select=None, prefix=''): |
---|
2583 | if not select: |
---|
2584 | select = SELECT(**self.attributes) |
---|
2585 | custom_items = [] |
---|
2586 | for item in data: |
---|
2587 | # Custom item aren't serialized as mobile |
---|
2588 | if len(item) >= 3 and (not item[0]) or (isinstance(item[0], DIV) and not (item[2])): |
---|
2589 | # ex: ('', False, A('title', _href=URL(...), _title="title")) |
---|
2590 | # ex: (A('title', _href=URL(...), _title="title"), False, None) |
---|
2591 | custom_items.append(item) |
---|
2592 | elif len(item) <= 4 or item[4] is True: |
---|
2593 | select.append(OPTION(CAT(prefix, item[0]), |
---|
2594 | _value=item[2], _selected=item[1])) |
---|
2595 | if len(item) > 3 and len(item[3]): |
---|
2596 | self.serialize_mobile( |
---|
2597 | item[3], select, prefix=CAT(prefix, item[0], '/')) |
---|
2598 | select['_onchange'] = 'window.location=this.value' |
---|
2599 | # avoid to wrap the select if no custom items are present |
---|
2600 | html = DIV(select, self.serialize(custom_items)) if len(custom_items) else select |
---|
2601 | return html |
---|
2602 | |
---|
2603 | def xml(self): |
---|
2604 | if self['mobile']: |
---|
2605 | return self.serialize_mobile(self.data, 0).xml() |
---|
2606 | else: |
---|
2607 | return self.serialize(self.data, 0).xml() |
---|
2608 | |
---|
2609 | |
---|
2610 | def embed64(filename=None, |
---|
2611 | file=None, |
---|
2612 | data=None, |
---|
2613 | extension='image/gif' |
---|
2614 | ): |
---|
2615 | """ |
---|
2616 | helper to encode the provided (binary) data into base64. |
---|
2617 | |
---|
2618 | Args: |
---|
2619 | filename: if provided, opens and reads this file in 'rb' mode |
---|
2620 | file: if provided, reads this file |
---|
2621 | data: if provided, uses the provided data |
---|
2622 | """ |
---|
2623 | |
---|
2624 | if filename and os.path.exists(file): |
---|
2625 | fp = open(filename, 'rb') |
---|
2626 | data = fp.read() |
---|
2627 | fp.close() |
---|
2628 | data = base64.b64encode(data) |
---|
2629 | return 'data:%s;base64,%s' % (extension, data) |
---|
2630 | |
---|
2631 | |
---|
2632 | # TODO: Check if this test() is still relevant now that we have gluon/tests/test_html.py |
---|
2633 | def test(): |
---|
2634 | """ |
---|
2635 | Example: |
---|
2636 | |
---|
2637 | >>> from validators import * |
---|
2638 | >>> print(DIV(A('click me', _href=URL(a='a', c='b', f='c')), BR(), HR(), DIV(SPAN("World"), _class='unknown')).xml()) |
---|
2639 | <div><a href="/a/b/c">click me</a><br /><hr /><div class=\"unknown\"><span>World</span></div></div> |
---|
2640 | >>> print(DIV(UL("doc","cat","mouse")).xml()) |
---|
2641 | <div><ul><li>doc</li><li>cat</li><li>mouse</li></ul></div> |
---|
2642 | >>> print(DIV(UL("doc", LI("cat", _class='feline'), 18)).xml()) |
---|
2643 | <div><ul><li>doc</li><li class=\"feline\">cat</li><li>18</li></ul></div> |
---|
2644 | >>> print(TABLE(['a', 'b', 'c'], TR('d', 'e', 'f'), TR(TD(1), TD(2), TD(3))).xml()) |
---|
2645 | <table><tr><td>a</td><td>b</td><td>c</td></tr><tr><td>d</td><td>e</td><td>f</td></tr><tr><td>1</td><td>2</td><td>3</td></tr></table> |
---|
2646 | >>> form=FORM(INPUT(_type='text', _name='myvar', requires=IS_EXPR('int(value)<10'))) |
---|
2647 | >>> print(form.xml()) |
---|
2648 | <form action=\"#\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"myvar\" type=\"text\" /></form> |
---|
2649 | >>> print(form.accepts({'myvar':'34'}, formname=None)) |
---|
2650 | False |
---|
2651 | >>> print(form.xml()) |
---|
2652 | <form action="#" enctype="multipart/form-data" method="post"><input class="invalidinput" name="myvar" type="text" value="34" /><div class="error_wrapper"><div class="error" id="myvar__error">Invalid expression</div></div></form> |
---|
2653 | >>> print(form.accepts({'myvar':'4'}, formname=None, keepvalues=True)) |
---|
2654 | True |
---|
2655 | >>> print(form.xml()) |
---|
2656 | <form action=\"#\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"myvar\" type=\"text\" value=\"4\" /></form> |
---|
2657 | >>> form=FORM(SELECT('cat', 'dog', _name='myvar')) |
---|
2658 | >>> print(form.accepts({'myvar':'dog'}, formname=None, keepvalues=True)) |
---|
2659 | True |
---|
2660 | >>> print(form.xml()) |
---|
2661 | <form action=\"#\" enctype=\"multipart/form-data\" method=\"post\"><select name=\"myvar\"><option value=\"cat\">cat</option><option selected=\"selected\" value=\"dog\">dog</option></select></form> |
---|
2662 | >>> form=FORM(INPUT(_type='text', _name='myvar', requires=IS_MATCH('^\w+$', 'only alphanumeric!'))) |
---|
2663 | >>> print(form.accepts({'myvar':'as df'}, formname=None)) |
---|
2664 | False |
---|
2665 | >>> print(form.xml()) |
---|
2666 | <form action="#" enctype="multipart/form-data" method="post"><input class="invalidinput" name="myvar" type="text" value="as df" /><div class="error_wrapper"><div class="error" id="myvar__error">only alphanumeric!</div></div></form> |
---|
2667 | >>> session={} |
---|
2668 | >>> form=FORM(INPUT(value="Hello World", _name="var", requires=IS_MATCH('^\w+$'))) |
---|
2669 | >>> isinstance(form.as_dict(), dict) |
---|
2670 | True |
---|
2671 | >>> "vars" in form.as_dict(flat=True) |
---|
2672 | True |
---|
2673 | >>> isinstance(form.as_json(), basestring) and len(form.as_json(sanitize=False)) > 0 |
---|
2674 | True |
---|
2675 | >>> if form.accepts({}, session,formname=None): print('passed') |
---|
2676 | >>> if form.accepts({'var':'test ', '_formkey': session['_formkey[None]']}, session, formname=None): print('passed') |
---|
2677 | """ |
---|
2678 | pass |
---|
2679 | |
---|
2680 | |
---|
2681 | class web2pyHTMLParser(HTMLParser): |
---|
2682 | """ |
---|
2683 | obj = web2pyHTMLParser(text) parses and html/xml text into web2py helpers. |
---|
2684 | obj.tree contains the root of the tree, and tree can be manipulated |
---|
2685 | """ |
---|
2686 | |
---|
2687 | def __init__(self, text, closed=('input', 'link')): |
---|
2688 | HTMLParser.__init__(self) |
---|
2689 | self.tree = self.parent = TAG['']() |
---|
2690 | self.closed = closed |
---|
2691 | self.last = None |
---|
2692 | self.feed(text) |
---|
2693 | |
---|
2694 | def handle_starttag(self, tagname, attrs): |
---|
2695 | if tagname in self.closed: |
---|
2696 | tagname += '/' |
---|
2697 | tag = TAG[tagname]() |
---|
2698 | for key, value in attrs: |
---|
2699 | tag['_' + key] = value |
---|
2700 | tag.parent = self.parent |
---|
2701 | self.parent.append(tag) |
---|
2702 | if not tag.tag.endswith('/'): |
---|
2703 | self.parent = tag |
---|
2704 | else: |
---|
2705 | self.last = tag.tag[:-1] |
---|
2706 | |
---|
2707 | def handle_data(self, data): |
---|
2708 | if not isinstance(data, unicodeT): |
---|
2709 | try: |
---|
2710 | data = data.decode('utf8') |
---|
2711 | except: |
---|
2712 | data = data.decode('latin1') |
---|
2713 | self.parent.append(data.encode('utf8', 'xmlcharref')) |
---|
2714 | |
---|
2715 | def handle_charref(self, name): |
---|
2716 | if name.startswith('x'): |
---|
2717 | self.parent.append(unichr(int(name[1:], 16)).encode('utf8')) |
---|
2718 | else: |
---|
2719 | self.parent.append(unichr(int(name)).encode('utf8')) |
---|
2720 | |
---|
2721 | def handle_entityref(self, name): |
---|
2722 | self.parent.append(entitydefs[name]) |
---|
2723 | |
---|
2724 | def handle_endtag(self, tagname): |
---|
2725 | # this deals with unbalanced tags |
---|
2726 | if tagname == self.last: |
---|
2727 | return |
---|
2728 | while True: |
---|
2729 | try: |
---|
2730 | parent_tagname = self.parent.tag |
---|
2731 | self.parent = self.parent.parent |
---|
2732 | except: |
---|
2733 | raise RuntimeError("unable to balance tag %s" % tagname) |
---|
2734 | if parent_tagname[:len(tagname)] == tagname: |
---|
2735 | break |
---|
2736 | |
---|
2737 | |
---|
2738 | def markdown_serializer(text, tag=None, attr=None): |
---|
2739 | attr = attr or {} |
---|
2740 | if tag is None: |
---|
2741 | return re.sub('\s+', ' ', text) |
---|
2742 | if tag == 'br': |
---|
2743 | return '\n\n' |
---|
2744 | if tag == 'h1': |
---|
2745 | return '#' + text + '\n\n' |
---|
2746 | if tag == 'h2': |
---|
2747 | return '#' * 2 + text + '\n\n' |
---|
2748 | if tag == 'h3': |
---|
2749 | return '#' * 3 + text + '\n\n' |
---|
2750 | if tag == 'h4': |
---|
2751 | return '#' * 4 + text + '\n\n' |
---|
2752 | if tag == 'p': |
---|
2753 | return text + '\n\n' |
---|
2754 | if tag == 'b' or tag == 'strong': |
---|
2755 | return '**%s**' % text |
---|
2756 | if tag == 'em' or tag == 'i': |
---|
2757 | return '*%s*' % text |
---|
2758 | if tag == 'tt' or tag == 'code': |
---|
2759 | return '`%s`' % text |
---|
2760 | if tag == 'a': |
---|
2761 | return '[%s](%s)' % (text, attr.get('_href', '')) |
---|
2762 | if tag == 'img': |
---|
2763 | return '' % (attr.get('_alt', ''), attr.get('_src', '')) |
---|
2764 | return text |
---|
2765 | |
---|
2766 | |
---|
2767 | def markmin_serializer(text, tag=None, attr=None): |
---|
2768 | attr = attr or {} |
---|
2769 | # if tag is None: return re.sub('\s+',' ',text) |
---|
2770 | if tag == 'br': |
---|
2771 | return '\n\n' |
---|
2772 | if tag == 'h1': |
---|
2773 | return '# ' + text + '\n\n' |
---|
2774 | if tag == 'h2': |
---|
2775 | return '#' * 2 + ' ' + text + '\n\n' |
---|
2776 | if tag == 'h3': |
---|
2777 | return '#' * 3 + ' ' + text + '\n\n' |
---|
2778 | if tag == 'h4': |
---|
2779 | return '#' * 4 + ' ' + text + '\n\n' |
---|
2780 | if tag == 'p': |
---|
2781 | return text + '\n\n' |
---|
2782 | if tag == 'li': |
---|
2783 | return '\n- ' + text.replace('\n', ' ') |
---|
2784 | if tag == 'tr': |
---|
2785 | return text[3:].replace('\n', ' ') + '\n' |
---|
2786 | if tag in ['table', 'blockquote']: |
---|
2787 | return '\n-----\n' + text + '\n------\n' |
---|
2788 | if tag in ['td', 'th']: |
---|
2789 | return ' | ' + text |
---|
2790 | if tag in ['b', 'strong', 'label']: |
---|
2791 | return '**%s**' % text |
---|
2792 | if tag in ['em', 'i']: |
---|
2793 | return "''%s''" % text |
---|
2794 | if tag in ['tt']: |
---|
2795 | return '``%s``' % text.strip() |
---|
2796 | if tag in ['code']: |
---|
2797 | return '``\n%s``' % text |
---|
2798 | if tag == 'a': |
---|
2799 | return '[[%s %s]]' % (text, attr.get('_href', '')) |
---|
2800 | if tag == 'img': |
---|
2801 | return '[[%s %s left]]' % (attr.get('_alt', 'no title'), attr.get('_src', '')) |
---|
2802 | return text |
---|
2803 | |
---|
2804 | |
---|
2805 | class MARKMIN(XmlComponent): |
---|
2806 | """ |
---|
2807 | For documentation: http://web2py.com/examples/static/markmin.html |
---|
2808 | """ |
---|
2809 | def __init__(self, |
---|
2810 | text, extra=None, allowed=None, sep='p', |
---|
2811 | url=None, environment=None, latex='google', |
---|
2812 | autolinks='default', |
---|
2813 | protolinks='default', |
---|
2814 | class_prefix='', |
---|
2815 | id_prefix='markmin_', |
---|
2816 | **kwargs): |
---|
2817 | self.text = to_bytes(text) |
---|
2818 | self.extra = extra or {} |
---|
2819 | self.allowed = allowed or {} |
---|
2820 | self.sep = sep |
---|
2821 | self.url = URL if url is True else url |
---|
2822 | self.environment = environment |
---|
2823 | self.latex = latex |
---|
2824 | self.autolinks = autolinks |
---|
2825 | self.protolinks = protolinks |
---|
2826 | self.class_prefix = class_prefix |
---|
2827 | self.id_prefix = id_prefix |
---|
2828 | self.kwargs = kwargs |
---|
2829 | |
---|
2830 | def flatten(self): |
---|
2831 | return self.text |
---|
2832 | |
---|
2833 | def xml(self): |
---|
2834 | from gluon.contrib.markmin.markmin2html import render |
---|
2835 | html = render(self.text, extra=self.extra, |
---|
2836 | allowed=self.allowed, sep=self.sep, latex=self.latex, |
---|
2837 | URL=self.url, environment=self.environment, |
---|
2838 | autolinks=self.autolinks, protolinks=self.protolinks, |
---|
2839 | class_prefix=self.class_prefix, id_prefix=self.id_prefix) |
---|
2840 | return to_bytes(html) if not self.kwargs else to_bytes(DIV(XML(html), **self.kwargs).xml()) |
---|
2841 | |
---|
2842 | def __str__(self): |
---|
2843 | # In PY3 __str__ cannot return bytes (TypeError: __str__ returned non-string (type bytes)) |
---|
2844 | return to_native(self.xml()) |
---|
2845 | |
---|
2846 | |
---|
2847 | def ASSIGNJS(**kargs): |
---|
2848 | """ |
---|
2849 | Example: |
---|
2850 | ASSIGNJS(var1='1', var2='2') will return the following javascript variables assignations : |
---|
2851 | |
---|
2852 | var var1 = "1"; |
---|
2853 | var var2 = "2"; |
---|
2854 | |
---|
2855 | Args: |
---|
2856 | **kargs: Any keywords arguments and assigned values. |
---|
2857 | |
---|
2858 | Returns: |
---|
2859 | Javascript vars assignations for the key/value passed. |
---|
2860 | |
---|
2861 | """ |
---|
2862 | from gluon.serializers import json |
---|
2863 | s = "" |
---|
2864 | for key, value in kargs.items(): |
---|
2865 | s += 'var %s = %s;\n' % (key, json(value)) |
---|
2866 | return XML(s) |
---|
2867 | |
---|
2868 | |
---|
2869 | if __name__ == '__main__': |
---|
2870 | import doctest |
---|
2871 | doctest.testmod() |
---|