[REF] fields: replace old-API _symbol_get/_symbol_set by field converters

This commit is contained in:
Raphael Collet
2016-08-31 17:21:49 +02:00
parent f49f80380e
commit 73bdc181ff
3 changed files with 112 additions and 35 deletions
+88 -3
View File
@@ -8,13 +8,16 @@ from datetime import date, datetime
from functools import partial
from operator import attrgetter
from types import NoneType
import json
import logging
import pytz
import xmlrpclib
import psycopg2
from openerp.sql_db import LazyCursor
from openerp.tools import float_precision, float_round, frozendict, \
html_sanitize, pg_varchar, ustr, OrderedSet
from openerp.tools import float_precision, float_repr, float_round, frozendict, \
html_sanitize, human_size, pg_varchar, ustr, OrderedSet
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
from openerp.tools.translate import html_translate
@@ -291,6 +294,7 @@ class Field(object):
relational = False # whether the field is a relational one
column_type = None # database column type (ident, spec)
column_format = '%s' # placeholder for value in queries
_slots = {
'args': EMPTY_DICT, # the parameters given to __init__()
@@ -797,6 +801,14 @@ class Field(object):
""" Return the null value for this field in the record format. """
return False
def convert_to_column(self, value, record):
""" Convert ``value`` from the ``write`` format to the SQL format. """
if value is None or value == False:
return None
if isinstance(value, unicode):
return value.encode('utf8')
return str(value)
def convert_to_cache(self, value, record, validate=True):
""" Convert ``value`` to the cache format; ``value`` may come from an
assignment, or have the format of methods :meth:`BaseModel.read` or
@@ -1090,6 +1102,9 @@ class Boolean(Field):
type = 'boolean'
column_type = ('bool', 'bool')
def convert_to_column(self, value, record):
return bool(value)
def convert_to_cache(self, value, record, validate=True):
return bool(value)
@@ -1106,6 +1121,9 @@ class Integer(Field):
'group_operator': 'sum',
}
def convert_to_column(self, value, record):
return int(value or 0)
def convert_to_cache(self, value, record, validate=True):
if isinstance(value, dict):
# special case, when an integer field is used as inverse for a one2many
@@ -1169,6 +1187,14 @@ class Float(Field):
_column_digits = property(lambda self: not callable(self._digits) and self._digits)
_column_digits_compute = property(lambda self: callable(self._digits) and self._digits)
def convert_to_column(self, value, record):
result = float(value or 0.0)
digits = self.digits
if digits:
precision, scale = digits
result = float_repr(float_round(result, precision_digits=scale), precision_digits=scale)
return result
def convert_to_cache(self, value, record, validate=True):
# apply rounding here, otherwise value in cache may be wrong!
value = float(value or 0.0)
@@ -1215,6 +1241,12 @@ class Monetary(Field):
assert self.currency_field in model._fields, \
"Field %s with unknown currency_field %r" % (self, self.currency_field)
def convert_to_column(self, value, record):
try:
return value.float_repr() # see float_precision.float_repr()
except Exception:
return float(value or 0.0)
def convert_to_cache(self, value, record, validate=True):
if validate:
currency = record[self.currency_field]
@@ -1307,6 +1339,17 @@ class Char(_String):
assert isinstance(self.size, (NoneType, int)), \
"Char field %s with non-integer size %r" % (self, self.size)
def convert_to_column(self, value, record):
#TODO:
# * we need to remove the "value==False" from the next line BUT
# for now too many things rely on this broken behavior
# * the value==None test should be common to all data types
if value is None or value == False:
return None
# we need to convert the string to a unicode object to be able
# to evaluate its length (and possibly truncate it) reliably
return ustr(value)[:self.size].encode('utf8')
def convert_to_cache(self, value, record, validate=True):
if value is None or value is False:
return False
@@ -1374,6 +1417,19 @@ class Html(_String):
_related_strip_classes = property(attrgetter('strip_classes'))
_description_strip_classes = property(attrgetter('strip_classes'))
def convert_to_column(self, value, record):
if value is None or value is False:
return None
if self.sanitize:
return html_sanitize(
value, silent=True,
sanitize_tags=self.sanitize_tags,
sanitize_attributes=self.sanitize_attributes,
sanitize_style=self.sanitize_style,
strip_style=self.strip_style,
strip_classes=self.strip_classes)
return value
def convert_to_cache(self, value, record, validate=True):
if value is None or value is False:
return False
@@ -1540,6 +1596,27 @@ class Binary(Field):
_column_attachment = property(attrgetter('attachment'))
_description_attachment = property(attrgetter('attachment'))
def convert_to_column(self, value, record):
# Binary values may be byte strings (python 2.6 byte array), but
# the legacy OpenERP convention is to transfer and store binaries
# as base64-encoded strings. The base64 string may be provided as a
# unicode in some circumstances, hence the str() cast here.
# This str() coercion will only work for pure ASCII unicode strings,
# on purpose - non base64 data must be passed as a 8bit byte strings.
return psycopg2.Binary(str(value)) if value else None
def convert_to_cache(self, value, record, validate=True):
if isinstance(value, buffer):
return str(value)
if isinstance(value, (int, long)) and \
(record._context.get('bin_size') or
record._context.get('bin_size_' + self.name)):
# If the client requests only the size of the field, we return that
# instead of the content. Presumably a separate request will be done
# to read the actual content, if necessary.
return human_size(value)
return value
class Selection(Field):
"""
@@ -1786,6 +1863,9 @@ class Many2one(_Relational):
"""
records._cache[self] = self.convert_to_cache(value, records, validate=False)
def convert_to_column(self, value, record):
return value or None
def convert_to_cache(self, value, record, validate=True):
# cache format: tuple(ids)
def process(ids):
@@ -2122,8 +2202,13 @@ class Serialized(Field):
type = 'serialized'
column_type = ('text', 'text')
def convert_to_column(self, value, record):
return json.dumps(value)
def convert_to_cache(self, value, record, validate=True):
return value or {}
# cache format: dict
value = value or {}
return value if isinstance(value, dict) else json.loads(value)
class Id(Field):
+18 -27
View File
@@ -2312,13 +2312,12 @@ class BaseModel(object):
# get the default value; ideally, we should use default_get(), but it
# fails due to ir.values not being ready
field = self._fields[column_name]
setc, setf = field.column._symbol_set
if field.default:
value = field.default(self)
value = field.convert_to_cache(value, self, validate=False)
value = field.convert_to_record(value, self)
value = field.convert_to_write(value, self)
value = setf(value)
value = field.convert_to_column(value, self)
else:
value = None
# Write value if non-NULL, except for booleans for which False means
@@ -2328,7 +2327,7 @@ class BaseModel(object):
_logger.debug("Table '%s': setting default value of new column %s to %r",
self._table, column_name, value)
query = 'UPDATE "%s" SET "%s"=%s WHERE "%s" IS NULL' % (
self._table, column_name, setc, column_name)
self._table, column_name, field.column_format, column_name)
self._cr.execute(query, (value,))
# this is a disgrace
self._cr.commit()
@@ -3230,14 +3229,6 @@ class BaseModel(object):
for vals in result:
vals[f] = translate(vals['id'], vals[f])
# apply the symbol_get functions of the fields we just read
for field in fields_pre:
symbol_get = field.base_field.column._symbol_get
if symbol_get:
f = field.name
for vals in result:
vals[f] = symbol_get(vals[f])
# store result in cache for POST fields
for vals in result:
record = self.browse(vals['id'], self._prefetch)
@@ -3710,15 +3701,14 @@ class BaseModel(object):
field = self._fields[name]
if field and field.deprecated:
_logger.warning('Field %s.%s is deprecated: %s', self._name, name, field.deprecated)
if field.column:
column = field.column
if hasattr(column, 'selection') and val:
if field.store:
if hasattr(field, 'selection') and val:
self._check_selection_field_value(name, val)
if column._classic_write and not hasattr(column, '_fnct_inv'):
if single_lang or not (has_trans and column.translate and not callable(column.translate)):
if field.column_type:
if single_lang or not (has_trans and getattr(field, 'translate', None) and not callable(field.translate)):
# val is not a translation: update the table
setc, setf = column._symbol_set
updates.append((name, setc, setf(val)))
val = field.convert_to_column(val, self)
updates.append((name, field.column_format, val))
direct.append(name)
else:
upd_todo.append(name)
@@ -3744,20 +3734,22 @@ class BaseModel(object):
# TODO: optimize
for name in direct:
column = self._columns[name]
if callable(column.translate):
field = self._fields[name]
translate = getattr(field, 'translate', None)
if callable(translate):
# The source value of a field has been modified,
# synchronize translated terms when possible.
self.env['ir.translation']._sync_terms_translations(self._fields[name], self)
elif has_trans and column.translate:
elif has_trans and translate:
# The translated value of a field has been modified.
src_trans = self.read([name])[0][name]
if not src_trans:
# Insert value to DB
src_trans = vals[name]
self.with_context(lang=None).write({name: src_trans})
val = column._symbol_set[1](vals[name])
val = field.convert_to_column(vals[name], self)
tname = "%s,%s" % (self._name, name)
self.env['ir.translation']._set_ids(
tname, 'model', self.env.lang, self.ids, val, src_trans)
@@ -3987,14 +3979,13 @@ class BaseModel(object):
# determine SQL values
for name, val in vals.iteritems():
column = self._columns[name]
if column._classic_write:
setc, setf = column._symbol_set
updates.append((name, setc, setf(val)))
field = self._fields[name]
if field.store and field.column_type:
updates.append((name, field.column_format, field.convert_to_column(val, self)))
else:
upd_todo.append(name)
if hasattr(column, 'selection') and val:
if hasattr(field, 'selection') and val:
self._check_selection_field_value(name, val)
if self._log_access:
+6 -5
View File
@@ -117,6 +117,7 @@ import collections
import logging
import traceback
from functools import partial
from zlib import crc32
import openerp.modules
@@ -1182,9 +1183,9 @@ class expression(object):
if left == 'id':
instr = ','.join(['%s'] * len(params))
else:
ss = model._columns[left]._symbol_set
instr = ','.join([ss[0]] * len(params))
params = map(ss[1], params)
field = model._fields[left]
instr = ','.join([field.column_format] * len(params))
params = map(partial(field.convert_to_column, record=model), params)
query = '(%s."%s" %s (%s))' % (table_alias, left, operator, instr)
else:
# The case for (left, 'in', []) or (left, 'not in', []).
@@ -1235,7 +1236,7 @@ class expression(object):
cast = '::text' if sql_operator.endswith('like') else ''
if left in model._columns:
format = need_wildcard and '%s' or model._columns[left]._symbol_set[0]
format = need_wildcard and '%s' or model._fields[left].column_format
unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x
column = '%s.%s' % (table_alias, _quote(left))
query = '(%s %s %s)' % (unaccent(column + cast), sql_operator, unaccent(format))
@@ -1256,7 +1257,7 @@ class expression(object):
params = '%%%s%%' % str_utf8
add_null = not str_utf8
elif left in model._columns:
params = model._columns[left]._symbol_set[1](right)
params = model._fields[left].convert_to_column(right, model)
if add_null:
query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left)