diff --git a/openerp/fields.py b/openerp/fields.py index cdb148152a6..a13b7603763 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -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): diff --git a/openerp/models.py b/openerp/models.py index 6234824f445..36bdff01502 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -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: diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 837750e5286..985b1135d4b 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -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)