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