[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:
parent
2837c1dc27
commit
789c0d8a6b
|
@ -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', }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import models
|
|
@ -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:
|
|
@ -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
|
|
|
@ -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()
|
||||
}
|
|
@ -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:
|
|
@ -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<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, """<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 <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))
|
||||
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<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, '<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 |
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue