[IMP] tools.safe_eval_qweb: methods intended to provide more restricted alternatives...
[odoo/odoo.git] / openerp / tools / qweb.py
1 import logging
2 import re
3
4 import werkzeug.utils
5 from openerp.tools.safe_eval_qweb import safe_eval_qweb as eval, UndefinedError, SecurityError
6
7 import xml   # FIXME use lxml
8 import traceback
9 from openerp.osv import osv, orm
10
11 _logger = logging.getLogger(__name__)
12
13
14 class QWebXml(object):
15     """QWeb Xml templating engine
16
17     The templating engine use a very simple syntax, "magic" xml attributes, to
18     produce any kind of texutal output (even non-xml).
19
20     QWebXml:
21         the template engine core implements the basic magic attributes:
22
23         t-att t-raw t-esc t-if t-foreach t-set t-call t-trim
24
25
26     - loader: function that return a template
27
28
29     """
30     def __init__(self, loader=None, undefined_handler=None):
31         self.loader = loader
32         self.undefined_handler = undefined_handler
33         self.node = xml.dom.Node
34         self._t = {}
35         self._render_tag = {}
36         self._format_regex = re.compile('(#\{(.*?)\})|(\{\{(.*?)\}\})')
37         self._void_elements = set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
38                                   'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
39         prefix = 'render_tag_'
40         for i in [j for j in dir(self) if j.startswith(prefix)]:
41             name = i[len(prefix):].replace('_', '-')
42             self._render_tag[name] = getattr(self.__class__, i)
43
44         self._render_att = {}
45         prefix = 'render_att_'
46         for i in [j for j in dir(self) if j.startswith(prefix)]:
47             name = i[len(prefix):].replace('_', '-')
48             self._render_att[name] = getattr(self.__class__, i)
49
50     def register_tag(self, tag, func):
51         self._render_tag[tag] = func
52
53     def add_template(self, x):
54         if hasattr(x, 'documentElement'):
55             dom = x
56         elif x.startswith("<?xml"):
57             dom = xml.dom.minidom.parseString(x)
58         else:
59             dom = xml.dom.minidom.parse(x)
60         for n in dom.documentElement.childNodes:
61             if n.nodeType == 1 and n.getAttribute('t-name'):
62                 self._t[str(n.getAttribute("t-name"))] = n
63
64     def get_template(self, name):
65         if name in self._t:
66             return self._t[name]
67         elif self.loader:
68             xml = self.loader(name)
69             self.add_template(xml)
70             if name in self._t:
71                 return self._t[name]
72         raise KeyError('qweb: template "%s" not found' % name)
73
74     def eval(self, expr, v):
75         try:
76             return eval(expr, None, v)
77         except (osv.except_osv, orm.except_orm), err:
78             raise orm.except_orm("QWeb Error", "Invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), err[1]))
79         except (UndefinedError, SecurityError), err:
80             if self.undefined_handler:
81                 return self.undefined_handler(expr, v)
82             else:
83                 raise SyntaxError(err.message)
84         except Exception:
85             raise SyntaxError("QWeb: invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), traceback.format_exc()))
86
87     def eval_object(self, expr, v):
88         return self.eval(expr, v)
89
90     def eval_str(self, expr, v):
91         if expr == "0":
92             return v.get(0, '')
93         val = self.eval(expr, v)
94
95         if isinstance(val, unicode):
96             return val.encode("utf8")
97         return str(val)
98
99     def eval_format(self, expr, v):
100         use_native = True
101         for m in self._format_regex.finditer(expr):
102             use_native = False
103             expr = expr.replace(m.group(), self.eval_str(m.groups()[1] or m.groups()[3], v))
104
105         if not use_native:
106             return expr
107         else:
108             try:
109                 return str(expr % v)
110             except:
111                 raise Exception("QWeb: format error '%s' " % expr)
112
113     def eval_bool(self, expr, v):
114         val = self.eval(expr, v)
115         if val:
116             return 1
117         else:
118             return 0
119
120     def render(self, tname, v=None, out=None):
121         if v is None:
122             v = {}
123         v['__template__'] = tname
124         stack = v.get('__stack__', [])
125         if stack:
126             v['__caller__'] = stack[-1]
127         stack.append(tname)
128         v['__stack__'] = stack
129         return self.render_node(self.get_template(tname), v)
130
131     def render_node(self, e, v):
132         r = ""
133         if e.nodeType == self.node.TEXT_NODE or e.nodeType == self.node.CDATA_SECTION_NODE:
134             r = e.data.encode("utf8")
135         elif e.nodeType == self.node.ELEMENT_NODE:
136             g_att = ""
137             t_render = None
138             t_att = {}
139             for (an, av) in e.attributes.items():
140                 an = str(an)
141                 if isinstance(av, unicode):
142                     av = av.encode("utf8")
143                 else:
144                     av = av.nodeValue.encode("utf8")
145                 if an.startswith("t-"):
146                     for i in self._render_att:
147                         if an[2:].startswith(i):
148                             g_att += self._render_att[i](self, e, an, av, v)
149                             break
150                     else:
151                         if an[2:] in self._render_tag:
152                             t_render = an[2:]
153                         t_att[an[2:]] = av
154                 else:
155                     g_att += ' %s="%s"' % (an, werkzeug.utils.escape(av))
156
157             if 'debug' in t_att:
158                 debugger = t_att.get('debug', 'pdb')
159                 __import__(debugger).set_trace() # pdb, ipdb, pudb, ...
160             if t_render:
161                 if t_render in self._render_tag:
162                     r = self._render_tag[t_render](self, e, t_att, g_att, v)
163             else:
164                 r = self.render_element(e, t_att, g_att, v)
165         if isinstance(r, unicode):
166             return r.encode('utf-8')
167         return r
168
169     def render_element(self, e, t_att, g_att, v, inner=None):
170         # e: element
171         # t_att: t-* attributes
172         # g_att: generated attributes
173         # v: values
174         # inner: optional innerXml
175         if inner:
176             g_inner = inner
177         else:
178             g_inner = []
179             for n in e.childNodes:
180                 g_inner.append(self.render_node(n, v))
181         name = str(e.nodeName)
182         inner = "".join(g_inner)
183         trim = t_att.get("trim", 0)
184         if trim == 0:
185             pass
186         elif trim == 'left':
187             inner = inner.lstrip()
188         elif trim == 'right':
189             inner = inner.rstrip()
190         elif trim == 'both':
191             inner = inner.strip()
192         if name == "t":
193             return inner
194         elif len(inner) or name not in self._void_elements:
195             return "<%s%s>%s</%s>" % tuple(
196                 v if isinstance(v, str) else v.encode('utf-8')
197                 for v in (name, g_att, inner, name))
198         else:
199             return "<%s%s/>" % (name, g_att)
200
201     # Attributes
202     def render_att_att(self, e, an, av, v):
203         if an.startswith("t-attf-"):
204             att, val = an[7:], self.eval_format(av, v)
205         elif an.startswith("t-att-"):
206             att, val = an[6:], self.eval(av, v)
207             if isinstance(val, unicode):
208                 val = val.encode("utf8")
209         else:
210             att, val = self.eval_object(av, v)
211         return val and ' %s="%s"' % (att, werkzeug.utils.escape(val)) or " "
212
213     def render_att_href(self, e, an, av, v):
214         return self.url_for(e, an, av, v)
215     def render_att_src(self, e, an, av, v):
216         return self.url_for(e, an, av, v)
217     def render_att_action(self, e, an, av, v):
218         return self.url_for(e, an, av, v)
219     def url_for(self, e, an, av, v):
220         if 'url_for' not in v:
221             raise KeyError("qweb: no 'url_for' found in context")
222         path = str(v['url_for'](self.eval_format(av, v)))
223         return ' %s="%s"' % (an[2:], werkzeug.utils.escape(path))
224
225     # Tags
226     def render_tag_raw(self, e, t_att, g_att, v):
227         inner = self.eval_str(t_att["raw"], v)
228         return self.render_element(e, t_att, g_att, v, inner)
229
230     def render_tag_rawf(self, e, t_att, g_att, v):
231         inner = self.eval_format(t_att["rawf"], v)
232         return self.render_element(e, t_att, g_att, v, inner)
233
234     def render_tag_esc(self, e, t_att, g_att, v):
235         inner = werkzeug.utils.escape(self.eval_str(t_att["esc"], v))
236         return self.render_element(e, t_att, g_att, v, inner)
237
238     def render_tag_escf(self, e, t_att, g_att, v):
239         inner = werkzeug.utils.escape(self.eval_format(t_att["escf"], v))
240         return self.render_element(e, t_att, g_att, v, inner)
241
242     def render_tag_foreach(self, e, t_att, g_att, v):
243         expr = t_att["foreach"]
244         enum = self.eval_object(expr, v)
245         if enum is not None:
246             var = t_att.get('as', expr).replace('.', '_')
247             d = v.copy()
248             size = -1
249             if isinstance(enum, (list, tuple)):
250                 size = len(enum)
251             elif hasattr(enum, 'count'):
252                 size = enum.count()
253             d["%s_size" % var] = size
254             d["%s_all" % var] = enum
255             index = 0
256             ru = []
257             for i in enum:
258                 d["%s_value" % var] = i
259                 d["%s_index" % var] = index
260                 d["%s_first" % var] = index == 0
261                 d["%s_even" % var] = index % 2
262                 d["%s_odd" % var] = (index + 1) % 2
263                 d["%s_last" % var] = index + 1 == size
264                 if index % 2:
265                     d["%s_parity" % var] = 'odd'
266                 else:
267                     d["%s_parity" % var] = 'even'
268                 if 'as' in t_att:
269                     d[var] = i
270                 elif isinstance(i, dict):
271                     d.update(i)
272                 ru.append(self.render_element(e, t_att, g_att, d))
273                 index += 1
274             return "".join(ru)
275         else:
276             raise NameError("QWeb: foreach enumerator %r is not defined while rendering template %r" % (expr, v.get('__template__')))
277
278     def render_tag_if(self, e, t_att, g_att, v):
279         if self.eval_bool(t_att["if"], v):
280             return self.render_element(e, t_att, g_att, v)
281         else:
282             return ""
283
284     def render_tag_call(self, e, t_att, g_att, v):
285         if "import" in t_att:
286             d = v
287         else:
288             d = v.copy()
289         d[0] = self.render_element(e, t_att, g_att, d)
290         return self.render(self.eval_format(t_att["call"], d), d)
291
292     def render_tag_set(self, e, t_att, g_att, v):
293         if "value" in t_att:
294             v[t_att["set"]] = self.eval_object(t_att["value"], v)
295         elif "valuef" in t_att:
296             v[t_att["set"]] = self.eval_format(t_att["valuef"], v)
297         else:
298             v[t_att["set"]] = self.render_element(e, t_att, g_att, v)
299         return ""
300
301     def render_tag_field(self, e, t_att, g_att, v):
302         """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
303         node_name = e.nodeName
304         assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
305                                  "ol", "ul", "ol", "dl", "dt", "dd"),\
306             "RTE widgets do not work correctly on %r elements" % node_name
307         assert node_name != 't',\
308             "t-field can not be used on a t element, provide an actual HTML node"
309
310         record, field = t_att["field"].rsplit('.', 1)
311         record = self.eval_object(record, v)
312
313         column = record._model._all_columns[field].column
314         field_type = column._type
315
316         req = v['request']
317         converter = req.registry['ir.fields.converter'].from_field(
318             req.cr, req.uid, record._model, column, totype='html')
319
320         content = None
321         try:
322             value = record[field]
323             if value:
324                 content, warnings = converter(value)
325                 assert not warnings
326         except KeyError:
327             _logger.warning("t-field no field %s for model %s", field, record._model._name)
328
329         g_att += ''.join(
330             ' %s="%s"' % (name, werkzeug.utils.escape(value))
331             for name, value in [
332                 ('data-oe-model', record._model._name),
333                 ('data-oe-id', record.id),
334                 ('data-oe-field', field),
335                 ('data-oe-type', field_type),
336                 ('data-oe-translate', '1' if column.translate else '0'),
337                 ('data-oe-expression', t_att['field']),
338             ]
339         )
340
341         return self.render_element(e, t_att, g_att, v, content or "")
342
343 # leave this, al.