From cd31b4af4db3059587d59d18d98e1e8b9eb36721 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 7 Oct 2013 11:15:56 +0200 Subject: [PATCH] [IMP] move field conversion around ir.fields architecture turns out not to be a very good or simple fit, especially as different widgets/fields need different granularity of customizations. Having objects dedicated to each field type/widget makes things simpler as it allows a conversion pipeline which can be plugged into at any point. bzr revid: xmo@openerp.com-20131007091556-03azc2mdhcb7kmwo --- openerp/addons/base/ir/ir_fields.py | 71 --------- openerp/addons/base/ir/ir_qweb.py | 149 +++++++++++++++--- openerp/addons/base/ir/ir_ui_view.py | 2 +- .../addons/base/security/ir.model.access.csv | 9 +- .../addons/test_converter/tests/test_html.py | 80 +++++----- openerp/tests/test_qweb.py | 11 +- 6 files changed, 176 insertions(+), 146 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 49ccf2ce91a..39f7f99b5fa 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -149,30 +149,6 @@ 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") @@ -460,50 +436,3 @@ 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_date = _html_from_datetime = _html_from_passthrough - - def _html_from_float(self, cr, uid, model, column, value, context=None): - width, precision = column.digits or (None, None) - if precision is None: - fmt = '{value}' - else: - fmt = '{value:.{precision}f}' - - return werkzeug.utils.escape( - fmt.format(value=value, width=width, precision=precision, )), [] - - 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]).replace('\n', '
\n'), [] - - 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") - try: image.verify() - except: # no idea what "suitable exceptions" are - raise ValueError("Invalid image content") - - return ('' % (Image.MIME[image.format], value)), [] diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 78b5f401d61..8d9bd97adf6 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- +import cStringIO +import json import logging import re +import Image import werkzeug.utils import xml # FIXME use lxml import traceback -from openerp.osv import osv, orm +from openerp.osv import osv, orm, fields _logger = logging.getLogger(__name__) @@ -73,7 +76,7 @@ class QWeb(orm.AbstractModel): """ - _name = 'ir.templating.qweb' + _name = 'ir.qweb' node = xml.dom.Node _void_elements = frozenset([ @@ -364,37 +367,139 @@ class QWeb(orm.AbstractModel): assert node_name != 't',\ "t-field can not be used on a t element, provide an actual HTML node" - record, field = t_att["field"].rsplit('.', 1) + record, field_name = t_att["field"].rsplit('.', 1) record = self.eval_object(record, v) - column = record._model._all_columns[field].column - field_type = column._type + column = record._model._all_columns[field_name].column + options = json.loads(t_att.get('field-options') or '{}') + field_type = get_field_type(column, options) - req = v['request'] - converter = req.registry['ir.fields.converter'].from_field( - req.cr, req.uid, record._model, column, totype='html') + converter = self.pool.get('ir.qweb.field.' + field_type, + self.pool['ir.qweb.field']) + return converter.to_html(record._cr, record._uid, field_name, record, options, + e, t_att, g_att, v) + + +class FieldConverter(osv.AbstractModel): + _name = 'ir.qweb.field' + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context): + column = record._model._all_columns[field_name].column + field_type = get_field_type(column, options) + return [ + ('data-oe-model', record._model._name), + ('data-oe-id', record.id), + ('data-oe-field', field_name), + ('data-oe-type', field_type), + ('data-oe-translate', '1' if column.translate else '0'), + ('data-oe-expression', t_att['field']), + ] + + def value_to_html(self, cr, uid, value, column, options=None): + """ Converts a single value to its HTML version/output + """ + return werkzeug.utils.escape(value) + + def record_to_html(self, cr, uid, field_name, record, column, options=None): + """ Converts the specified field of a ``record`` to HTML + """ + return self.value_to_html( + cr, uid, record[field_name], column, options=None) + + def to_html(self, cr, uid, field_name, record, options, + source_element, t_att, g_att, qweb_context): + """ Converts a ``t-field[t-field-options]?`` to its HTML output + """ content = None try: - value = record[field] - if value: - content, warnings = converter(value) - assert not warnings + content = self.record_to_html( + cr, uid, field_name, record, + record._model._all_columns[field_name].column, + options) except KeyError: - _logger.warning("t-field no field %s for model %s", field, record._model._name) + _logger.warning("t-field no field %s for model %s", field_name, record._model._name) g_att += ''.join( ' %s="%s"' % (name, werkzeug.utils.escape(value)) - for name, value in [ - ('data-oe-model', record._model._name), - ('data-oe-id', record.id), - ('data-oe-field', field), - ('data-oe-type', field_type), - ('data-oe-translate', '1' if column.translate else '0'), - ('data-oe-expression', t_att['field']), - ] + for name, value in self.attributes( + cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context) ) - return self.render_element(e, t_att, g_att, v, content or "") + return self.render_element(cr, uid, source_element, t_att, g_att, + qweb_context, content) + + def render_element(self, cr, uid, source_element, t_att, g_att, qweb_context, content): + return self.pool['ir.qweb'].render_element( + source_element, t_att, g_att, qweb_context, content or '') + +class FloatConverter(osv.AbstractModel): + _name = 'ir.qweb.field.float' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + width, precision = column.digits or (None, None) + fmt = '{value}' if precision is None else '{value:.{precision}f}' + + return werkzeug.utils.escape( + fmt.format(value=value, width=width, precision=precision, )) + +class TextConverter(osv.AbstractModel): + _name = 'ir.qweb.field.text' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + """ + Escapes the value and converts newlines to br. This is bullshit. + """ + return werkzeug.utils.escape(value).replace('\n', '
\n') + +class SelectionConverter(osv.AbstractModel): + _name = 'ir.qweb.field.selection' + _inherit = 'ir.qweb.field' + + def record_to_html(self, cr, uid, field_name, record, column, options=None): + # FIXME: context + value = record[field_name] + selection = dict(fields.selection.reify( + cr, uid, record._model, column)) + return self.value_to_html( + cr, uid, selection[value], column, options=options) + +class ManyToOneConverter(osv.AbstractModel): + _name = 'ir.qweb.field.many2one' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + return werkzeug.utils.escape(value.name_get()[0][1]).replace('\n', '
\n') + +class HTMLConverter(osv.AbstractModel): + _name = 'ir.qweb.field.html' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + return value + +class BinaryConverter(osv.AbstractModel): + _name = 'ir.qweb.field.binary' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + try: + image = Image.open(cStringIO.StringIO(value.decode('base64'))) + except IOError: + raise ValueError("Non-image binary fields can not be converted to HTML") + try: image.verify() + except: # no idea what "suitable exceptions" are + raise ValueError("Invalid image content") + + return ('' % (Image.MIME[image.format], value)) + +def get_field_type(column, options): + """ Gets a t-field's effective type from the field's column and its options + """ + return options.get('widget', column._type) # leave this, al. diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 2e0cd3eb541..3813221c7cf 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -771,7 +771,7 @@ class view(osv.osv): def loader(name): return self.read_template(cr, uid, name, context=context) - engine = self.pool['ir.templating.qweb'] + engine = self.pool['ir.qweb'] return engine.render( id_or_xml_id, values, loader=loader, undefined_handler=lambda key, v: None) diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index 62b68c67aab..8003579b2ae 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -110,4 +110,11 @@ "access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0 "access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1 "access_res_partner_public","res.partner","base.model_res_partner","base.group_public",1,0,0,0 - +access_ir_qweb,access_ir_qweb,model_ir_qweb,,0,0,0,0 +access_ir_qweb_field,access_ir_qweb_field,model_ir_qweb_field,,0,0,0,0 +access_ir_qweb_field_float,access_ir_qweb_field_float,model_ir_qweb_field_float,,0,0,0,0 +access_ir_qweb_field_text,access_ir_qweb_field_text,model_ir_qweb_field_text,,0,0,0,0 +access_ir_qweb_field_selection,access_ir_qweb_field_selection,model_ir_qweb_field_selection,,0,0,0,0 +access_ir_qweb_field_many2one,access_ir_qweb_field_many2one,model_ir_qweb_field_many2one,,0,0,0,0 +access_ir_qweb_field_html,access_ir_qweb_field_html,model_ir_qweb_field_html,,0,0,0,0 +access_ir_qweb_field_binary,access_ir_qweb_field_binary,model_ir_qweb_field_binary,,0,0,0,0 diff --git a/openerp/tests/addons/test_converter/tests/test_html.py b/openerp/tests/addons/test_converter/tests/test_html.py index b4ad5c35679..7f2713b2761 100644 --- a/openerp/tests/addons/test_converter/tests/test_html.py +++ b/openerp/tests/addons/test_converter/tests/test_html.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- import base64 +import functools import os from openerp.tests import common @@ -9,69 +10,65 @@ 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') + self.columns = self.Model._all_columns 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") + column = self.get_column(name) + try: + model = self.registry('ir.qweb.field.' + column._type) + except KeyError: + model = self.registry('ir.qweb.field') + + return lambda value: model.value_to_html( + self.cr, self.uid, value, column) def test_char(self): converter = self.get_converter('char') - value, warnings = converter('foo') + value = converter('foo') self.assertEqual(value, 'foo') - self.assertEqual(warnings, []) - value, warnings = converter("foo") + value = converter("foo") self.assertEqual(value, "foo<bar>") - self.assertEqual(warnings, []) def test_integer(self): converter = self.get_converter('integer') - value, warnings = converter(42) + value = converter(42) self.assertEqual(value, "42") - self.assertEqual(warnings, []) def test_float(self): converter = self.get_converter('float') - value, warnings = converter(42.0) + value = converter(42.0) self.assertEqual(value, "42.0") - self.assertEqual(warnings, []) - value, warnings = converter(42.0100) + value = converter(42.0100) self.assertEqual(value, "42.01") - self.assertEqual(warnings, []) - value, warnings = converter(42.01234) + value = converter(42.01234) self.assertEqual(value, "42.01234") - self.assertEqual(warnings, []) def test_numeric(self): converter = self.get_converter('numeric') - value, warnings = converter(42.0) + value = converter(42.0) self.assertEqual(value, '42.00') - self.assertEqual(warnings, []) - value, warnings = converter(42.01234) + value = converter(42.01234) self.assertEqual(value, '42.01') - self.assertEqual(warnings, []) def test_text(self): converter = self.get_converter('text') - value, warnings = converter("This is my text-kai") + value = converter("This is my text-kai") self.assertEqual(value, "This is my text-kai") - self.assertEqual(warnings, []) - value, warnings = converter(""" + value = 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,$]. @@ -89,9 +86,8 @@ class TestHTMLExport(common.TransactionCase): -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(""" + value = converter(""" fgdkls;hjas;lj fdslkj d;lasjfa lkdja lfks fldkjsfhs fldskjh """) @@ -99,33 +95,29 @@ class TestHTMLExport(common.TransactionCase): 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)) + value = 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)) + value = 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')) + value = 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() @@ -140,24 +132,30 @@ class TestHTMLExport(common.TransactionCase): converter(content.encode('base64')) def test_selection(self): - converter = self.get_converter('selection') + [record] = self.Model.browse(self.cr, self.uid, [self.Model.create(self.cr, self.uid, { + 'selection': 2, + 'selection_str': 'C', + })]) - value, warnings = converter(2) + column_name = 'selection' + column = self.get_column(column_name) + converter = self.registry('ir.qweb.field.selection') + + value = converter.record_to_html( + self.cr, self.uid, column_name, record, column) self.assertEqual(value, "réponse B") - self.assertEqual(warnings, []) - converter = self.get_converter('selection_str') - - value, warnings = converter('C') + column_name = 'selection_str' + column = self.get_column(column_name) + value = converter.record_to_html( + self.cr, self.uid, column_name, record, column) 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) + value = converter(input) self.assertEqual(value, input) - self.assertEqual(warnings, []) # o2m, m2m? # reference? diff --git a/openerp/tests/test_qweb.py b/openerp/tests/test_qweb.py index f38aa3dd875..17e47dc6096 100644 --- a/openerp/tests/test_qweb.py +++ b/openerp/tests/test_qweb.py @@ -8,17 +8,10 @@ import common impl = dom.getDOMImplementation() document = impl.createDocument(None, None, None) -Request = namedtuple('Request', 'cr uid registry') -class RegistryProxy(object): - def __init__(self, func): - self.func = func - def __getitem__(self, name): - return self.func(name) - class TestQWebTField(common.TransactionCase): def setUp(self): super(TestQWebTField, self).setUp() - self.engine = self.registry('ir.templating.qweb') + self.engine = self.registry('ir.qweb') def test_trivial(self): field = document.createElement('span') @@ -32,7 +25,6 @@ class TestQWebTField(common.TransactionCase): result = self.engine.render_node(field, { 'company': root_company, - 'request': Request(self.cr, self.uid, RegistryProxy(self.registry)) }) self.assertEqual( @@ -57,7 +49,6 @@ class TestQWebTField(common.TransactionCase): result = self.engine.render_node(field, { 'company': root_company, - 'request': Request(self.cr, self.uid, RegistryProxy(self.registry)) }) self.assertEqual( result,