diff --git a/addons/website/__init__.py b/addons/website/__init__.py index d9cff35c01a..185183a2257 100644 --- a/addons/website/__init__.py +++ b/addons/website/__init__.py @@ -1,5 +1,3 @@ import controllers -import res_config -import website -import view +import models diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py new file mode 100644 index 00000000000..94d4412b1cf --- /dev/null +++ b/addons/website/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +import ir_fields +import res_config +import view +import website diff --git a/addons/website/models/ir_fields.py b/addons/website/models/ir_fields.py new file mode 100644 index 00000000000..fbccf9c296d --- /dev/null +++ b/addons/website/models/ir_fields.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +from lxml import etree, html + +from openerp.osv import orm, fields +from openerp.tools import ustr + +class converter(orm.Model): + _inherit = 'ir.fields.converter' + + def _html_to_integer(self, cr, uid, model, column, value, context=None): + return int(value.text_content().strip()), [] + + def _html_to_float(self, cr, uid, model, column, value, context=None): + return float(value.text_content().strip()), [] + + def _html_to_passthrough(self, cr, uid, model, column, value, context=None): + return value.text_content().strip(), [] + + _html_to_char = _html_to_date = _html_to_datetime = _html_to_passthrough + + def _html_to_text(self, cr, uid, model, column, value, context=None): + return value.text_content(), [] + + def _html_to_selection(self, cr, uid, model, column, value, context=None): + text = value.text_content().strip() + + selection = column.reify(cr, uid, model, column, context=context) + for k, v in selection: + if isinstance(v, str): + v = ustr(v) + if text == v: + return k, [] + + warning = u"No value found for label %s in selection %s" % (text, selection) + # FIXME: ? + return False, [Warning(warning.encode('utf-8'))] + + def _html_to_many2one(self, cr, uid, model, column, value, context=None): + matches = self.pool[column._obj].name_search( + cr, uid, name=value.text_content().strip(), context=context) + # FIXME: more than one match, error reporting + return matches[0][0], [] + + def _html_to_html(self, cr, uid, model, column, value, context=None): + content = [] + if value.text: content.append(value.text) + content.extend(html.tostring(child) + for child in value.iterchildren(tag=etree.Element)) + return '\n'.join(content), [] + + +class test_converter(orm.Model): + _name = 'website.converter.test' + + _columns = { + 'char': fields.char(), + 'integer': fields.integer(), + 'float': fields.float(), + 'numeric': fields.float(digits=(16, 2)), + 'many2one': fields.many2one('website.converter.test.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_converter_sub(orm.Model): + _name = 'website.converter.test.sub' + + _columns = { + 'name': fields.char(), + } diff --git a/addons/website/res_config.py b/addons/website/models/res_config.py similarity index 100% rename from addons/website/res_config.py rename to addons/website/models/res_config.py diff --git a/addons/website/view.py b/addons/website/models/view.py similarity index 66% rename from addons/website/view.py rename to addons/website/models/view.py index 6290a326fcf..6ddae129185 100644 --- a/addons/website/view.py +++ b/addons/website/models/view.py @@ -55,57 +55,19 @@ class view(osv.osv): def extract_embedded_fields(self, cr, uid, arch, context=None): return arch.xpath('//*[@data-oe-model != "ir.ui.view"]') - def convert_embedded_field(self, cr, uid, el, column, - type_override=None, context=None): - """ Converts the content of an embedded field to a value acceptable - for writing in the column - - :param etree._Element el: embedded field being saved - :param fields._column column: column object corresponding to the field - :param type type_override: column type to dispatch on instead of the - column's actual type (for proxy column types - e.g. relateds) - :return: converted value - """ - column_type = type_override or type(column) - - if issubclass(column_type, fields.html): - content = [] - if el.text: content.append(el.text) - content.extend(html.tostring(child) - for child in el.iterchildren(tag=etree.Element)) - return '\n'.join(content) - elif issubclass(column_type, fields.integer): - return int(el.text_content()) - elif issubclass(column_type, fields.float): - return float(el.text_content()) - elif issubclass(column_type, (fields.char, fields.text, - fields.date, fields.datetime)): - return el.text_content() - # TODO: fields.selection - elif issubclass(column_type, fields.many2one): - matches = self.pool[column._obj].name_search( - cr, uid, name=el.text_content().strip(), context=context) - # FIXME: more than one match, error reporting - return matches[0][0] - elif issubclass(column_type, fields.function): - # FIXME: special-case selection as in get_pg_type? - return self.convert_embedded_field( - cr, uid, el, column, - type_override=getattr(fields, column._type), - context=context) - # TODO?: fields.many2many, fields.one2many - # TODO?: fields.reference - else: - raise TypeError("Un-convertable column type %s" % column_type) - def save_embedded_field(self, cr, uid, el, context=None): Model = self.pool[el.get('data-oe-model')] field = el.get('data-oe-field') column = Model._all_columns[field].column + convert = self.pool['ir.fields.converter'].to_field( + cr, uid, Model, column, fromtype="html", context=context) + value, warnings = convert(el) + # FIXME: report error + if warnings: return + Model.write(cr, uid, [int(el.get('data-oe-id'))], { - field: self.convert_embedded_field(cr, uid, el, column, context=context) + field: value }, context=context) def to_field_ref(self, cr, uid, el, context=None): @@ -137,7 +99,8 @@ class view(osv.osv): """ res_id = int(res_id) - arch_section = html.fromstring(value) + arch_section = html.fromstring( + value, parser=html.HTMLParser(encoding='utf-8')) if xpath is None: # value is an embedded field on its own, not a view section diff --git a/addons/website/website.py b/addons/website/models/website.py similarity index 100% rename from addons/website/website.py rename to addons/website/models/website.py diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py index 9ff1f01db62..b00c5984aa0 100644 --- a/addons/website/tests/__init__.py +++ b/addons/website/tests/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -import test_views +import test_views, test_converter -checks = [ test_views ] +checks = [ test_views, test_converter, ] diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py new file mode 100644 index 00000000000..7f042988916 --- /dev/null +++ b/addons/website/tests/test_converter.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +from collections import namedtuple +from functools import partial +from xml.dom.minidom import getDOMImplementation + +from lxml import etree, html + +from openerp.tests import common +from openerp.tools import qweb +import unittest2 + +renderer = qweb.QWebXml() +impl = 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 TestConvertBack(common.TransactionCase): + def setUp(self): + super(TestConvertBack, self).setUp() + + self.Converter = self.registry('ir.fields.converter') + + def field_rountrip_result(self, field, value, expected): + model = 'website.converter.test' + Model = self.registry(model) + id = Model.create( + self.cr, self.uid, { + field: value + }) + [record] = Model.browse(self.cr, self.uid, [id]) + + e = document.createElement('span') + field_value = 'record.%s' % field + e.setAttribute('t-field', field_value) + + rendered = renderer.render_tag_field( + e, {'field': field_value}, '', { + 'record': record, + 'request': Request(self.cr, self.uid, RegistryProxy( self.registry)), + }) + element = html.fromstring( + rendered, parser=html.HTMLParser(encoding='utf-8')) + + column = Model._all_columns[field].column + + from_html = self.Converter.to_field( + self.cr, self.uid, model, column, 'html') + + value_back, warnings = from_html(element) + self.assertEqual(warnings, []) + + if isinstance(expected, str): + expected = expected.decode('utf-8') + self.assertEqual(value_back, expected) + + def field_roundtrip(self, field, value): + self.field_rountrip_result(field, value, value) + + def test_integer(self): + self.field_roundtrip('integer', 42) + + def test_float(self): + self.field_roundtrip('float', 42.567890) + + def test_numeric(self): + self.field_roundtrip('numeric', 42.77) + + def test_char(self): + self.field_roundtrip('char', "foo bar") + + self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ") + + def test_m2o(self): + Sub = self.registry('website.converter.test.sub') + sub = partial(Sub.create, self.cr, self.uid) + ids = [ + sub({'name': "Foo"}), + sub({'name': "Bar"}), + sub({'name': "Baz"}), + ] + + self.field_rountrip_result( + 'many2one', + ids[2], + ids[2]) + + def test_selection(self): + self.field_roundtrip('selection', 3) + + def test_selection_str(self): + self.field_roundtrip('selection_str', 'B') + + def test_text(self): + self.field_roundtrip('text', """ + You must obey the dance commander + Givin' out the order for fun + You must obey the dance commander + You know that he's the only one + Who gives the orders here, + Alright + Who gives the orders here, + Alright + + It would be awesome + If we could dance-a + It would be awesome, yeah + Let's take the chance-a + It would be awesome, yeah + Let's start the show + Because you never know + You never know + You never know until you go + """)