[ADD] Conversions from field values to "html" content

> many2one --> mettre <br/> si multi-line, html escape le reste (ex:
> adresse sur un event, on a du mettre dans un <pre> mais ce n'est pas
> bien)

> text --> mettre <br/> si multi-line, html escape le reste (ex:
> description d'un produit, à droite)

> char --> normalement pas de multi-line

> fields.binary --> t-field on image field ne semble pas fonctionner
> en écriture (la photo d'une fiche produit)

(validates that the binary field's content is image data by opening it
with PIL, then generates an <img> tag)

TODO:

> fields.float --> utiliser le digits pour formatter les decimals
> correctement (ex: prix d'un produit, à deux décimales)

> On aura aussi besoin d'un widget="currency", un peu comme dans la
> vue form du client web.

bzr revid: xmo@openerp.com-20130926133850-ab14h241q878jbom
This commit is contained in:
Xavier Morel 2013-09-26 15:38:50 +02:00
parent 2837c1dc27
commit 789c0d8a6b
12 changed files with 317 additions and 15 deletions

View File

@ -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', '<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]), []
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 ('<img src="data:%s;base64,%s">' % (mime, value)), []
PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', }

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
import models

View File

@ -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:

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_converter_model access_converter_model model_test_converter_test_model 1 1 1 1
3 access_test_converter_test_model_sub access_test_converter_test_model_sub model_test_converter_test_model_sub 1 1 1 1

View File

@ -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()
}

View File

@ -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:

View File

@ -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<bar>")
self.assertEqual(value, "foo&lt;bar&gt;")
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, """<br>
. The current line (address) in the buffer.<br>
$ The last line in the buffer.<br>
n The nth, line in the buffer where n is a number in the range [0,$].<br>
$ The last line in the buffer.<br>
- The previous line. This is equivalent to -1 and may be repeated with cumulative effect.<br>
-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("""
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(value, """<br>
fgdkls;hjas;lj &lt;b&gt;fdslkj&lt;/b&gt; d;lasjfa lkdja &lt;a href=http://spam.com&gt;lfks&lt;/a&gt;<br>
fldkjsfhs &lt;i style=&quot;color: red&quot;&gt;&lt;a href=&quot;http://spamspam.com&quot;&gt;fldskjh&lt;/a&gt;&lt;/i&gt;<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))
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))
self.assertEqual(value, "Fo&lt;b&gt;o&lt;/b&gt;")
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, '<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()
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>span</span>'
value, warnings = converter(input)
self.assertEqual(value, input)
self.assertEqual(warnings, [])
# o2m, m2m?
# reference?

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB