~openerp-dev/openobject-server/trunk-missing-default-values-pza

« back to all changes in this revision

Viewing changes to openerp/addons/base/ir/ir_qweb.py

  • Committer: Antony Lesuisse
  • Date: 2014-01-31 00:52:07 UTC
  • mfrom: (4854.4.369 trunk-website-al)
  • mto: This revision was merged to the branch mainline in revision 5025.
  • Revision ID: al@openerp.com-20140131005207-mn7t6tar8cywe9hz
[MERGE] trunk-website-al

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
import collections
 
3
import cStringIO
 
4
import datetime
 
5
import json
 
6
import logging
 
7
import math
 
8
import re
 
9
import sys
 
10
import xml  # FIXME use lxml and etree
 
11
 
 
12
import babel
 
13
import babel.dates
 
14
import werkzeug.utils
 
15
from PIL import Image
 
16
 
 
17
import openerp.tools
 
18
from openerp.tools.safe_eval import safe_eval as eval
 
19
from openerp.osv import osv, orm, fields
 
20
from openerp.tools.translate import _
 
21
 
 
22
_logger = logging.getLogger(__name__)
 
23
 
 
24
#--------------------------------------------------------------------
 
25
# QWeb template engine
 
26
#--------------------------------------------------------------------
 
27
class QWebException(Exception):
 
28
    def __init__(self, message, **kw):
 
29
        Exception.__init__(self, message)
 
30
        self.qweb = dict(kw)
 
31
 
 
32
class QWebTemplateNotFound(QWebException):
 
33
    pass
 
34
 
 
35
def convert_to_qweb_exception(etype=None, **kw):
 
36
    if etype is None:
 
37
        etype = QWebException
 
38
    orig_type, original, tb = sys.exc_info()
 
39
    try:
 
40
        raise etype, original, tb
 
41
    except etype, e:
 
42
        for k, v in kw.items():
 
43
            e.qweb[k] = v
 
44
        e.qweb['inner'] = original
 
45
        return e
 
46
 
 
47
class QWebContext(dict):
 
48
    def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
 
49
        self.cr = cr
 
50
        self.uid = uid
 
51
        self.loader = loader
 
52
        self.templates = templates or {}
 
53
        self.context = context
 
54
        dic = dict(data)
 
55
        super(QWebContext, self).__init__(dic)
 
56
        self['defined'] = lambda key: key in self
 
57
 
 
58
    def safe_eval(self, expr):
 
59
        locals_dict = collections.defaultdict(lambda: None)
 
60
        locals_dict.update(self)
 
61
        locals_dict.pop('cr', None)
 
62
        locals_dict.pop('loader', None)
 
63
        return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
 
64
 
 
65
    def copy(self):
 
66
        return QWebContext(self.cr, self.uid, dict.copy(self),
 
67
                           loader=self.loader,
 
68
                           templates=self.templates,
 
69
                           context=self.context)
 
70
 
 
71
    def __copy__(self):
 
72
        return self.copy()
 
73
 
 
74
class QWeb(orm.AbstractModel):
 
75
    """QWeb Xml templating engine
 
76
 
 
77
    The templating engine use a very simple syntax based "magic" xml
 
78
    attributes, to produce textual output (even non-xml).
 
79
 
 
80
    The core magic attributes are:
 
81
 
 
82
    flow attributes:
 
83
        t-if t-foreach t-call
 
84
 
 
85
    output attributes:
 
86
        t-att t-raw t-esc t-trim
 
87
 
 
88
    assignation attribute:
 
89
        t-set
 
90
 
 
91
    QWeb can be extended like any OpenERP model and new attributes can be
 
92
    added.
 
93
 
 
94
    If you need to customize t-fields rendering, subclass the ir.qweb.field
 
95
    model (and its sub-models) then override :meth:`~.get_converter_for` to
 
96
    fetch the right field converters for your qweb model.
 
97
 
 
98
    Beware that if you need extensions or alterations which could be
 
99
    incompatible with other subsystems, you should create a local object
 
100
    inheriting from ``ir.qweb`` and customize that.
 
101
    """
 
102
 
 
103
    _name = 'ir.qweb'
 
104
 
 
105
    node = xml.dom.Node
 
106
    _void_elements = frozenset([
 
107
        'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
 
108
        'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
 
109
    _format_regex = re.compile(
 
110
        '(?:'
 
111
            # ruby-style pattern
 
112
            '#\{(.+?)\}'
 
113
        ')|(?:'
 
114
            # jinja-style pattern
 
115
            '\{\{(.+?)\}\}'
 
116
        ')')
 
117
 
 
118
    def __init__(self, pool, cr):
 
119
        super(QWeb, self).__init__(pool, cr)
 
120
 
 
121
        self._render_tag = self.prefixed_methods('render_tag_')
 
122
        self._render_att = self.prefixed_methods('render_att_')
 
123
 
 
124
    def prefixed_methods(self, prefix):
 
125
        """ Extracts all methods prefixed by ``prefix``, and returns a mapping
 
126
        of (t-name, method) where the t-name is the method name with prefix
 
127
        removed and underscore converted to dashes
 
128
 
 
129
        :param str prefix:
 
130
        :return: dict
 
131
        """
 
132
        n_prefix = len(prefix)
 
133
        return dict(
 
134
            (name[n_prefix:].replace('_', '-'), getattr(type(self), name))
 
135
            for name in dir(self)
 
136
            if name.startswith(prefix)
 
137
        )
 
138
 
 
139
    def register_tag(self, tag, func):
 
140
        self._render_tag[tag] = func
 
141
 
 
142
    def add_template(self, qwebcontext, name, node):
 
143
        """Add a parsed template in the context. Used to preprocess templates."""
 
144
        qwebcontext.templates[name] = node
 
145
 
 
146
    def load_document(self, document, qwebcontext):
 
147
        """
 
148
        Loads an XML document and installs any contained template in the engine
 
149
        """
 
150
        if hasattr(document, 'documentElement'):
 
151
            dom = document
 
152
        elif document.startswith("<?xml"):
 
153
            dom = xml.dom.minidom.parseString(document)
 
154
        else:
 
155
            dom = xml.dom.minidom.parse(document)
 
156
 
 
157
        for node in dom.documentElement.childNodes:
 
158
            if node.nodeType == self.node.ELEMENT_NODE and node.getAttribute('t-name'):
 
159
                name = str(node.getAttribute("t-name"))
 
160
                self.add_template(qwebcontext, name, node)
 
161
 
 
162
    def get_template(self, name, qwebcontext):
 
163
        origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
 
164
        if qwebcontext.loader and name not in qwebcontext.templates:
 
165
            try:
 
166
                xml_doc = qwebcontext.loader(name)
 
167
            except ValueError:
 
168
                raise convert_to_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
 
169
            self.load_document(xml_doc, qwebcontext=qwebcontext)
 
170
 
 
171
        if name in qwebcontext.templates:
 
172
            return qwebcontext.templates[name]
 
173
 
 
174
        raise convert_to_qweb_exception(QWebTemplateNotFound, message="Template %r not found" % name, template=origin_template)
 
175
 
 
176
    def eval(self, expr, qwebcontext):
 
177
        try:
 
178
            return qwebcontext.safe_eval(expr)
 
179
        except Exception:
 
180
            template = qwebcontext.get('__template__')
 
181
            raise convert_to_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
 
182
 
 
183
    def eval_object(self, expr, qwebcontext):
 
184
        return self.eval(expr, qwebcontext)
 
185
 
 
186
    def eval_str(self, expr, qwebcontext):
 
187
        if expr == "0":
 
188
            return qwebcontext.get(0, '')
 
189
        val = self.eval(expr, qwebcontext)
 
190
        if isinstance(val, unicode):
 
191
            return val.encode("utf8")
 
192
        if val is False or val is None:
 
193
            return ''
 
194
        return str(val)
 
195
 
 
196
    def eval_format(self, expr, qwebcontext):
 
197
        expr, replacements = self._format_regex.subn(
 
198
            lambda m: self.eval_str(m.group(1) or m.group(2), qwebcontext),
 
199
            expr
 
200
        )
 
201
 
 
202
        if replacements:
 
203
            return expr
 
204
 
 
205
        try:
 
206
            return str(expr % qwebcontext)
 
207
        except Exception:
 
208
            template = qwebcontext.get('__template__')
 
209
            raise convert_to_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
 
210
 
 
211
    def eval_bool(self, expr, qwebcontext):
 
212
        return int(bool(self.eval(expr, qwebcontext)))
 
213
 
 
214
    def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
 
215
        if qwebcontext is None:
 
216
            qwebcontext = {}
 
217
 
 
218
        if not isinstance(qwebcontext, QWebContext):
 
219
            qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
 
220
 
 
221
        qwebcontext['__template__'] = id_or_xml_id
 
222
        stack = qwebcontext.get('__stack__', [])
 
223
        if stack:
 
224
            qwebcontext['__caller__'] = stack[-1]
 
225
        stack.append(id_or_xml_id)
 
226
        qwebcontext['__stack__'] = stack
 
227
        qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
 
228
        return self.render_node(self.get_template(id_or_xml_id, qwebcontext), qwebcontext)
 
229
 
 
230
    def render_node(self, element, qwebcontext):
 
231
        result = ""
 
232
        if element.nodeType == self.node.TEXT_NODE or element.nodeType == self.node.CDATA_SECTION_NODE:
 
233
            result = element.data.encode("utf8")
 
234
        elif element.nodeType == self.node.ELEMENT_NODE:
 
235
            generated_attributes = ""
 
236
            t_render = None
 
237
            template_attributes = {}
 
238
            for (attribute_name, attribute_value) in element.attributes.items():
 
239
                attribute_name = str(attribute_name)
 
240
                if attribute_name == "groups":
 
241
                    cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
 
242
                    uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
 
243
                    can_see = self.user_has_groups(cr, uid, groups=attribute_value)
 
244
                    if not can_see:
 
245
                        return ''
 
246
                    continue
 
247
 
 
248
                if isinstance(attribute_value, unicode):
 
249
                    attribute_value = attribute_value.encode("utf8")
 
250
                else:
 
251
                    attribute_value = attribute_value.nodeValue.encode("utf8")
 
252
 
 
253
                if attribute_name.startswith("t-"):
 
254
                    for attribute in self._render_att:
 
255
                        if attribute_name[2:].startswith(attribute):
 
256
                            att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext)
 
257
                            generated_attributes += val and ' %s="%s"' % (att, werkzeug.utils.escape(val)) or " "
 
258
                            break
 
259
                    else:
 
260
                        if attribute_name[2:] in self._render_tag:
 
261
                            t_render = attribute_name[2:]
 
262
                        template_attributes[attribute_name[2:]] = attribute_value
 
263
                else:
 
264
                    generated_attributes += ' %s="%s"' % (attribute_name, werkzeug.utils.escape(attribute_value))
 
265
 
 
266
            if 'debug' in template_attributes:
 
267
                debugger = template_attributes.get('debug', 'pdb')
 
268
                __import__(debugger).set_trace()  # pdb, ipdb, pudb, ...
 
269
            if t_render:
 
270
                result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
 
271
            else:
 
272
                result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
 
273
        if isinstance(result, unicode):
 
274
            return result.encode('utf-8')
 
275
        return result
 
276
 
 
277
    def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
 
278
        # element: element
 
279
        # template_attributes: t-* attributes
 
280
        # generated_attributes: generated attributes
 
281
        # qwebcontext: values
 
282
        # inner: optional innerXml
 
283
        if inner:
 
284
            g_inner = inner
 
285
        else:
 
286
            g_inner = []
 
287
            for current_node in element.childNodes:
 
288
                try:
 
289
                    g_inner.append(self.render_node(current_node, qwebcontext))
 
290
                except QWebException:
 
291
                    raise
 
292
                except Exception:
 
293
                    template = qwebcontext.get('__template__')
 
294
                    raise convert_to_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template)
 
295
        name = str(element.nodeName)
 
296
        inner = "".join(g_inner)
 
297
        trim = template_attributes.get("trim", 0)
 
298
        if trim == 0:
 
299
            pass
 
300
        elif trim == 'left':
 
301
            inner = inner.lstrip()
 
302
        elif trim == 'right':
 
303
            inner = inner.rstrip()
 
304
        elif trim == 'both':
 
305
            inner = inner.strip()
 
306
        if name == "t":
 
307
            return inner
 
308
        elif len(inner) or name not in self._void_elements:
 
309
            return "<%s%s>%s</%s>" % tuple(
 
310
                qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
 
311
                for qwebcontext in (name, generated_attributes, inner, name)
 
312
            )
 
313
        else:
 
314
            return "<%s%s/>" % (name, generated_attributes)
 
315
 
 
316
    # Attributes
 
317
    def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
 
318
        if attribute_name.startswith("t-attf-"):
 
319
            att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext)
 
320
        elif attribute_name.startswith("t-att-"):
 
321
            att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext)
 
322
            if isinstance(val, unicode):
 
323
                val = val.encode("utf8")
 
324
        else:
 
325
            att, val = self.eval_object(attribute_value, qwebcontext)
 
326
        return att, val
 
327
 
 
328
    # Tags
 
329
    def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
 
330
        inner = self.eval_str(template_attributes["raw"], qwebcontext)
 
331
        return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
 
332
 
 
333
    def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
 
334
        inner = werkzeug.utils.escape(self.eval_str(template_attributes["esc"], qwebcontext))
 
335
        return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
 
336
 
 
337
    def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
 
338
        expr = template_attributes["foreach"]
 
339
        enum = self.eval_object(expr, qwebcontext)
 
340
        if enum is not None:
 
341
            var = template_attributes.get('as', expr).replace('.', '_')
 
342
            copy_qwebcontext = qwebcontext.copy()
 
343
            size = -1
 
344
            if isinstance(enum, (list, tuple)):
 
345
                size = len(enum)
 
346
            elif hasattr(enum, 'count'):
 
347
                size = enum.count()
 
348
            copy_qwebcontext["%s_size" % var] = size
 
349
            copy_qwebcontext["%s_all" % var] = enum
 
350
            index = 0
 
351
            ru = []
 
352
            for i in enum:
 
353
                copy_qwebcontext["%s_value" % var] = i
 
354
                copy_qwebcontext["%s_index" % var] = index
 
355
                copy_qwebcontext["%s_first" % var] = index == 0
 
356
                copy_qwebcontext["%s_even" % var] = index % 2
 
357
                copy_qwebcontext["%s_odd" % var] = (index + 1) % 2
 
358
                copy_qwebcontext["%s_last" % var] = index + 1 == size
 
359
                if index % 2:
 
360
                    copy_qwebcontext["%s_parity" % var] = 'odd'
 
361
                else:
 
362
                    copy_qwebcontext["%s_parity" % var] = 'even'
 
363
                if 'as' in template_attributes:
 
364
                    copy_qwebcontext[var] = i
 
365
                elif isinstance(i, dict):
 
366
                    copy_qwebcontext.update(i)
 
367
                ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
 
368
                index += 1
 
369
            return "".join(ru)
 
370
        else:
 
371
            template = qwebcontext.get('__template__')
 
372
            raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
 
373
 
 
374
    def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
 
375
        if self.eval_bool(template_attributes["if"], qwebcontext):
 
376
            return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
 
377
        return ""
 
378
 
 
379
    def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
 
380
        d = qwebcontext.copy()
 
381
        d[0] = self.render_element(element, template_attributes, generated_attributes, d)
 
382
        cr = d.get('request') and d['request'].cr or None
 
383
        uid = d.get('request') and d['request'].uid or None
 
384
 
 
385
        return self.render(cr, uid, self.eval_format(template_attributes["call"], d), d)
 
386
 
 
387
    def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
 
388
        if "value" in template_attributes:
 
389
            qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
 
390
        elif "valuef" in template_attributes:
 
391
            qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
 
392
        else:
 
393
            qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
 
394
        return ""
 
395
 
 
396
    def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
 
397
        """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
 
398
        node_name = element.nodeName
 
399
        assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
 
400
                                 "li", "ul", "ol", "dl", "dt", "dd"),\
 
401
            "RTE widgets do not work correctly on %r elements" % node_name
 
402
        assert node_name != 't',\
 
403
            "t-field can not be used on a t element, provide an actual HTML node"
 
404
 
 
405
        record, field_name = template_attributes["field"].rsplit('.', 1)
 
406
        record = self.eval_object(record, qwebcontext)
 
407
 
 
408
        column = record._model._all_columns[field_name].column
 
409
        options = json.loads(template_attributes.get('field-options') or '{}')
 
410
        field_type = get_field_type(column, options)
 
411
 
 
412
        converter = self.get_converter_for(field_type)
 
413
 
 
414
        return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
 
415
                                 element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
 
416
 
 
417
    def get_converter_for(self, field_type):
 
418
        return self.pool.get('ir.qweb.field.' + field_type,
 
419
                             self.pool['ir.qweb.field'])
 
420
 
 
421
#--------------------------------------------------------------------
 
422
# QWeb Fields converters
 
423
#--------------------------------------------------------------------
 
424
 
 
425
class FieldConverter(osv.AbstractModel):
 
426
    """ Used to convert a t-field specification into an output HTML field.
 
427
 
 
428
    :meth:`~.to_html` is the entry point of this conversion from QWeb, it:
 
429
 
 
430
    * converts the record value to html using :meth:`~.record_to_html`
 
431
    * generates the metadata attributes (``data-oe-``) to set on the root
 
432
      result node
 
433
    * generates the root result node itself through :meth:`~.render_element`
 
434
    """
 
435
    _name = 'ir.qweb.field'
 
436
 
 
437
    def attributes(self, cr, uid, field_name, record, options,
 
438
                   source_element, g_att, t_att, qweb_context,
 
439
                   context=None):
 
440
        """
 
441
        Generates the metadata attributes (prefixed by ``data-oe-`` for the
 
442
        root node of the field conversion. Attribute values are escaped by the
 
443
        parent using ``werkzeug.utils.escape``.
 
444
 
 
445
        The default attributes are:
 
446
 
 
447
        * ``model``, the name of the record's model
 
448
        * ``id`` the id of the record to which the field belongs
 
449
        * ``field`` the name of the converted field
 
450
        * ``type`` the logical field type (widget, may not match the column's
 
451
          ``type``, may not be any _column subclass name)
 
452
        * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
 
453
          column is translatable
 
454
        * ``expression``, the original expression
 
455
 
 
456
        :returns: iterable of (attribute name, attribute value) pairs.
 
457
        """
 
458
        column = record._model._all_columns[field_name].column
 
459
        field_type = get_field_type(column, options)
 
460
        return [
 
461
            ('data-oe-model', record._model._name),
 
462
            ('data-oe-id', record.id),
 
463
            ('data-oe-field', field_name),
 
464
            ('data-oe-type', field_type),
 
465
            ('data-oe-expression', t_att['field']),
 
466
        ]
 
467
 
 
468
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
469
        """ Converts a single value to its HTML version/output
 
470
        """
 
471
        if not value: return ''
 
472
        return value
 
473
 
 
474
    def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
 
475
        """ Converts the specified field of the browse_record ``record`` to
 
476
        HTML
 
477
        """
 
478
        return self.value_to_html(
 
479
            cr, uid, record[field_name], column, options=options, context=context)
 
480
 
 
481
    def to_html(self, cr, uid, field_name, record, options,
 
482
                source_element, t_att, g_att, qweb_context, context=None):
 
483
        """ Converts a ``t-field`` to its HTML output. A ``t-field`` may be
 
484
        extended by a ``t-field-options``, which is a JSON-serialized mapping
 
485
        of configuration values.
 
486
 
 
487
        A default configuration key is ``widget`` which can override the
 
488
        field's own ``_type``.
 
489
        """
 
490
        content = None
 
491
        try:
 
492
            content = self.record_to_html(
 
493
                cr, uid, field_name, record,
 
494
                record._model._all_columns[field_name].column,
 
495
                options, context=context)
 
496
            if options.get('html-escape', True):
 
497
                content = werkzeug.utils.escape(content)
 
498
            elif hasattr(content, '__html__'):
 
499
                content = content.__html__()
 
500
        except Exception:
 
501
            _logger.warning("Could not get field %s for model %s",
 
502
                            field_name, record._model._name, exc_info=True)
 
503
            content = None
 
504
 
 
505
        g_att += ''.join(
 
506
            ' %s="%s"' % (name, werkzeug.utils.escape(value))
 
507
            for name, value in self.attributes(
 
508
                cr, uid, field_name, record, options,
 
509
                source_element, g_att, t_att, qweb_context)
 
510
        )
 
511
 
 
512
        return self.render_element(cr, uid, source_element, t_att, g_att,
 
513
                                   qweb_context, content)
 
514
 
 
515
    def qweb_object(self):
 
516
        return self.pool['ir.qweb']
 
517
 
 
518
    def render_element(self, cr, uid, source_element, t_att, g_att,
 
519
                       qweb_context, content):
 
520
        """ Final rendering hook, by default just calls ir.qweb's ``render_element``
 
521
        """
 
522
        return self.qweb_object().render_element(
 
523
            source_element, t_att, g_att, qweb_context, content or '')
 
524
 
 
525
    def user_lang(self, cr, uid, context):
 
526
        """
 
527
        Fetches the res.lang object corresponding to the language code stored
 
528
        in the user's context. Fallbacks to en_US if no lang is present in the
 
529
        context *or the language code is not valid*.
 
530
 
 
531
        :returns: res.lang browse_record
 
532
        """
 
533
        if context is None: context = {}
 
534
 
 
535
        lang_code = context.get('lang') or 'en_US'
 
536
        Lang = self.pool['res.lang']
 
537
 
 
538
        lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
 
539
               or  Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
 
540
 
 
541
        return Lang.browse(cr, uid, lang_ids[0], context=context)
 
542
 
 
543
class FloatConverter(osv.AbstractModel):
 
544
    _name = 'ir.qweb.field.float'
 
545
    _inherit = 'ir.qweb.field'
 
546
 
 
547
    def precision(self, cr, uid, column, options=None, context=None):
 
548
        _, precision = column.digits or (None, None)
 
549
        return precision
 
550
 
 
551
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
552
        if context is None:
 
553
            context = {}
 
554
        precision = self.precision(cr, uid, column, options=options, context=context)
 
555
        fmt = '%f' if precision is None else '%.{precision}f'
 
556
 
 
557
        lang_code = context.get('lang') or 'en_US'
 
558
        lang = self.pool['res.lang']
 
559
        formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
 
560
 
 
561
        # %f does not strip trailing zeroes. %g does but its precision causes
 
562
        # it to switch to scientific notation starting at a million *and* to
 
563
        # strip decimals. So use %f and if no precision was specified manually
 
564
        # strip trailing 0.
 
565
        if not precision:
 
566
            formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
 
567
        return formatted
 
568
 
 
569
class DateConverter(osv.AbstractModel):
 
570
    _name = 'ir.qweb.field.date'
 
571
    _inherit = 'ir.qweb.field'
 
572
 
 
573
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
574
        if not value: return ''
 
575
        lang = self.user_lang(cr, uid, context=context)
 
576
        locale = babel.Locale.parse(lang.code)
 
577
 
 
578
        if isinstance(value, basestring):
 
579
            value = datetime.datetime.strptime(
 
580
                value, openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
 
581
 
 
582
        if options and 'format' in options:
 
583
            pattern = options['format']
 
584
        else:
 
585
            strftime_pattern = lang.date_format
 
586
            pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
 
587
 
 
588
        return babel.dates.format_datetime(
 
589
            value, format=pattern,
 
590
            locale=locale)
 
591
 
 
592
class DateTimeConverter(osv.AbstractModel):
 
593
    _name = 'ir.qweb.field.datetime'
 
594
    _inherit = 'ir.qweb.field'
 
595
 
 
596
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
597
        if not value: return ''
 
598
        lang = self.user_lang(cr, uid, context=context)
 
599
        locale = babel.Locale.parse(lang.code)
 
600
 
 
601
        if isinstance(value, basestring):
 
602
            value = datetime.datetime.strptime(
 
603
                value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
 
604
        value = column.context_timestamp(
 
605
            cr, uid, timestamp=value, context=context)
 
606
 
 
607
        if options and 'format' in options:
 
608
            pattern = options['format']
 
609
        else:
 
610
            strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
 
611
            pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
 
612
 
 
613
        return babel.dates.format_datetime(value, format=pattern, locale=locale)
 
614
 
 
615
class TextConverter(osv.AbstractModel):
 
616
    _name = 'ir.qweb.field.text'
 
617
    _inherit = 'ir.qweb.field'
 
618
 
 
619
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
620
        """
 
621
        Escapes the value and converts newlines to br. This is bullshit.
 
622
        """
 
623
        if not value: return ''
 
624
 
 
625
        return nl2br(value, options=options)
 
626
 
 
627
class SelectionConverter(osv.AbstractModel):
 
628
    _name = 'ir.qweb.field.selection'
 
629
    _inherit = 'ir.qweb.field'
 
630
 
 
631
    def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
 
632
        value = record[field_name]
 
633
        if not value: return ''
 
634
        selection = dict(fields.selection.reify(
 
635
            cr, uid, record._model, column))
 
636
        return self.value_to_html(
 
637
            cr, uid, selection[value], column, options=options)
 
638
 
 
639
class ManyToOneConverter(osv.AbstractModel):
 
640
    _name = 'ir.qweb.field.many2one'
 
641
    _inherit = 'ir.qweb.field'
 
642
 
 
643
    def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
 
644
        [read] = record.read([field_name])
 
645
        if not read[field_name]: return ''
 
646
        _, value = read[field_name]
 
647
        return nl2br(value, options=options)
 
648
 
 
649
class HTMLConverter(osv.AbstractModel):
 
650
    _name = 'ir.qweb.field.html'
 
651
    _inherit = 'ir.qweb.field'
 
652
 
 
653
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
654
        return HTMLSafe(value or '')
 
655
 
 
656
class ImageConverter(osv.AbstractModel):
 
657
    """ ``image`` widget rendering, inserts a data:uri-using image tag in the
 
658
    document. May be overridden by e.g. the website module to generate links
 
659
    instead.
 
660
 
 
661
    .. todo:: what happens if different output need different converters? e.g.
 
662
              reports may need embedded images or FS links whereas website
 
663
              needs website-aware
 
664
    """
 
665
    _name = 'ir.qweb.field.image'
 
666
    _inherit = 'ir.qweb.field'
 
667
 
 
668
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
669
        try:
 
670
            image = Image.open(cStringIO.StringIO(value.decode('base64')))
 
671
            image.verify()
 
672
        except IOError:
 
673
            raise ValueError("Non-image binary fields can not be converted to HTML")
 
674
        except: # image.verify() throws "suitable exceptions", I have no idea what they are
 
675
            raise ValueError("Invalid image content")
 
676
 
 
677
        return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
 
678
 
 
679
class MonetaryConverter(osv.AbstractModel):
 
680
    """ ``monetary`` converter, has a mandatory option
 
681
    ``display_currency``.
 
682
 
 
683
    The currency is used for formatting *and rounding* of the float value. It
 
684
    is assumed that the linked res_currency has a non-empty rounding value and
 
685
    res.currency's ``round`` method is used to perform rounding.
 
686
 
 
687
    .. note:: the monetary converter internally adds the qweb context to its
 
688
              options mapping, so that the context is available to callees.
 
689
              It's set under the ``_qweb_context`` key.
 
690
    """
 
691
    _name = 'ir.qweb.field.monetary'
 
692
    _inherit = 'ir.qweb.field'
 
693
 
 
694
    def to_html(self, cr, uid, field_name, record, options,
 
695
                source_element, t_att, g_att, qweb_context, context=None):
 
696
        options['_qweb_context'] = qweb_context
 
697
        return super(MonetaryConverter, self).to_html(
 
698
            cr, uid, field_name, record, options,
 
699
            source_element, t_att, g_att, qweb_context, context=context)
 
700
 
 
701
    def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
 
702
        if context is None:
 
703
            context = {}
 
704
        Currency = self.pool['res.currency']
 
705
        display = self.display_currency(cr, uid, options)
 
706
 
 
707
        # lang.format mandates a sprintf-style format. These formats are non-
 
708
        # minimal (they have a default fixed precision instead), and
 
709
        # lang.format will not set one by default. currency.round will not
 
710
        # provide one either. So we need to generate a precision value
 
711
        # (integer > 0) from the currency's rounding (a float generally < 1.0).
 
712
        #
 
713
        # The log10 of the rounding should be the number of digits involved if
 
714
        # negative, if positive clamp to 0 digits and call it a day.
 
715
        # nb: int() ~ floor(), we want nearest rounding instead
 
716
        precision = int(round(math.log10(display.rounding)))
 
717
        fmt = "%.{0}f".format(-precision if precision < 0 else 0)
 
718
 
 
719
        lang_code = context.get('lang') or 'en_US'
 
720
        lang = self.pool['res.lang']
 
721
        formatted_amount = lang.format(cr, uid, [lang_code], 
 
722
            fmt, Currency.round(cr, uid, display, record[field_name]),
 
723
            grouping=True, monetary=True)
 
724
 
 
725
        pre = post = u''
 
726
        if display.position == 'before':
 
727
            pre = u'{symbol} '
 
728
        else:
 
729
            post = u' {symbol}'
 
730
 
 
731
        return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
 
732
            formatted_amount,
 
733
            pre=pre, post=post,
 
734
        ).format(
 
735
            symbol=display.symbol,
 
736
        ))
 
737
 
 
738
    def display_currency(self, cr, uid, options):
 
739
        return self.qweb_object().eval_object(
 
740
            options['display_currency'], options['_qweb_context'])
 
741
 
 
742
TIMEDELTA_UNITS = (
 
743
    ('year',   3600 * 24 * 365),
 
744
    ('month',  3600 * 24 * 30),
 
745
    ('week',   3600 * 24 * 7),
 
746
    ('day',    3600 * 24),
 
747
    ('hour',   3600),
 
748
    ('minute', 60),
 
749
    ('second', 1)
 
750
)
 
751
class DurationConverter(osv.AbstractModel):
 
752
    """ ``duration`` converter, to display integral or fractional values as
 
753
    human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
 
754
 
 
755
    Can be used on any numerical field.
 
756
 
 
757
    Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
 
758
    ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
 
759
    field value before converting it.
 
760
 
 
761
    Sub-second values will be ignored.
 
762
    """
 
763
    _name = 'ir.qweb.field.duration'
 
764
    _inherit = 'ir.qweb.field'
 
765
 
 
766
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
767
        units = dict(TIMEDELTA_UNITS)
 
768
        if value < 0:
 
769
            raise ValueError(_("Durations can't be negative"))
 
770
        if not options or options.get('unit') not in units:
 
771
            raise ValueError(_("A unit must be provided to duration widgets"))
 
772
 
 
773
        locale = babel.Locale.parse(
 
774
            self.user_lang(cr, uid, context=context).code)
 
775
        factor = units[options['unit']]
 
776
 
 
777
        sections = []
 
778
        r = value * factor
 
779
        for unit, secs_per_unit in TIMEDELTA_UNITS:
 
780
            v, r = divmod(r, secs_per_unit)
 
781
            if not v: continue
 
782
            section = babel.dates.format_timedelta(
 
783
                v*secs_per_unit, threshold=1, locale=locale)
 
784
            if section:
 
785
                sections.append(section)
 
786
        return u' '.join(sections)
 
787
 
 
788
class RelativeDatetimeConverter(osv.AbstractModel):
 
789
    _name = 'ir.qweb.field.relative'
 
790
    _inherit = 'ir.qweb.field'
 
791
 
 
792
    def value_to_html(self, cr, uid, value, column, options=None, context=None):
 
793
        parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
 
794
        locale = babel.Locale.parse(
 
795
            self.user_lang(cr, uid, context=context).code)
 
796
 
 
797
        if isinstance(value, basestring):
 
798
            value = datetime.datetime.strptime(value, parse_format)
 
799
 
 
800
        # value should be a naive datetime in UTC. So is fields.datetime.now()
 
801
        reference = datetime.datetime.strptime(column.now(), parse_format)
 
802
 
 
803
        return babel.dates.format_timedelta(
 
804
            value - reference, add_direction=True, locale=locale)
 
805
 
 
806
class HTMLSafe(object):
 
807
    """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
 
808
    objects with a ``__html__`` methods but AFAIK does not provide any such
 
809
    object.
 
810
 
 
811
    Wrapping a string in HTML will prevent its escaping
 
812
    """
 
813
    __slots__ = ['string']
 
814
    def __init__(self, string):
 
815
        self.string = string
 
816
    def __html__(self):
 
817
        return self.string
 
818
    def __str__(self):
 
819
        s = self.string
 
820
        if isinstance(s, unicode):
 
821
            return s.encode('utf-8')
 
822
        return s
 
823
    def __unicode__(self):
 
824
        s = self.string
 
825
        if isinstance(s, str):
 
826
            return s.decode('utf-8')
 
827
        return s
 
828
 
 
829
def nl2br(string, options=None):
 
830
    """ Converts newlines to HTML linebreaks in ``string``. Automatically
 
831
    escapes content unless options['html-escape'] is set to False, and returns
 
832
    the result wrapped in an HTMLSafe object.
 
833
 
 
834
    :param str string:
 
835
    :param dict options:
 
836
    :rtype: HTMLSafe
 
837
    """
 
838
    if options is None: options = {}
 
839
 
 
840
    if options.get('html-escape', True):
 
841
        string = werkzeug.utils.escape(string)
 
842
    return HTMLSafe(string.replace('\n', '<br>\n'))
 
843
 
 
844
def get_field_type(column, options):
 
845
    """ Gets a t-field's effective type from the field's column and its options
 
846
    """
 
847
    return options.get('widget', column._type)
 
848
 
 
849
# vim:et: