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