[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
This commit is contained in:
parent
856e3d0b91
commit
cd31b4af4d
|
@ -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', '<br>\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', '<br>\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 ('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value)), []
|
||||
|
|
|
@ -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', '<br>\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', '<br>\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 ('<img src="data:%s;base64,%s">' % (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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
|
@ -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<bar>")
|
||||
value = converter("foo<bar>")
|
||||
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.<br>
|
||||
+ The next line. This is equivalent to +1 and may be repeated with cumulative effect.<br>
|
||||
""")
|
||||
self.assertEqual(warnings, [])
|
||||
|
||||
value, warnings = converter("""
|
||||
value = converter("""
|
||||
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>
|
||||
""")
|
||||
|
@ -99,33 +95,29 @@ class TestHTMLExport(common.TransactionCase):
|
|||
fgdkls;hjas;lj <b>fdslkj</b> d;lasjfa lkdja <a href=http://spam.com>lfks</a><br>
|
||||
fldkjsfhs <i style="color: red"><a href="http://spamspam.com">fldskjh</a></i><br>
|
||||
""")
|
||||
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': "Fo<b>o</b>"})
|
||||
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, '<img src="data:image/jpeg;base64,%s">' % (
|
||||
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>span</span>'
|
||||
value, warnings = converter(input)
|
||||
value = converter(input)
|
||||
self.assertEqual(value, input)
|
||||
self.assertEqual(warnings, [])
|
||||
# o2m, m2m?
|
||||
# reference?
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue