diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py
index 302b11eb526..08264175d7b 100644
--- a/openerp/addons/base/ir/ir_fields.py
+++ b/openerp/addons/base/ir/ir_fields.py
@@ -1,13 +1,18 @@
# -*- coding: utf-8 -*-
+import cStringIO
import datetime
import functools
import operator
import itertools
import time
+import Image
import psycopg2
import pytz
+import werkzeug.utils
+import openerp.osv.fields
+import openerp.tools.func
from openerp.osv import orm
from openerp.tools.translate import _
from openerp.tools.misc import DEFAULT_SERVER_DATE_FORMAT,\
@@ -141,6 +146,30 @@ class ir_fields_converter(orm.Model):
return functools.partial(
converter, cr, uid, model, column, context=context)
+ def from_field(self, cr, uid, model, column, totype='str', context=None):
+ """ Fetches a converter for the provided column object, to the
+ specified type.
+
+ A converter is a callable taking a value as returned by a read on the
+ column, and returning a formatted value of type ``totype``.
+
+ By default, tries to get a method on itself with a name matching the
+ pattern ``_$totype_from_${column._type}`` and returns it.
+
+ Error conditions are similar to :meth:`~.to_field`
+
+ :param column: column object to generate a value from
+ :param str totype:
+ :return: a function (column.read_type -> totype)
+ :rtype: Callable | None
+ """
+ converter = getattr(
+ self, '_%s_from_%s' % (totype, column._type), None)
+ if not converter: return None
+
+ return functools.partial(
+ converter, cr, uid, model, column, context=context)
+
def _str_to_boolean(self, cr, uid, model, column, value, context=None):
# all translatables used for booleans
true, yes, false, no = _(u"true"), _(u"yes"), _(u"false"), _(u"no")
@@ -428,3 +457,43 @@ class ir_fields_converter(orm.Model):
commands.append(CREATE(writable))
return commands, warnings
+
+ def _html_from_passthrough(self, cr, uid, model, column, value, context=None):
+ """
+ escapes the value and returns it directly
+ """
+ return werkzeug.utils.escape(value), []
+
+ _html_from_char = _html_from_integer = _html_from_float = \
+ _html_from_date = _html_from_datetime = _html_from_passthrough
+
+ def _html_from_text(self, cr, uid, model, column, value, context=None):
+ """
+ Escapes the value and converts newlines to br. This is bullshit.
+ """
+ return werkzeug.utils.escape(value).replace('\n', '
\n'), []
+
+ def _html_from_selection(self, cr, uid, model, column, value, context=None):
+ selection = dict(openerp.osv.fields.selection.reify(
+ cr, uid, model, column, context=context))
+ return werkzeug.utils.escape(selection[value]), []
+
+ def _html_from_html(self, cr, uid, model, column, value, context=None):
+ return value, []
+
+ def _html_from_many2one(self, cr, uid, model, column, value, context=None):
+ return werkzeug.utils.escape(value.name_get()[0][1]), []
+
+ def _html_from_binary(self, cr, uid, model, column, value, context=None):
+ try:
+ image = Image.open(cStringIO.StringIO(value.decode('base64')))
+ except IOError:
+ raise ValueError("Non-image binary fields can not be converted to HTML")
+
+ mime = PIL_MIME_MAPPING.get(image.format)
+ if mime is None:
+ raise ValueError("Unknown PIL format %s" % image.format)
+
+ return ('
' % (mime, value)), []
+
+PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', }
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index 3f0642c9229..1db2d1a111d 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -37,6 +37,7 @@
import base64
import datetime as DT
+import functools
import logging
import pytz
import re
@@ -449,6 +450,39 @@ class selection(_column):
_column.__init__(self, string=string, **args)
self.selection = selection
+ @classmethod
+ def reify(cls, cr, uid, model, field, context=None):
+ """ Munges the field's ``selection`` attribute as necessary to get
+ something useable out of it: calls it if it's a function, applies
+ translations to labels if it's not.
+
+ A callable ``selection`` is considered translated on its own.
+
+ :param orm.Model model:
+ :param _column field:
+ """
+ if callable(field.selection):
+ return field.selection(model, cr, uid, context)
+
+ if not (context and 'lang' in context):
+ return field.selection
+
+ # field_to_dict isn't given a field name, only a field object, we
+ # need to get the name back in order to perform the translation lookup
+ field_name = next(
+ name for name, column in model._columns.iteritems()
+ if column == field)
+
+ translation_filter = "%s,%s" % (model._name, field_name)
+ translate = functools.partial(
+ (model.pool['ir.translation'])._get_source,
+ cr, uid, translation_filter, 'selection', context['lang'])
+
+ return [
+ (value, translate(source=label))
+ for value, label in field.selection
+ ]
+
# ---------------------------------------------------------
# Relationals fields
# ---------------------------------------------------------
@@ -1553,11 +1587,7 @@ def field_to_dict(model, cr, user, field, context=None):
res[arg] = getattr(field, arg)
if hasattr(field, 'selection'):
- if isinstance(field.selection, (tuple, list)):
- res['selection'] = field.selection
- else:
- # call the 'dynamic selection' function
- res['selection'] = field.selection(model, cr, user, context)
+ res['selection'] = selection.reify(cr, user, model, field, context=context)
if res['type'] in ('one2many', 'many2many', 'many2one'):
res['relation'] = field._obj
res['domain'] = field._domain(model) if callable(field._domain) else field._domain
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index 4bb53e75bd7..6ee2fbbd7cb 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -3083,16 +3083,6 @@ class BaseModel(object):
help_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'help', context['lang'])
if help_trans:
res[f]['help'] = help_trans
- if 'selection' in res[f]:
- if isinstance(field.selection, (tuple, list)):
- sel = field.selection
- sel2 = []
- for key, val in sel:
- val2 = None
- if val:
- val2 = translation_obj._get_source(cr, user, self._name + ',' + f, 'selection', context['lang'], val)
- sel2.append((key, val2 or val))
- res[f]['selection'] = sel2
return res
diff --git a/openerp/tests/addons/test_converter/__init__.py b/openerp/tests/addons/test_converter/__init__.py
new file mode 100644
index 00000000000..89d26e2f597
--- /dev/null
+++ b/openerp/tests/addons/test_converter/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+import models
diff --git a/openerp/tests/addons/test_converter/__openerp__.py b/openerp/tests/addons/test_converter/__openerp__.py
new file mode 100644
index 00000000000..c76b1fec875
--- /dev/null
+++ b/openerp/tests/addons/test_converter/__openerp__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'test-field-converter',
+ 'version': '0.1',
+ 'category': 'Tests',
+ 'description': """Tests of field conversions""",
+ 'author': 'OpenERP SA',
+ 'maintainer': 'OpenERP SA',
+ 'website': 'http://www.openerp.com',
+ 'depends': ['base'],
+ 'data': ['ir.model.access.csv'],
+ 'installable': True,
+ 'auto_install': False,
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_converter/ir.model.access.csv b/openerp/tests/addons/test_converter/ir.model.access.csv
new file mode 100644
index 00000000000..66937821c94
--- /dev/null
+++ b/openerp/tests/addons/test_converter/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_converter_model,access_converter_model,model_test_converter_test_model,,1,1,1,1
+access_test_converter_test_model_sub,access_test_converter_test_model_sub,model_test_converter_test_model_sub,,1,1,1,1
diff --git a/openerp/tests/addons/test_converter/models.py b/openerp/tests/addons/test_converter/models.py
new file mode 100644
index 00000000000..0f6ab3906c1
--- /dev/null
+++ b/openerp/tests/addons/test_converter/models.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+from openerp.osv import orm, fields
+
+class test_model(orm.Model):
+ _name = 'test_converter.test_model'
+
+ _columns = {
+ 'char': fields.char(),
+ 'integer': fields.integer(),
+ 'float': fields.float(),
+ 'many2one': fields.many2one('test_converter.test_model.sub'),
+ 'binary': fields.binary(),
+ 'date': fields.date(),
+ 'datetime': fields.datetime(),
+ 'selection': fields.selection([
+ (1, "réponse A"),
+ (2, "réponse B"),
+ (3, "réponse C"),
+ (4, "réponse D"),
+ ]),
+ 'selection_str': fields.selection([
+ ('A', "Qu'il n'est pas arrivé à Toronto"),
+ ('B', "Qu'il était supposé arriver à Toronto"),
+ ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"),
+ ('D', "La réponse D"),
+ ], string="Lorsqu'un pancake prend l'avion à destination de Toronto et "
+ "qu'il fait une escale technique à St Claude, on dit:"),
+ 'html': fields.html(),
+ 'text': fields.text(),
+ }
+
+class test_model_sub(orm.Model):
+ _name = 'test_converter.test_model.sub'
+
+ _columns = {
+ 'name': fields.char()
+ }
diff --git a/openerp/tests/addons/test_converter/tests/__init__.py b/openerp/tests/addons/test_converter/tests/__init__.py
new file mode 100644
index 00000000000..493b164d44a
--- /dev/null
+++ b/openerp/tests/addons/test_converter/tests/__init__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+from . import test_html
+
+fast_suite = [
+]
+
+checks = [
+ test_html
+]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_converter/tests/test_html.py b/openerp/tests/addons/test_converter/tests/test_html.py
new file mode 100644
index 00000000000..d11156db8dc
--- /dev/null
+++ b/openerp/tests/addons/test_converter/tests/test_html.py
@@ -0,0 +1,144 @@
+# -*- encoding: utf-8 -*-
+import base64
+import os
+
+from openerp.tests import common
+
+directory = os.path.dirname(__file__)
+
+class TestHTMLExport(common.TransactionCase):
+ def setUp(self):
+ super(TestHTMLExport, self).setUp()
+ self.Converter = self.registry('ir.fields.converter')
+ self.Model = self.registry('test_converter.test_model')
+
+ def get_column(self, name):
+ return self.Model._all_columns[name].column
+
+ def get_converter(self, name):
+ return self.Converter.from_field(
+ self.cr, self.uid, self.Model, self.get_column(name),
+ totype="html")
+
+ def test_char(self):
+ converter = self.get_converter('char')
+
+ value, warnings = converter('foo')
+ self.assertEqual(value, 'foo')
+ self.assertEqual(warnings, [])
+
+ value, warnings = converter("foo")
+ self.assertEqual(value, "foo<bar>")
+ self.assertEqual(warnings, [])
+
+ def test_integer(self):
+ converter = self.get_converter('integer')
+
+ value, warnings = converter(42)
+ self.assertEqual(value, "42")
+ self.assertEqual(warnings, [])
+
+ def test_float(self):
+ converter = self.get_converter('float')
+
+ value, warnings = converter(42.0)
+ self.assertEqual(value, "42.0")
+ self.assertEqual(warnings, [])
+
+ def test_text(self):
+ converter = self.get_converter('text')
+
+ value, warnings = converter("This is my text-kai")
+ self.assertEqual(value, "This is my text-kai")
+ self.assertEqual(warnings, [])
+
+ value, warnings = converter("""
+ . The current line (address) in the buffer.
+ $ The last line in the buffer.
+ n The nth, line in the buffer where n is a number in the range [0,$].
+ $ The last line in the buffer.
+ - The previous line. This is equivalent to -1 and may be repeated with cumulative effect.
+ -n The nth previous line, where n is a non-negative number.
+ + The next line. This is equivalent to +1 and may be repeated with cumulative effect.
+ """)
+ self.assertEqual(value, """
+ . The current line (address) in the buffer.
+ $ The last line in the buffer.
+ n The nth, line in the buffer where n is a number in the range [0,$].
+ $ The last line in the buffer.
+ - The previous line. This is equivalent to -1 and may be repeated with cumulative effect.
+ -n The nth previous line, where n is a non-negative number.
+ + The next line. This is equivalent to +1 and may be repeated with cumulative effect.
+ """)
+ self.assertEqual(warnings, [])
+
+ value, warnings = converter("""
+ fgdkls;hjas;lj fdslkj d;lasjfa lkdja lfks
+ fldkjsfhs fldskjh
+ """)
+ self.assertEqual(value, """
+ fgdkls;hjas;lj <b>fdslkj</b> d;lasjfa lkdja <a href=http://spam.com>lfks</a>
+ fldkjsfhs <i style="color: red"><a href="http://spamspam.com">fldskjh</a></i>
+ """)
+ self.assertEqual(warnings, [])
+
+ def test_many2one(self):
+ converter = self.get_converter('many2one')
+ Sub = self.registry('test_converter.test_model.sub')
+
+ id0 = Sub.create(self.cr, self.uid, {'name': "Foo"})
+ value, warnings = converter(Sub.browse(self.cr, self.uid, id0))
+ self.assertEqual(value, "Foo")
+ self.assertEqual(warnings, [])
+
+ id1 = Sub.create(self.cr, self.uid, {'name': "Foo"})
+ value, warnings = converter(Sub.browse(self.cr, self.uid, id1))
+ self.assertEqual(value, "Fo<b>o</b>")
+ self.assertEqual(warnings, [])
+
+ def test_binary(self):
+ converter = self.get_converter('binary')
+ with open(os.path.join(directory, 'test_vectors', 'image'), 'rb') as f:
+ content = f.read()
+
+ value, warnings = converter(content.encode('base64'))
+ self.assertEqual(
+ value, '
' % (
+ content.encode('base64')
+ ))
+ self.assertEqual(warnings, [])
+
+ with open(os.path.join(directory, 'test_vectors', 'pdf'), 'rb') as f:
+ content = f.read()
+
+ with self.assertRaises(ValueError):
+ converter(content.encode('base64'))
+
+ with open(os.path.join(directory, 'test_vectors', 'pptx'), 'rb') as f:
+ content = f.read()
+
+ with self.assertRaises(ValueError):
+ converter(content.encode('base64'))
+
+ def test_selection(self):
+ converter = self.get_converter('selection')
+
+ value, warnings = converter(2)
+ self.assertEqual(value, "réponse B")
+ self.assertEqual(warnings, [])
+
+ converter = self.get_converter('selection_str')
+
+ value, warnings = converter('C')
+ self.assertEqual(value, "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?")
+ self.assertEqual(warnings, [])
+
+ def test_html(self):
+ converter = self.get_converter('html')
+
+ input = 'span'
+ value, warnings = converter(input)
+ self.assertEqual(value, input)
+ self.assertEqual(warnings, [])
+ # o2m, m2m?
+ # reference?
diff --git a/openerp/tests/addons/test_converter/tests/test_vectors/image b/openerp/tests/addons/test_converter/tests/test_vectors/image
new file mode 100644
index 00000000000..889c9051721
Binary files /dev/null and b/openerp/tests/addons/test_converter/tests/test_vectors/image differ
diff --git a/openerp/tests/addons/test_converter/tests/test_vectors/pdf b/openerp/tests/addons/test_converter/tests/test_vectors/pdf
new file mode 100644
index 00000000000..6ae543d29d4
Binary files /dev/null and b/openerp/tests/addons/test_converter/tests/test_vectors/pdf differ
diff --git a/openerp/tests/addons/test_converter/tests/test_vectors/pptx b/openerp/tests/addons/test_converter/tests/test_vectors/pptx
new file mode 100644
index 00000000000..f798aca350b
Binary files /dev/null and b/openerp/tests/addons/test_converter/tests/test_vectors/pptx differ