[IMP] guard for None enum, remove 1 level of indent
[odoo/odoo.git] / openerp / addons / base / ir / ir_qweb.py
1 # -*- coding: utf-8 -*-
2 import collections
3 import cStringIO
4 import datetime
5 import hashlib
6 import json
7 import logging
8 import math
9 import os
10 import re
11 import sys
12 import xml  # FIXME use lxml and etree
13 import itertools
14 import lxml.html
15 from urlparse import urlparse
16
17 import babel
18 import babel.dates
19 import werkzeug
20 from PIL import Image
21
22 import openerp.http
23 import openerp.tools
24 import openerp.tools.func
25 import openerp.tools.lru
26 from openerp.http import request
27 from openerp.tools.safe_eval import safe_eval as eval
28 from openerp.osv import osv, orm, fields
29 from openerp.tools.translate import _
30 from openerp import SUPERUSER_ID
31
32 _logger = logging.getLogger(__name__)
33
34 #--------------------------------------------------------------------
35 # QWeb template engine
36 #--------------------------------------------------------------------
37 class QWebException(Exception):
38     def __init__(self, message, **kw):
39         Exception.__init__(self, message)
40         self.qweb = dict(kw)
41
42 class QWebTemplateNotFound(QWebException):
43     pass
44
45 def raise_qweb_exception(etype=None, **kw):
46     if etype is None:
47         etype = QWebException
48     orig_type, original, tb = sys.exc_info()
49     try:
50         raise etype, original, tb
51     except etype, e:
52         for k, v in kw.items():
53             e.qweb[k] = v
54         # Will use `raise foo from bar` in python 3 and rename cause to __cause__
55         e.qweb['cause'] = original
56         raise
57
58 class QWebContext(dict):
59     def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
60         self.cr = cr
61         self.uid = uid
62         self.loader = loader
63         self.templates = templates or {}
64         self.context = context
65         dic = dict(data)
66         super(QWebContext, self).__init__(dic)
67         self['defined'] = lambda key: key in self
68
69     def safe_eval(self, expr):
70         locals_dict = collections.defaultdict(lambda: None)
71         locals_dict.update(self)
72         locals_dict.pop('cr', None)
73         locals_dict.pop('loader', None)
74         return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
75
76     def copy(self):
77         return QWebContext(self.cr, self.uid, dict.copy(self),
78                            loader=self.loader,
79                            templates=self.templates,
80                            context=self.context)
81
82     def __copy__(self):
83         return self.copy()
84
85 class QWeb(orm.AbstractModel):
86     """QWeb Xml templating engine
87
88     The templating engine use a very simple syntax based "magic" xml
89     attributes, to produce textual output (even non-xml).
90
91     The core magic attributes are:
92
93     flow attributes:
94         t-if t-foreach t-call
95
96     output attributes:
97         t-att t-raw t-esc t-trim
98
99     assignation attribute:
100         t-set
101
102     QWeb can be extended like any OpenERP model and new attributes can be
103     added.
104
105     If you need to customize t-fields rendering, subclass the ir.qweb.field
106     model (and its sub-models) then override :meth:`~.get_converter_for` to
107     fetch the right field converters for your qweb model.
108
109     Beware that if you need extensions or alterations which could be
110     incompatible with other subsystems, you should create a local object
111     inheriting from ``ir.qweb`` and customize that.
112     """
113
114     _name = 'ir.qweb'
115
116     node = xml.dom.Node
117     _void_elements = frozenset([
118         'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
119         'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
120     _format_regex = re.compile(
121         '(?:'
122             # ruby-style pattern
123             '#\{(.+?)\}'
124         ')|(?:'
125             # jinja-style pattern
126             '\{\{(.+?)\}\}'
127         ')')
128
129     def __init__(self, pool, cr):
130         super(QWeb, self).__init__(pool, cr)
131
132         self._render_tag = self.prefixed_methods('render_tag_')
133         self._render_att = self.prefixed_methods('render_att_')
134
135     def prefixed_methods(self, prefix):
136         """ Extracts all methods prefixed by ``prefix``, and returns a mapping
137         of (t-name, method) where the t-name is the method name with prefix
138         removed and underscore converted to dashes
139
140         :param str prefix:
141         :return: dict
142         """
143         n_prefix = len(prefix)
144         return dict(
145             (name[n_prefix:].replace('_', '-'), getattr(type(self), name))
146             for name in dir(self)
147             if name.startswith(prefix)
148         )
149
150     def register_tag(self, tag, func):
151         self._render_tag[tag] = func
152
153     def add_template(self, qwebcontext, name, node):
154         """Add a parsed template in the context. Used to preprocess templates."""
155         qwebcontext.templates[name] = node
156
157     def load_document(self, document, res_id, qwebcontext):
158         """
159         Loads an XML document and installs any contained template in the engine
160         """
161         if hasattr(document, 'documentElement'):
162             dom = document
163         elif document.startswith("<?xml"):
164             dom = xml.dom.minidom.parseString(document)
165         else:
166             dom = xml.dom.minidom.parse(document)
167
168         for node in dom.documentElement.childNodes:
169             if node.nodeType == self.node.ELEMENT_NODE:
170                 if node.getAttribute('t-name'):
171                     name = str(node.getAttribute("t-name"))
172                     self.add_template(qwebcontext, name, node)
173                 if res_id and node.tagName == "t":
174                     self.add_template(qwebcontext, res_id, node)
175                     res_id = None
176
177     def get_template(self, name, qwebcontext):
178         origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
179         if qwebcontext.loader and name not in qwebcontext.templates:
180             try:
181                 xml_doc = qwebcontext.loader(name)
182             except ValueError:
183                 raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
184             self.load_document(xml_doc, isinstance(name, (int, long)) and name or None, qwebcontext=qwebcontext)
185
186         if name in qwebcontext.templates:
187             return qwebcontext.templates[name]
188
189         raise QWebTemplateNotFound("Template %r not found" % name, template=origin_template)
190
191     def eval(self, expr, qwebcontext):
192         try:
193             return qwebcontext.safe_eval(expr)
194         except Exception:
195             template = qwebcontext.get('__template__')
196             raise_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
197
198     def eval_object(self, expr, qwebcontext):
199         return self.eval(expr, qwebcontext)
200
201     def eval_str(self, expr, qwebcontext):
202         if expr == "0":
203             return qwebcontext.get(0, '')
204         val = self.eval(expr, qwebcontext)
205         if isinstance(val, unicode):
206             return val.encode("utf8")
207         if val is False or val is None:
208             return ''
209         return str(val)
210
211     def eval_format(self, expr, qwebcontext):
212         expr, replacements = self._format_regex.subn(
213             lambda m: self.eval_str(m.group(1) or m.group(2), qwebcontext),
214             expr
215         )
216
217         if replacements:
218             return expr
219
220         try:
221             return str(expr % qwebcontext)
222         except Exception:
223             template = qwebcontext.get('__template__')
224             raise_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
225
226     def eval_bool(self, expr, qwebcontext):
227         return int(bool(self.eval(expr, qwebcontext)))
228
229     def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
230         if qwebcontext is None:
231             qwebcontext = {}
232
233         if not isinstance(qwebcontext, QWebContext):
234             qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
235
236         qwebcontext['__template__'] = id_or_xml_id
237         stack = qwebcontext.get('__stack__', [])
238         if stack:
239             qwebcontext['__caller__'] = stack[-1]
240         stack.append(id_or_xml_id)
241         qwebcontext['__stack__'] = stack
242         qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
243         return self.render_node(self.get_template(id_or_xml_id, qwebcontext), qwebcontext)
244
245     def render_node(self, element, qwebcontext):
246         result = ""
247         if element.nodeType == self.node.TEXT_NODE or element.nodeType == self.node.CDATA_SECTION_NODE:
248             result = element.data.encode("utf8")
249         elif element.nodeType == self.node.ELEMENT_NODE:
250             generated_attributes = ""
251             t_render = None
252             template_attributes = {}
253             for (attribute_name, attribute_value) in element.attributes.items():
254                 attribute_name = str(attribute_name)
255                 if attribute_name == "groups":
256                     cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
257                     uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
258                     can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
259                     if not can_see:
260                         return ''
261                     continue
262
263                 if isinstance(attribute_value, unicode):
264                     attribute_value = attribute_value.encode("utf8")
265                 else:
266                     attribute_value = attribute_value.nodeValue.encode("utf8")
267
268                 if attribute_name.startswith("t-"):
269                     for attribute in self._render_att:
270                         if attribute_name[2:].startswith(attribute):
271                             att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext)
272                             generated_attributes += val and ' %s="%s"' % (att, werkzeug.utils.escape(val)) or " "
273                             break
274                     else:
275                         if attribute_name[2:] in self._render_tag:
276                             t_render = attribute_name[2:]
277                         template_attributes[attribute_name[2:]] = attribute_value
278                 else:
279                     generated_attributes += ' %s="%s"' % (attribute_name, werkzeug.utils.escape(attribute_value))
280
281             if 'debug' in template_attributes:
282                 debugger = template_attributes.get('debug', 'pdb')
283                 __import__(debugger).set_trace()  # pdb, ipdb, pudb, ...
284             if t_render:
285                 result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
286             else:
287                 result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
288         if isinstance(result, unicode):
289             return result.encode('utf-8')
290         return result
291
292     def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
293         # element: element
294         # template_attributes: t-* attributes
295         # generated_attributes: generated attributes
296         # qwebcontext: values
297         # inner: optional innerXml
298         if inner:
299             g_inner = inner
300         else:
301             g_inner = []
302             for current_node in element.childNodes:
303                 try:
304                     g_inner.append(self.render_node(current_node, qwebcontext))
305                 except QWebException:
306                     raise
307                 except Exception:
308                     template = qwebcontext.get('__template__')
309                     raise_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template)
310         name = str(element.nodeName)
311         inner = "".join(g_inner)
312         trim = template_attributes.get("trim", 0)
313         if trim == 0:
314             pass
315         elif trim == 'left':
316             inner = inner.lstrip()
317         elif trim == 'right':
318             inner = inner.rstrip()
319         elif trim == 'both':
320             inner = inner.strip()
321         if name == "t":
322             return inner
323         elif len(inner) or name not in self._void_elements:
324             return "<%s%s>%s</%s>" % tuple(
325                 qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
326                 for qwebcontext in (name, generated_attributes, inner, name)
327             )
328         else:
329             return "<%s%s/>" % (name, generated_attributes)
330
331     # Attributes
332     def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
333         if attribute_name.startswith("t-attf-"):
334             att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext)
335         elif attribute_name.startswith("t-att-"):
336             att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext)
337             if isinstance(val, unicode):
338                 val = val.encode("utf8")
339         else:
340             att, val = self.eval_object(attribute_value, qwebcontext)
341         return att, val
342
343     # Tags
344     def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
345         inner = self.eval_str(template_attributes["raw"], qwebcontext)
346         return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
347
348     def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
349         options = json.loads(template_attributes.get('esc-options') or '{}')
350         widget = self.get_widget_for(options.get('widget', ''))
351         inner = widget.format(template_attributes['esc'], options, qwebcontext)
352         return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
353
354     def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
355         expr = template_attributes["foreach"]
356         enum = self.eval_object(expr, qwebcontext)
357         if enum is None:
358             template = qwebcontext.get('__template__')
359             raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
360
361         varname = template_attributes['as'].replace('.', '_')
362         copy_qwebcontext = qwebcontext.copy()
363         size = -1
364         if isinstance(enum, collections.Sized):
365             size = len(enum)
366         copy_qwebcontext["%s_size" % varname] = size
367         copy_qwebcontext["%s_all" % varname] = enum
368         ru = []
369         for index, item in enumerate(enum):
370             copy_qwebcontext.update({
371                 varname: item,
372                 '%s_value' % varname: item,
373                 '%s_index' % varname: index,
374                 '%s_first' % varname: index == 0,
375                 '%s_last' % varname: index + 1 == size,
376             })
377             if index % 2:
378                 copy_qwebcontext.update({
379                     '%s_parity' % varname: 'odd',
380                     '%s_even' % varname: False,
381                     '%s_odd' % varname: True,
382                 })
383             else:
384                 copy_qwebcontext.update({
385                     '%s_parity' % varname: 'even',
386                     '%s_even' % varname: True,
387                     '%s_odd' % varname: False,
388                 })
389             ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
390         return "".join(ru)
391
392     def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
393         if self.eval_bool(template_attributes["if"], qwebcontext):
394             return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
395         return ""
396
397     def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
398         d = qwebcontext.copy()
399         d[0] = self.render_element(element, template_attributes, generated_attributes, d)
400         cr = d.get('request') and d['request'].cr or None
401         uid = d.get('request') and d['request'].uid or None
402
403         template = self.eval_format(template_attributes["call"], d)
404         try:
405             template = int(template)
406         except ValueError:
407             pass
408         return self.render(cr, uid, template, d)
409
410     def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
411         """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
412         name = template_attributes['call-assets']
413
414         # Backward compatibility hack for manifest usage
415         qwebcontext['manifest_list'] = openerp.addons.web.controllers.main.manifest_list
416
417         d = qwebcontext.copy()
418         d.context['inherit_branding'] = False
419         content = self.render_tag_call(
420             element, {'call': name}, generated_attributes, d)
421         bundle = AssetsBundle(name, html=content)
422         css = self.get_attr_bool(template_attributes.get('css'), default=True)
423         js = self.get_attr_bool(template_attributes.get('js'), default=True)
424         return bundle.to_html(css=css, js=js, debug=bool(qwebcontext.get('debug')))
425
426     def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
427         if "value" in template_attributes:
428             qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
429         elif "valuef" in template_attributes:
430             qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
431         else:
432             qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
433         return ""
434
435     def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
436         """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
437         node_name = element.nodeName
438         assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
439                                  "li", "ul", "ol", "dl", "dt", "dd"),\
440             "RTE widgets do not work correctly on %r elements" % node_name
441         assert node_name != 't',\
442             "t-field can not be used on a t element, provide an actual HTML node"
443
444         record, field_name = template_attributes["field"].rsplit('.', 1)
445         record = self.eval_object(record, qwebcontext)
446
447         column = record._model._all_columns[field_name].column
448         options = json.loads(template_attributes.get('field-options') or '{}')
449         field_type = get_field_type(column, options)
450
451         converter = self.get_converter_for(field_type)
452
453         return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
454                                  element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
455
456     def get_converter_for(self, field_type):
457         return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
458
459     def get_widget_for(self, widget):
460         return self.pool.get('ir.qweb.widget.' + widget, self.pool['ir.qweb.widget'])
461
462     def get_attr_bool(self, attr, default=False):
463         if attr:
464             attr = attr.lower()
465             if attr in ('false', '0'):
466                 return False
467             elif attr in ('true', '1'):
468                 return True
469         return default
470
471 #--------------------------------------------------------------------
472 # QWeb Fields converters
473 #--------------------------------------------------------------------
474
475 class FieldConverter(osv.AbstractModel):
476     """ Used to convert a t-field specification into an output HTML field.
477
478     :meth:`~.to_html` is the entry point of this conversion from QWeb, it:
479
480     * converts the record value to html using :meth:`~.record_to_html`
481     * generates the metadata attributes (``data-oe-``) to set on the root
482       result node
483     * generates the root result node itself through :meth:`~.render_element`
484     """
485     _name = 'ir.qweb.field'
486
487     def attributes(self, cr, uid, field_name, record, options,
488                    source_element, g_att, t_att, qweb_context,
489                    context=None):
490         """
491         Generates the metadata attributes (prefixed by ``data-oe-`` for the
492         root node of the field conversion. Attribute values are escaped by the
493         parent using ``werkzeug.utils.escape``.
494
495         The default attributes are:
496
497         * ``model``, the name of the record's model
498         * ``id`` the id of the record to which the field belongs
499         * ``field`` the name of the converted field
500         * ``type`` the logical field type (widget, may not match the column's
501           ``type``, may not be any _column subclass name)
502         * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
503           column is translatable
504         * ``expression``, the original expression
505
506         :returns: iterable of (attribute name, attribute value) pairs.
507         """
508         column = record._model._all_columns[field_name].column
509         field_type = get_field_type(column, options)
510         return [
511             ('data-oe-model', record._model._name),
512             ('data-oe-id', record.id),
513             ('data-oe-field', field_name),
514             ('data-oe-type', field_type),
515             ('data-oe-expression', t_att['field']),
516         ]
517
518     def value_to_html(self, cr, uid, value, column, options=None, context=None):
519         """ Converts a single value to its HTML version/output
520         """
521         if not value: return ''
522         return value
523
524     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
525         """ Converts the specified field of the browse_record ``record`` to
526         HTML
527         """
528         return self.value_to_html(
529             cr, uid, record[field_name], column, options=options, context=context)
530
531     def to_html(self, cr, uid, field_name, record, options,
532                 source_element, t_att, g_att, qweb_context, context=None):
533         """ Converts a ``t-field`` to its HTML output. A ``t-field`` may be
534         extended by a ``t-field-options``, which is a JSON-serialized mapping
535         of configuration values.
536
537         A default configuration key is ``widget`` which can override the
538         field's own ``_type``.
539         """
540         try:
541             content = self.record_to_html(
542                 cr, uid, field_name, record,
543                 record._model._all_columns[field_name].column,
544                 options, context=context)
545             if options.get('html-escape', True):
546                 content = werkzeug.utils.escape(content)
547             elif hasattr(content, '__html__'):
548                 content = content.__html__()
549         except Exception:
550             _logger.warning("Could not get field %s for model %s",
551                             field_name, record._model._name, exc_info=True)
552             content = None
553
554         if context and context.get('inherit_branding'):
555             # add branding attributes
556             g_att += ''.join(
557                 ' %s="%s"' % (name, werkzeug.utils.escape(value))
558                 for name, value in self.attributes(
559                     cr, uid, field_name, record, options,
560                     source_element, g_att, t_att, qweb_context)
561             )
562
563         return self.render_element(cr, uid, source_element, t_att, g_att,
564                                    qweb_context, content)
565
566     def qweb_object(self):
567         return self.pool['ir.qweb']
568
569     def render_element(self, cr, uid, source_element, t_att, g_att,
570                        qweb_context, content):
571         """ Final rendering hook, by default just calls ir.qweb's ``render_element``
572         """
573         return self.qweb_object().render_element(
574             source_element, t_att, g_att, qweb_context, content or '')
575
576     def user_lang(self, cr, uid, context):
577         """
578         Fetches the res.lang object corresponding to the language code stored
579         in the user's context. Fallbacks to en_US if no lang is present in the
580         context *or the language code is not valid*.
581
582         :returns: res.lang browse_record
583         """
584         if context is None: context = {}
585
586         lang_code = context.get('lang') or 'en_US'
587         Lang = self.pool['res.lang']
588
589         lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
590                or  Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
591
592         return Lang.browse(cr, uid, lang_ids[0], context=context)
593
594 class FloatConverter(osv.AbstractModel):
595     _name = 'ir.qweb.field.float'
596     _inherit = 'ir.qweb.field'
597
598     def precision(self, cr, uid, column, options=None, context=None):
599         _, precision = column.digits or (None, None)
600         return precision
601
602     def value_to_html(self, cr, uid, value, column, options=None, context=None):
603         if context is None:
604             context = {}
605         precision = self.precision(cr, uid, column, options=options, context=context)
606         fmt = '%f' if precision is None else '%.{precision}f'
607
608         lang_code = context.get('lang') or 'en_US'
609         lang = self.pool['res.lang']
610         formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
611
612         # %f does not strip trailing zeroes. %g does but its precision causes
613         # it to switch to scientific notation starting at a million *and* to
614         # strip decimals. So use %f and if no precision was specified manually
615         # strip trailing 0.
616         if precision is None:
617             formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
618         return formatted
619
620 class DateConverter(osv.AbstractModel):
621     _name = 'ir.qweb.field.date'
622     _inherit = 'ir.qweb.field'
623
624     def value_to_html(self, cr, uid, value, column, options=None, context=None):
625         if not value: return ''
626         lang = self.user_lang(cr, uid, context=context)
627         locale = babel.Locale.parse(lang.code)
628
629         if isinstance(value, basestring):
630             value = datetime.datetime.strptime(
631                 value, openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
632
633         if options and 'format' in options:
634             pattern = options['format']
635         else:
636             strftime_pattern = lang.date_format
637             pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
638
639         return babel.dates.format_date(
640             value, format=pattern,
641             locale=locale)
642
643 class DateTimeConverter(osv.AbstractModel):
644     _name = 'ir.qweb.field.datetime'
645     _inherit = 'ir.qweb.field'
646
647     def value_to_html(self, cr, uid, value, column, options=None, context=None):
648         if not value: return ''
649         lang = self.user_lang(cr, uid, context=context)
650         locale = babel.Locale.parse(lang.code)
651
652         if isinstance(value, basestring):
653             value = datetime.datetime.strptime(
654                 value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
655         value = fields.datetime.context_timestamp(
656             cr, uid, timestamp=value, context=context)
657
658         if options and 'format' in options:
659             pattern = options['format']
660         else:
661             strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
662             pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
663
664         if options and options.get('hide_seconds'):
665             pattern = pattern.replace(":ss", "").replace(":s", "")
666         
667         return babel.dates.format_datetime(value, format=pattern, locale=locale)
668
669 class TextConverter(osv.AbstractModel):
670     _name = 'ir.qweb.field.text'
671     _inherit = 'ir.qweb.field'
672
673     def value_to_html(self, cr, uid, value, column, options=None, context=None):
674         """
675         Escapes the value and converts newlines to br. This is bullshit.
676         """
677         if not value: return ''
678
679         return nl2br(value, options=options)
680
681 class SelectionConverter(osv.AbstractModel):
682     _name = 'ir.qweb.field.selection'
683     _inherit = 'ir.qweb.field'
684
685     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
686         value = record[field_name]
687         if not value: return ''
688         selection = dict(fields.selection.reify(
689             cr, uid, record._model, column))
690         return self.value_to_html(
691             cr, uid, selection[value], column, options=options)
692
693 class ManyToOneConverter(osv.AbstractModel):
694     _name = 'ir.qweb.field.many2one'
695     _inherit = 'ir.qweb.field'
696
697     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
698         [read] = record.read([field_name])
699         if not read[field_name]: return ''
700         _, value = read[field_name]
701         return nl2br(value, options=options)
702
703 class HTMLConverter(osv.AbstractModel):
704     _name = 'ir.qweb.field.html'
705     _inherit = 'ir.qweb.field'
706
707     def value_to_html(self, cr, uid, value, column, options=None, context=None):
708         return HTMLSafe(value or '')
709
710 class ImageConverter(osv.AbstractModel):
711     """ ``image`` widget rendering, inserts a data:uri-using image tag in the
712     document. May be overridden by e.g. the website module to generate links
713     instead.
714
715     .. todo:: what happens if different output need different converters? e.g.
716               reports may need embedded images or FS links whereas website
717               needs website-aware
718     """
719     _name = 'ir.qweb.field.image'
720     _inherit = 'ir.qweb.field'
721
722     def value_to_html(self, cr, uid, value, column, options=None, context=None):
723         try:
724             image = Image.open(cStringIO.StringIO(value.decode('base64')))
725             image.verify()
726         except IOError:
727             raise ValueError("Non-image binary fields can not be converted to HTML")
728         except: # image.verify() throws "suitable exceptions", I have no idea what they are
729             raise ValueError("Invalid image content")
730
731         return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
732
733 class MonetaryConverter(osv.AbstractModel):
734     """ ``monetary`` converter, has a mandatory option
735     ``display_currency``.
736
737     The currency is used for formatting *and rounding* of the float value. It
738     is assumed that the linked res_currency has a non-empty rounding value and
739     res.currency's ``round`` method is used to perform rounding.
740
741     .. note:: the monetary converter internally adds the qweb context to its
742               options mapping, so that the context is available to callees.
743               It's set under the ``_qweb_context`` key.
744     """
745     _name = 'ir.qweb.field.monetary'
746     _inherit = 'ir.qweb.field'
747
748     def to_html(self, cr, uid, field_name, record, options,
749                 source_element, t_att, g_att, qweb_context, context=None):
750         options['_qweb_context'] = qweb_context
751         return super(MonetaryConverter, self).to_html(
752             cr, uid, field_name, record, options,
753             source_element, t_att, g_att, qweb_context, context=context)
754
755     def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
756         if context is None:
757             context = {}
758         Currency = self.pool['res.currency']
759         display = self.display_currency(cr, uid, options)
760
761         # lang.format mandates a sprintf-style format. These formats are non-
762         # minimal (they have a default fixed precision instead), and
763         # lang.format will not set one by default. currency.round will not
764         # provide one either. So we need to generate a precision value
765         # (integer > 0) from the currency's rounding (a float generally < 1.0).
766         #
767         # The log10 of the rounding should be the number of digits involved if
768         # negative, if positive clamp to 0 digits and call it a day.
769         # nb: int() ~ floor(), we want nearest rounding instead
770         precision = int(round(math.log10(display.rounding)))
771         fmt = "%.{0}f".format(-precision if precision < 0 else 0)
772
773         lang_code = context.get('lang') or 'en_US'
774         lang = self.pool['res.lang']
775         formatted_amount = lang.format(cr, uid, [lang_code], 
776             fmt, Currency.round(cr, uid, display, record[field_name]),
777             grouping=True, monetary=True)
778
779         pre = post = u''
780         if display.position == 'before':
781             pre = u'{symbol} '
782         else:
783             post = u' {symbol}'
784
785         return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
786             formatted_amount,
787             pre=pre, post=post,
788         ).format(
789             symbol=display.symbol,
790         ))
791
792     def display_currency(self, cr, uid, options):
793         return self.qweb_object().eval_object(
794             options['display_currency'], options['_qweb_context'])
795
796 TIMEDELTA_UNITS = (
797     ('year',   3600 * 24 * 365),
798     ('month',  3600 * 24 * 30),
799     ('week',   3600 * 24 * 7),
800     ('day',    3600 * 24),
801     ('hour',   3600),
802     ('minute', 60),
803     ('second', 1)
804 )
805 class DurationConverter(osv.AbstractModel):
806     """ ``duration`` converter, to display integral or fractional values as
807     human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
808
809     Can be used on any numerical field.
810
811     Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
812     ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
813     field value before converting it.
814
815     Sub-second values will be ignored.
816     """
817     _name = 'ir.qweb.field.duration'
818     _inherit = 'ir.qweb.field'
819
820     def value_to_html(self, cr, uid, value, column, options=None, context=None):
821         units = dict(TIMEDELTA_UNITS)
822         if value < 0:
823             raise ValueError(_("Durations can't be negative"))
824         if not options or options.get('unit') not in units:
825             raise ValueError(_("A unit must be provided to duration widgets"))
826
827         locale = babel.Locale.parse(
828             self.user_lang(cr, uid, context=context).code)
829         factor = units[options['unit']]
830
831         sections = []
832         r = value * factor
833         for unit, secs_per_unit in TIMEDELTA_UNITS:
834             v, r = divmod(r, secs_per_unit)
835             if not v: continue
836             section = babel.dates.format_timedelta(
837                 v*secs_per_unit, threshold=1, locale=locale)
838             if section:
839                 sections.append(section)
840         return ' '.join(sections)
841
842
843 class RelativeDatetimeConverter(osv.AbstractModel):
844     _name = 'ir.qweb.field.relative'
845     _inherit = 'ir.qweb.field'
846
847     def value_to_html(self, cr, uid, value, column, options=None, context=None):
848         parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
849         locale = babel.Locale.parse(
850             self.user_lang(cr, uid, context=context).code)
851
852         if isinstance(value, basestring):
853             value = datetime.datetime.strptime(value, parse_format)
854
855         # value should be a naive datetime in UTC. So is fields.datetime.now()
856         reference = datetime.datetime.strptime(column.now(), parse_format)
857
858         return babel.dates.format_timedelta(
859             value - reference, add_direction=True, locale=locale)
860
861 class Contact(orm.AbstractModel):
862     _name = 'ir.qweb.field.contact'
863     _inherit = 'ir.qweb.field.many2one'
864
865     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
866         if options is None:
867             options = {}
868         opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
869
870         if not getattr(record, field_name):
871             return None
872
873         id = getattr(record, field_name).id
874         field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context={"show_address": True})
875         value = field_browse.name_get()[0][1]
876
877         val = {
878             'name': value.split("\n")[0],
879             'address': werkzeug.utils.escape("\n".join(value.split("\n")[1:])),
880             'phone': field_browse.phone,
881             'mobile': field_browse.mobile,
882             'fax': field_browse.fax,
883             'city': field_browse.city,
884             'country_id': field_browse.country_id and field_browse.country_id.name_get()[0][1],
885             'website': field_browse.website,
886             'email': field_browse.email,
887             'fields': opf,
888             'object': field_browse,
889             'options': options
890         }
891
892         html = self.pool["ir.ui.view"].render(cr, uid, "base.contact", val, engine='ir.qweb', context=context).decode('utf8')
893
894         return HTMLSafe(html)
895
896 class QwebView(orm.AbstractModel):
897     _name = 'ir.qweb.field.qweb'
898     _inherit = 'ir.qweb.field.many2one'
899
900     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
901         if not getattr(record, field_name):
902             return None
903
904         view = getattr(record, field_name)
905
906         if view._model._name != "ir.ui.view":
907             _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
908             return None
909
910         ctx = (context or {}).copy()
911         ctx['object'] = record
912         html = view.render(ctx, engine='ir.qweb', context=ctx).decode('utf8')
913
914         return HTMLSafe(html)
915
916 class QwebWidget(osv.AbstractModel):
917     _name = 'ir.qweb.widget'
918
919     def _format(self, inner, options, qwebcontext):
920         return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
921
922     def format(self, inner, options, qwebcontext):
923         return werkzeug.utils.escape(self._format(inner, options, qwebcontext))
924
925 class QwebWidgetMonetary(osv.AbstractModel):
926     _name = 'ir.qweb.widget.monetary'
927     _inherit = 'ir.qweb.widget'
928
929     def _format(self, inner, options, qwebcontext):
930         inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
931         display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
932         precision = int(round(math.log10(display.rounding)))
933         fmt = "%.{0}f".format(-precision if precision < 0 else 0)
934         lang_code = qwebcontext.context.get('lang') or 'en_US'
935         formatted_amount = self.pool['res.lang'].format(
936             qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
937         )
938         pre = post = u''
939         if display.position == 'before':
940             pre = u'{symbol} '
941         else:
942             post = u' {symbol}'
943
944         return u'{pre}{0}{post}'.format(
945             formatted_amount, pre=pre, post=post
946         ).format(symbol=display.symbol,)
947
948 class HTMLSafe(object):
949     """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
950     objects with a ``__html__`` methods but AFAIK does not provide any such
951     object.
952
953     Wrapping a string in HTML will prevent its escaping
954     """
955     __slots__ = ['string']
956     def __init__(self, string):
957         self.string = string
958     def __html__(self):
959         return self.string
960     def __str__(self):
961         s = self.string
962         if isinstance(s, unicode):
963             return s.encode('utf-8')
964         return s
965     def __unicode__(self):
966         s = self.string
967         if isinstance(s, str):
968             return s.decode('utf-8')
969         return s
970
971 def nl2br(string, options=None):
972     """ Converts newlines to HTML linebreaks in ``string``. Automatically
973     escapes content unless options['html-escape'] is set to False, and returns
974     the result wrapped in an HTMLSafe object.
975
976     :param str string:
977     :param dict options:
978     :rtype: HTMLSafe
979     """
980     if options is None: options = {}
981
982     if options.get('html-escape', True):
983         string = werkzeug.utils.escape(string)
984     return HTMLSafe(string.replace('\n', '<br>\n'))
985
986 def get_field_type(column, options):
987     """ Gets a t-field's effective type from the field's column and its options
988     """
989     return options.get('widget', column._type)
990
991 class AssetsBundle(object):
992     cache = openerp.tools.lru.LRU(32)
993     rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
994
995     def __init__(self, xmlid, html=None, debug=False):
996         self.debug = debug
997         self.xmlid = xmlid
998         self.javascripts = []
999         self.stylesheets = []
1000         self.remains = []
1001         self._checksum = None
1002         if html:
1003             self.parse(html)
1004
1005     def parse(self, html):
1006         fragments = lxml.html.fragments_fromstring(html)
1007         for el in fragments:
1008             if isinstance(el, basestring):
1009                 self.remains.append(el)
1010             elif isinstance(el, lxml.html.HtmlElement):
1011                 src = el.get('src')
1012                 href = el.get('href')
1013                 if el.tag == 'style':
1014                     self.stylesheets.append(StylesheetAsset(source=el.text))
1015                 elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
1016                     self.stylesheets.append(StylesheetAsset(url=href))
1017                 elif el.tag == 'script' and not src:
1018                     self.javascripts.append(JavascriptAsset(source=el.text))
1019                 elif el.tag == 'script' and self.can_aggregate(src):
1020                     self.javascripts.append(JavascriptAsset(url=src))
1021                 else:
1022                     self.remains.append(lxml.html.tostring(el))
1023             else:
1024                 try:
1025                     self.remains.append(lxml.html.tostring(el))
1026                 except Exception:
1027                     # notYETimplementederror
1028                     raise NotImplementedError
1029
1030     def can_aggregate(self, url):
1031         return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
1032
1033     def to_html(self, sep='\n            ', css=True, js=True, debug=False):
1034         response = []
1035         if debug:
1036             if css:
1037                 for style in self.stylesheets:
1038                     response.append(style.to_html())
1039             if js:
1040                 for jscript in self.javascripts:
1041                     response.append(jscript.to_html())
1042         else:
1043             if css and self.stylesheets:
1044                 response.append('<link href="/web/css/%s" rel="stylesheet"/>' % self.xmlid)
1045             if js and self.javascripts:
1046                 response.append('<script type="text/javascript" src="/web/js/%s"></script>' % self.xmlid)
1047         response.extend(self.remains)
1048         return sep + sep.join(response)
1049
1050     @openerp.tools.func.lazy_property
1051     def last_modified(self):
1052         return max(itertools.chain(
1053             (asset.last_modified for asset in self.javascripts),
1054             (asset.last_modified for asset in self.stylesheets),
1055             [datetime.datetime(1970, 1, 1)],
1056         ))
1057
1058     @openerp.tools.func.lazy_property
1059     def checksum(self):
1060         checksum = hashlib.new('sha1')
1061         for asset in itertools.chain(self.javascripts, self.stylesheets):
1062             checksum.update(asset.content.encode("utf-8"))
1063         return checksum.hexdigest()
1064
1065     def js(self):
1066         key = 'js_' + self.checksum
1067         if key not in self.cache:
1068             content =';\n'.join(asset.minify() for asset in self.javascripts)
1069             self.cache[key] = content
1070         if self.debug:
1071             return "/*\n%s\n*/\n" % '\n'.join(
1072                 [asset.url for asset in self.javascripts if asset.url]) + self.cache[key]
1073         return self.cache[key]
1074
1075     def css(self):
1076         key = 'css_' + self.checksum
1077         if key not in self.cache:
1078             content = '\n'.join(asset.minify() for asset in self.stylesheets)
1079             # move up all @import rules to the top
1080             matches = []
1081             def push(matchobj):
1082                 matches.append(matchobj.group(0))
1083                 return ''
1084
1085             content = re.sub(self.rx_css_import, push, content)
1086
1087             matches.append(content)
1088             content = u'\n'.join(matches)
1089             self.cache[key] = content
1090         if self.debug:
1091             return "/*\n%s\n*/\n" % '\n'.join(
1092                 [asset.url for asset in self.javascripts if asset.url]) + self.cache[key]
1093         return self.cache[key]
1094
1095 class WebAsset(object):
1096     def __init__(self, source=None, url=None):
1097         self.source = source
1098         self.url = url
1099         self._irattach = None
1100         self._content = None
1101         self.filename = None
1102         self.last_modified = None
1103         if source:
1104             self.last_modified = datetime.datetime(1970, 1, 1)
1105         if url:
1106             module = filter(None, self.url.split('/'))[0]
1107             try:
1108                 # Test url against modules static assets
1109                 mpath = openerp.http.addons_manifest[module]['addons_path']
1110                 self.filename = mpath + self.url.replace('/', os.path.sep)
1111                 self.last_modified = datetime.datetime.fromtimestamp(os.path.getmtime(self.filename))
1112             except Exception:
1113                 try:
1114                     # Test url against ir.attachments
1115                     domain = [('type', '=', 'binary'), ('url', '=', self.url)]
1116                     attach = request.registry['ir.attachment'].search_read(request.cr, SUPERUSER_ID, domain, ['__last_update', 'datas', 'mimetype'], context=request.context)
1117                     self._irattach = attach[0]
1118                     server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1119                     try:
1120                         self.last_modified =  datetime.datetime.strptime(attach[0]['__last_update'], server_format + '.%f')
1121                     except ValueError:
1122                         self.last_modified =  datetime.datetime.strptime(attach[0]['__last_update'], server_format)
1123                 except Exception:
1124                     raise KeyError("Could not find asset '%s' for '%s' addon" % (self.url, module))
1125
1126     @openerp.tools.func.lazy_property
1127     def content(self):
1128         if self.source:
1129             return self.source
1130         if self._irattach:
1131             return self._irattach['datas'].decode('base64')
1132         return self.get_content()
1133
1134     def get_content(self):
1135         with open(self.filename, 'rb') as fp:
1136             return fp.read().decode('utf-8')
1137
1138     def minify(self):
1139         return self.content
1140
1141 class JavascriptAsset(WebAsset):
1142     def minify(self):
1143         return rjsmin(self.content)
1144
1145     def to_html(self):
1146         if self.url:
1147             return '<script type="text/javascript" src="%s"></script>' % self.url
1148         else:
1149             return '<script type="text/javascript" charset="utf-8">%s</script>' % self.source
1150
1151 class StylesheetAsset(WebAsset):
1152     rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
1153     rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
1154     rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
1155
1156     def _get_content(self):
1157         with open(self.filename, 'rb') as fp:
1158             firstline = fp.readline()
1159             m = re.match(r'@charset "([^"]+)";', firstline)
1160             if m:
1161                 encoding = m.group(1)
1162             else:
1163                 encoding = "utf-8"
1164                 # "reinject" first line as it's not @charset
1165                 fp.seek(0)
1166
1167             return fp.read().decode(encoding)
1168
1169     def get_content(self):
1170         content = self._get_content()
1171         if self.url:
1172             web_dir = os.path.dirname(self.url)
1173
1174             content = self.rx_import.sub(
1175                 r"""@import \1%s/""" % (web_dir,),
1176                 content,
1177             )
1178
1179             content = self.rx_url.sub(
1180                 r"url(\1%s/" % (web_dir,),
1181                 content,
1182             )
1183         return content
1184
1185     def minify(self):
1186         # remove existing sourcemaps, make no sense after re-mini
1187         content = self.rx_sourceMap.sub('', self.content)
1188         # comments
1189         content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
1190         # space
1191         content = re.sub(r'\s+', ' ', content)
1192         content = re.sub(r' *([{}]) *', r'\1', content)
1193         return content
1194
1195     def to_html(self):
1196         if self.url:
1197             return '<link rel="stylesheet" href="%s" type="text/css"/>' % self.url
1198         else:
1199             return '<style type="text/css">%s</style>' % self.source
1200
1201 def rjsmin(script):
1202     """ Minify js with a clever regex.
1203     Taken from http://opensource.perlig.de/rjsmin
1204     Apache License, Version 2.0 """
1205     def subber(match):
1206         """ Substitution callback """
1207         groups = match.groups()
1208         return (
1209             groups[0] or
1210             groups[1] or
1211             groups[2] or
1212             groups[3] or
1213             (groups[4] and '\n') or
1214             (groups[5] and ' ') or
1215             (groups[6] and ' ') or
1216             (groups[7] and ' ') or
1217             ''
1218         )
1219
1220     result = re.sub(
1221         r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
1222         r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
1223         r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
1224         r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
1225         r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
1226         r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
1227         r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
1228         r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
1229         r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
1230         r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
1231         r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
1232         r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
1233         r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
1234         r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
1235         r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
1236         r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
1237         r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
1238         r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
1239         r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
1240         r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
1241         r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
1242     ).strip()
1243     return result
1244
1245 # vim:et: