summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphael Collet <rco@openerp.com>2017-01-19 16:29:29 +0100
committerGitHub <noreply@github.com>2017-01-19 16:29:29 +0100
commitb0646f9d2c983481f20bea31f8f6427bbfccb245 (patch)
treef3e22dd4dc6ab5c09331af50e30c71616fcd0048
parentcb90aeade61c8b19ee0313a948ef2fbbf44b2380 (diff)
[REF] move support for sparse fields into a separate module (#15070)
This removal has led to a necessary refactoring: - make the setup of field attributes extensible; - make the instantiation of custom fields extensible; - delegate model and field reflection to `ir.model` and `ir.model.fields`; - move the implementation of sparse and serialized fields to module `base_sparse_field`;
-rw-r--r--addons/base_sparse_field/__init__.py12
-rw-r--r--addons/base_sparse_field/__manifest__.py18
-rw-r--r--addons/base_sparse_field/models/__init__.py4
-rw-r--r--addons/base_sparse_field/models/fields.py92
-rw-r--r--addons/base_sparse_field/models/models.py61
-rw-r--r--addons/base_sparse_field/tests/__init__.py3
-rw-r--r--addons/base_sparse_field/tests/test_sparse_fields.py40
-rw-r--r--addons/base_sparse_field/views/views.xml32
-rw-r--r--odoo/addons/base/base.sql1
-rw-r--r--odoo/addons/base/ir/ir_model.py158
-rw-r--r--odoo/addons/base/ir/ir_model_view.xml6
-rw-r--r--odoo/addons/test_new_api/ir.model.access.csv1
-rw-r--r--odoo/addons/test_new_api/models.py12
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py34
-rw-r--r--odoo/fields.py71
-rw-r--r--odoo/models.py102
16 files changed, 412 insertions, 235 deletions
diff --git a/addons/base_sparse_field/__init__.py b/addons/base_sparse_field/__init__.py
new file mode 100644
index 00000000000..edbaa61c052
--- /dev/null
+++ b/addons/base_sparse_field/__init__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+from . import models
+from . import tests
+
+def _update_database_schema(cr):
+ # The reflection of fields is done before updating the model's schema,
+ # therefore we first update the schema.
+ cr.execute("""
+ ALTER TABLE ir_model_fields
+ ADD COLUMN serialization_field_id integer REFERENCES ir_model_fields ON DELETE cascade
+ """)
diff --git a/addons/base_sparse_field/__manifest__.py b/addons/base_sparse_field/__manifest__.py
new file mode 100644
index 00000000000..031a6c179de
--- /dev/null
+++ b/addons/base_sparse_field/__manifest__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': "Sparse Fields",
+ 'summary': """Implementation of sparse fields.""",
+ 'description': """
+ The purpose of this module is to implement "sparse" fields, i.e., fields
+ that are mostly null. This implementation circumvents the PostgreSQL
+ limitation on the number of columns in a table. The values of all sparse
+ fields are stored in a "serialized" field in the form of a JSON mapping.
+ """,
+ 'category': 'Technical Settings',
+ 'version': '1.0',
+ 'depends': ['base'],
+ 'data': [
+ 'views/views.xml',
+ ],
+ 'pre_init_hook': '_update_database_schema',
+}
diff --git a/addons/base_sparse_field/models/__init__.py b/addons/base_sparse_field/models/__init__.py
new file mode 100644
index 00000000000..37c0f9345cc
--- /dev/null
+++ b/addons/base_sparse_field/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import fields
+from . import models
diff --git a/addons/base_sparse_field/models/fields.py b/addons/base_sparse_field/models/fields.py
new file mode 100644
index 00000000000..cb1e1981193
--- /dev/null
+++ b/addons/base_sparse_field/models/fields.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+
+import json
+
+from odoo import fields
+
+
+def monkey_patch(cls):
+ """ Return a method decorator to monkey-patch the given class. """
+ def decorate(func):
+ name = func.__name__
+ func.super = getattr(cls, name, None)
+ setattr(cls, name, func)
+ return func
+ return decorate
+
+
+#
+# Implement sparse fields by monkey-patching fields.Field
+#
+
+fields.Field.__doc__ += """
+
+ .. _field-sparse:
+
+ .. rubric:: Sparse fields
+
+ Sparse fields have a very small probability of being not null. Therefore
+ many such fields can be serialized compactly into a common location, the
+ latter being a so-called "serialized" field.
+
+ :param sparse: the name of the field where the value of this field must
+ be stored.
+"""
+
+@monkey_patch(fields.Field)
+def _get_attrs(self, model, name):
+ attrs = _get_attrs.super(self, model, name)
+ if attrs.get('sparse'):
+ # by default, sparse fields are not stored and not copied
+ attrs['store'] = False
+ attrs['copy'] = attrs.get('copy', False)
+ attrs['compute'] = self._compute_sparse
+ if not attrs.get('readonly'):
+ attrs['inverse'] = self._inverse_sparse
+ return attrs
+
+@monkey_patch(fields.Field)
+def _compute_sparse(self, records):
+ for record in records:
+ values = record[self.sparse]
+ record[self.name] = values.get(self.name)
+ if self.relational:
+ for record in records:
+ record[self.name] = record[self.name].exists()
+
+@monkey_patch(fields.Field)
+def _inverse_sparse(self, records):
+ for record in records:
+ values = record[self.sparse]
+ value = self.convert_to_read(record[self.name], record, use_name_get=False)
+ if value:
+ if values.get(self.name) != value:
+ values[self.name] = value
+ record[self.sparse] = values
+ else:
+ if self.name in values:
+ values.pop(self.name)
+ record[self.sparse] = values
+
+
+#
+# Definition and implementation of serialized fields
+#
+
+class Serialized(fields.Field):
+ """ Serialized fields provide the storage for sparse fields. """
+ type = 'serialized'
+ _slots = {
+ 'prefetch': False, # not prefetched by default
+ }
+ column_type = ('text', 'text')
+
+ def convert_to_column(self, value, record):
+ return json.dumps(value)
+
+ def convert_to_cache(self, value, record, validate=True):
+ # cache format: dict
+ value = value or {}
+ return value if isinstance(value, dict) else json.loads(value)
+
+fields.Serialized = Serialized
diff --git a/addons/base_sparse_field/models/models.py b/addons/base_sparse_field/models/models.py
new file mode 100644
index 00000000000..b0ea825b7d3
--- /dev/null
+++ b/addons/base_sparse_field/models/models.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models, fields, api
+
+
+class IrModelFields(models.Model):
+ _inherit = 'ir.model.fields'
+
+ ttype = fields.Selection(selection_add=[('serialized', 'serialized')])
+ serialization_field_id = fields.Many2one('ir.model.fields', string='Serialization Field',
+ ondelete='cascade', domain="[('ttype','=','serialized')]",
+ help="If set, this field will be stored in the sparse structure of the "
+ "serialization field, instead of having its own database column. "
+ "This cannot be changed after creation.",
+ )
+
+ @api.multi
+ def write(self, vals):
+ # Limitation: renaming a sparse field or changing the storing system is
+ # currently not allowed
+ if 'serialization_field_id' in vals or 'name' in vals:
+ for field in self:
+ if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
+ raise UserError(_('Changing the storing system for field "%s" is not allowed.') % field.name)
+ if field.serialization_field_id and (field.name != vals['name']):
+ raise UserError(_('Renaming sparse field "%s" is not allowed') % field.name)
+
+ return super(IrModelFields, self).write(vals)
+
+ def _reflect_values(self, field, existing):
+ vals = super(IrModelFields, self)._reflect_values(field, existing)
+
+ vals['serialization_field_id'] = None
+ if getattr(field, 'sparse', None):
+ model = self.env[field.model_name]
+ serialization_field = model._fields.get(field.sparse)
+ if serialization_field is None:
+ raise UserError(_("Serialization field `%s` not found for sparse field `%s`!") % (field.sparse, field.name))
+ serialization_record = self._reflect(serialization_field, existing)
+ vals['serialization_field_id'] = serialization_record.id
+
+ return vals
+
+ def _instanciate_attrs(self, field_data, partial):
+ attrs = super(IrModelFields, self)._instanciate_attrs(field_data, partial)
+ if field_data['serialization_field_id']:
+ serialization_record = self.browse(field_data['serialization_field_id'])
+ attrs['sparse'] = serialization_record.name
+ return attrs
+
+
+class TestSparse(models.TransientModel):
+ _name = 'sparse_fields.test'
+
+ data = fields.Serialized()
+ boolean = fields.Boolean(sparse='data')
+ integer = fields.Integer(sparse='data')
+ float = fields.Float(sparse='data')
+ char = fields.Char(sparse='data')
+ selection = fields.Selection([('one', 'One'), ('two', 'Two')], sparse='data')
+ partner = fields.Many2one('res.partner', sparse='data')
diff --git a/addons/base_sparse_field/tests/__init__.py b/addons/base_sparse_field/tests/__init__.py
new file mode 100644
index 00000000000..1dc570533d5
--- /dev/null
+++ b/addons/base_sparse_field/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import test_sparse_fields
diff --git a/addons/base_sparse_field/tests/test_sparse_fields.py b/addons/base_sparse_field/tests/test_sparse_fields.py
new file mode 100644
index 00000000000..7c7e8264d34
--- /dev/null
+++ b/addons/base_sparse_field/tests/test_sparse_fields.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+from odoo.tests import common
+
+
+class TestSparseFields(common.TransactionCase):
+
+ def test_sparse(self):
+ """ test sparse fields. """
+ record = self.env['sparse_fields.test'].create({})
+ self.assertFalse(record.data)
+
+ partner = self.env.ref('base.main_partner')
+ values = [
+ ('boolean', True),
+ ('integer', 42),
+ ('float', 3.14),
+ ('char', 'John'),
+ ('selection', 'two'),
+ ('partner', partner.id),
+ ]
+ for n, (key, val) in enumerate(values):
+ record.write({key: val})
+ self.assertEqual(record.data, dict(values[:n+1]))
+
+ for key, val in values[:-1]:
+ self.assertEqual(record[key], val)
+ self.assertEqual(record.partner, partner)
+
+ for n, (key, val) in enumerate(values):
+ record.write({key: False})
+ self.assertEqual(record.data, dict(values[n+1:]))
+
+ # check reflection of sparse fields in 'ir.model.fields'
+ names = [name for name, _ in values]
+ domain = [('model', '=', 'sparse_fields.test'), ('name', 'in', names)]
+ fields = self.env['ir.model.fields'].search(domain)
+ self.assertEqual(len(fields), len(names))
+ for field in fields:
+ self.assertEqual(field.serialization_field_id.name, 'data')
diff --git a/addons/base_sparse_field/views/views.xml b/addons/base_sparse_field/views/views.xml
new file mode 100644
index 00000000000..95c3d23634a
--- /dev/null
+++ b/addons/base_sparse_field/views/views.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <!-- Add 'serialization_field_id' in ir.model form view -->
+ <record model="ir.ui.view" id="model_form_view">
+ <field name="model">ir.model</field>
+ <field name="inherit_id" ref="base.view_model_form"/>
+ <field name="arch" type="xml">
+ <field name="related" position="before">
+ <field name="serialization_field_id"
+ domain="[('ttype','=','serialized'), ('model_id','=',parent.model)]"
+ attrs="{'readonly': [('state','=','base')]}"/>
+ </field>
+ </field>
+ </record>
+
+ <!-- Add 'serialization_field_id' in ir.model.fields form view -->
+ <record model="ir.ui.view" id="field_form_view">
+ <field name="model">ir.model.fields</field>
+ <field name="inherit_id" ref="base.view_model_fields_form"/>
+ <field name="arch" type="xml">
+ <field name="related" position="before">
+ <field name="serialization_field_id"
+ domain="[('ttype','=','serialized'), ('model_id','=',model_id)]"
+ attrs="{'readonly': [('state','=','base')]}"/>
+ </field>
+ </field>
+ </record>
+
+ </data>
+</odoo>
diff --git a/odoo/addons/base/base.sql b/odoo/addons/base/base.sql
index 7bfbc9106ad..7372d2ff14f 100644
--- a/odoo/addons/base/base.sql
+++ b/odoo/addons/base/base.sql
@@ -41,7 +41,6 @@ CREATE TABLE ir_model_fields (
required boolean default False,
selectable boolean default False,
translate boolean default False,
- serialization_field_id integer references ir_model_fields on delete cascade,
relation_table varchar,
column1 varchar,
column2 varchar,
diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py
index 4785359063b..a83ab6d3d8a 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -35,6 +35,28 @@ def make_compute(text, deps):
return api.depends(*deps)(func)
+# generic INSERT and UPDATE queries
+INSERT_QUERY = "INSERT INTO {table} ({cols}) VALUES ({vals}) RETURNING id"
+UPDATE_QUERY = "UPDATE {table} SET {assignment} WHERE {condition} RETURNING id"
+
+def query_insert(cr, table, values):
+ query = INSERT_QUERY.format(
+ table=table,
+ cols=",".join(values),
+ vals=",".join(map("%({0})s".format, values)),
+ )
+ cr.execute(query, values)
+
+def query_update(cr, table, values, selectors):
+ setters = set(values) - set(selectors)
+ query = UPDATE_QUERY.format(
+ table=table,
+ assignment=",".join(map("{0}=%({0})s".format, setters)),
+ condition=" AND ".join(map("{0}=%({0})s".format, selectors)),
+ )
+ cr.execute(query, values)
+
+
#
# IMPORTANT: this must be the first model declared in the module
#
@@ -190,6 +212,39 @@ class IrModel(models.Model):
}
return self.create(vals).name_get()[0]
+ def _reflect(self, model):
+ """ Reflect the given model and its fields in the models 'ir.model' and
+ 'ir.model.fields'. Also create entries in 'ir.model.data' if the key
+ 'module' is passed into the context.
+ """
+ cr = self.env.cr
+
+ # create/update the entries in 'ir.model' and 'ir.model.data'
+ params = {
+ 'model': model._name,
+ 'name': model._description,
+ 'info': next(cls.__doc__ for cls in type(model).mro() if cls.__doc__),
+ 'state': 'manual' if model._custom else 'base',
+ 'transient': model._transient,
+ }
+ query_update(cr, self._table, params, ['model'])
+ if not cr.rowcount:
+ query_insert(cr, self._table, params)
+
+ record = self.browse(cr.fetchone())
+ self._context['todo'].append((10, record.modified, [list(params)]))
+
+ if 'module' in self._context:
+ xmlid = 'model_' + model._name.replace('.', '_')
+ cr.execute("SELECT * FROM ir_model_data WHERE name=%s AND module=%s",
+ (xmlid, self._context['module']))
+ if not cr.rowcount:
+ cr.execute(""" INSERT INTO ir_model_data (module, name, model, res_id, date_init, date_update)
+ VALUES (%s, %s, %s, %s, (now() at time zone 'UTC'), (now() at time zone 'UTC')) """,
+ (self._context['module'], xmlid, record._name, record.id))
+
+ return record
+
@api.model
def _instanciate(self, model_data):
""" Return a class for the custom model given by parameters ``model_data``. """
@@ -204,6 +259,10 @@ class IrModel(models.Model):
return CustomModel
+# retrieve field types defined by the framework only (not extensions)
+FIELD_TYPES = [(key, key) for key in sorted(fields.Field.by_type)]
+
+
class IrModelFields(models.Model):
_name = 'ir.model.fields'
_description = "Fields"
@@ -221,7 +280,7 @@ class IrModelFields(models.Model):
help="The model this field belongs to")
field_description = fields.Char(string='Field Label', default='', required=True, translate=True)
help = fields.Text(string='Field Help', translate=True)
- ttype = fields.Selection(selection='_get_field_types', string='Field Type', required=True)
+ ttype = fields.Selection(selection=FIELD_TYPES, string='Field Type', required=True)
selection = fields.Char(string='Selection Options', default="",
help="List of options for a selection field, "
"specified as a Python expression defining a list of (key, label) pairs. "
@@ -242,11 +301,6 @@ class IrModelFields(models.Model):
groups = fields.Many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id')
selectable = fields.Boolean(default=True)
modules = fields.Char(compute='_in_modules', string='In Apps', help='List of modules in which the field is defined')
- serialization_field_id = fields.Many2one('ir.model.fields', 'Serialization Field', domain="[('ttype','=','serialized')]",
- ondelete='cascade', help="If set, this field will be stored in the sparse "
- "structure of the serialization field, instead "
- "of having its own database column. This cannot be "
- "changed after creation.")
relation_table = fields.Char(help="Used for custom many2many fields to define a custom relation table name")
column1 = fields.Char(string='Column 1', help="Column referring to the record in the model table")
column2 = fields.Char(string="Column 2", help="Column referring to the record in the comodel table")
@@ -260,11 +314,6 @@ class IrModelFields(models.Model):
" name, partner_id.name")
store = fields.Boolean(string='Stored', default=True, help="Whether the value is stored in the database.")
- @api.model
- def _get_field_types(self):
- # retrieve the possible field types from the field classes' metaclass
- return sorted((key, key) for key in fields.MetaField.by_type)
-
@api.depends()
def _in_modules(self):
installed_modules = self.env['ir.module.module'].search([('state', '=', 'installed')])
@@ -522,15 +571,6 @@ class IrModelFields(models.Model):
@api.multi
def write(self, vals):
- # For the moment renaming a sparse field or changing the storing system
- # is not allowed. This may be done later
- if 'serialization_field_id' in vals or 'name' in vals:
- for field in self:
- if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
- raise UserError(_('Changing the storing system for field "%s" is not allowed.') % field.name)
- if field.serialization_field_id and (field.name != vals['name']):
- raise UserError(_('Renaming sparse field "%s" is not allowed') % field.name)
-
# if set, *one* column can be renamed here
column_rename = None
@@ -613,9 +653,76 @@ class IrModelFields(models.Model):
res.append((field.id, '%s (%s)' % (field.field_description, field.model)))
return res
- @api.model
- def _instanciate(self, field_data, partial):
- """ Return a field instance corresponding to parameters ``field_data``. """
+ def _reflect_values(self, field, existing):
+ """ Return the values corresponding to the given ``field``. """
+ model = self.env['ir.model']._get(field.model_name)
+ return {
+ 'model_id': model.id,
+ 'model': field.model_name,
+ 'name': field.name,
+ 'field_description': field.string,
+ 'help': field.help or None,
+ 'ttype': field.type,
+ 'relation': field.comodel_name or None,
+ 'index': bool(field.index),
+ 'store': bool(field.store),
+ 'copy': bool(field.copy),
+ 'related': ".".join(field.related) if field.related else None,
+ 'readonly': bool(field.readonly),
+ 'required': bool(field.required),
+ 'selectable': bool(field.search or field.store),
+ 'translate': bool(field.translate),
+ 'relation_field': field.inverse_name if field.type == 'one2many' else None,
+ 'relation_table': field.relation if field.type == 'many2many' else None,
+ 'column1': field.column1 if field.type == 'many2many' else None,
+ 'column2': field.column2 if field.type == 'many2many' else None,
+ }
+
+ def _reflect(self, field, existing):
+ """ Reflect the given field in the model `ir.model.fields` and return
+ its corresponding record.
+ """
+ vals = self._reflect_values(field, existing)
+ existing_data = existing.get(field.name)
+
+ if existing_data is None:
+ cr = self.env.cr
+ # create an entry in this table
+ query_insert(cr, self._table, vals)
+ record = self.browse(cr.fetchone())
+ self._context['todo'].append((20, record.modified, [list(vals)]))
+ # create a corresponding xml id
+ module = field._module or self._context.get('module')
+ if module:
+ model = self.env[field.model_name]
+ xmlid = 'field_%s_%s' % (model._table, field.name)
+ cr.execute("SELECT name FROM ir_model_data WHERE name=%s", (xmlid,))
+ if cr.fetchone():
+ xmlid = xmlid + "_" + str(record.id)
+ cr.execute(""" INSERT INTO ir_model_data (module, name, model, res_id, date_init, date_update)
+ VALUES (%s, %s, %s, %s, (now() at time zone 'UTC'), (now() at time zone 'UTC')) """,
+ (module, xmlid, record._name, record.id))
+ # update existing (for recursive calls)
+ existing[field.name] = dict(vals, id=record.id)
+ return record
+
+ if not all(existing_data[key] == val for key, val in vals.iteritems()):
+ cr = self.env.cr
+ # update the entry in this table
+ query_update(cr, self._table, vals, ['model', 'name'])
+ record = self.browse(cr.fetchone())
+ modified = set(vals) - set(['model', 'name'])
+ self._context['todo'].append((20, record.modified, [modified]))
+ # update existing (for recursive calls)
+ existing_data.update(vals)
+ return record
+
+ else:
+ # nothing to update, simply return the corresponding record
+ return self.browse(existing_data['id'])
+
+ def _instanciate_attrs(self, field_data, partial):
+ """ Return the parameters for a field instance for ``field_data``. """
attrs = {
'manual': True,
'string': field_data['field_description'],
@@ -627,7 +734,6 @@ class IrModelFields(models.Model):
'readonly': bool(field_data['readonly']),
'store': bool(field_data['store']),
}
- # FIXME: ignore field_data['serialization_field_id']
if field_data['ttype'] in ('char', 'text', 'html'):
attrs['translate'] = bool(field_data['translate'])
attrs['size'] = field_data['size'] or None
@@ -661,7 +767,11 @@ class IrModelFields(models.Model):
# add compute function if given
if field_data['compute']:
attrs['compute'] = make_compute(field_data['compute'], field_data['depends'])
+ return attrs
+ def _instanciate(self, field_data, partial):
+ """ Return a field instance corresponding to parameters ``field_data``. """
+ attrs = self._instanciate_attrs(field_data, partial)
return fields.Field.by_type[field_data['ttype']](**attrs)
diff --git a/odoo/addons/base/ir/ir_model_view.xml b/odoo/addons/base/ir/ir_model_view.xml
index ef321efb40b..e11ff11bdb4 100644
--- a/odoo/addons/base/ir/ir_model_view.xml
+++ b/odoo/addons/base/ir/ir_model_view.xml
@@ -108,9 +108,6 @@
</group>
</group>
<group string="Advanced Properties" groups="base.group_no_one">
- <field name="serialization_field_id"
- domain="[('ttype','=','serialized'), ('model_id', '=', parent.model)]"
- attrs="{'readonly': [('state','=','base')]}"/>
<field name="related"/>
<field name="depends"/>
<field name="compute" widget="ace" options="{'mode': 'python'}"/>
@@ -281,9 +278,6 @@
</group>
</group>
<group string="Advanced Properties" groups="base.group_no_one">
- <field name="serialization_field_id"
- domain="[('ttype','=','serialized'), ('model_id', '=', model_id)]"
- attrs="{'readonly': [('state','=','base')]}"/>
<field name="related"/>
<field name="depends" attrs="{'required': [('compute', '!=', False)] }"/>
<field name="compute" widget="ace" options="{'mode': 'python'}"/>
diff --git a/odoo/addons/test_new_api/ir.model.access.csv b/odoo/addons/test_new_api/ir.model.access.csv
index 3862f4ce84f..ee467a56828 100644
--- a/odoo/addons/test_new_api/ir.model.access.csv
+++ b/odoo/addons/test_new_api/ir.model.access.csv
@@ -11,5 +11,4 @@ access_test_new_api_foo,access_test_new_api_foo,model_test_new_api_foo,,1,1,1,1
access_test_new_api_bar,access_test_new_api_bar,model_test_new_api_bar,,1,1,1,1
access_test_new_api_related,access_test_new_api_related,model_test_new_api_related,,1,1,1,1
access_test_new_api_company,access_test_new_api_company,model_test_new_api_company,,1,1,1,1
-access_test_new_api_sparse,access_test_new_api_sparse,model_test_new_api_sparse,,1,1,1,1
access_test_new_api_compute_inverse,access_test_new_api_compute_inverse,model_test_new_api_compute_inverse,,1,1,1,1
diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py
index 8642bc60f74..b4f14abef50 100644
--- a/odoo/addons/test_new_api/models.py
+++ b/odoo/addons/test_new_api/models.py
@@ -296,15 +296,3 @@ class CompanyDependent(models.Model):
_name = 'test_new_api.company'
foo = fields.Char(company_dependent=True)
-
-
-class Sparse(models.Model):
- _name = 'test_new_api.sparse'
-
- data = fields.Serialized()
- boolean = fields.Boolean(sparse='data')
- integer = fields.Integer(sparse='data')
- float = fields.Float(sparse='data')
- char = fields.Char(sparse='data')
- selection = fields.Selection([('one', 'One'), ('two', 'Two')], sparse='data')
- partner = fields.Many2one('res.partner', sparse='data')
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index 84b77d5852d..9247d32370f 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -498,40 +498,6 @@ class TestFields(common.TransactionCase):
self.assertEqual(record.sudo(user1).foo, 'alpha')
self.assertEqual(record.sudo(user2).foo, 'default')
- def test_28_sparse(self):
- """ test sparse fields. """
- record = self.env['test_new_api.sparse'].create({})
- self.assertFalse(record.data)
-
- partner = self.env.ref('base.main_partner')
- values = [
- ('boolean', True),
- ('integer', 42),
- ('float', 3.14),
- ('char', 'John'),
- ('selection', 'two'),
- ('partner', partner.id),
- ]
- for n, (key, val) in enumerate(values):
- record.write({key: val})
- self.assertEqual(record.data, dict(values[:n+1]))
-
- for key, val in values[:-1]:
- self.assertEqual(record[key], val)
- self.assertEqual(record.partner, partner)
-
- for n, (key, val) in enumerate(values):
- record.write({key: False})
- self.assertEqual(record.data, dict(values[n+1:]))
-
- # check reflection of sparse fields in 'ir.model.fields'
- names = [name for name, _ in values]
- domain = [('model', '=', 'test_new_api.sparse'), ('name', 'in', names)]
- fields = self.env['ir.model.fields'].search(domain)
- self.assertEqual(len(fields), len(names))
- for field in fields:
- self.assertEqual(field.serialization_field_id.name, 'data')
-
def test_30_read(self):
""" test computed fields as returned by read(). """
discussion = self.env.ref('test_new_api.discussion_0')
diff --git a/odoo/fields.py b/odoo/fields.py
index 3240b83d801..4a7a5174272 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -8,7 +8,6 @@ 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
@@ -254,17 +253,6 @@ class Field(object):
:param company_dependent: whether the field is company-dependent (boolean)
- .. _field-sparse:
-
- .. rubric:: Sparse fields
-
- Sparse fields have a very small probability of being not null. Therefore
- many such fields can be serialized compactly into a common location, the
- latter being a so-called "serialized" field.
-
- :param sparse: the name of the field where the value of this field must
- be stored.
-
.. _field-incremental-definition:
.. rubric:: Incremental definition
@@ -323,7 +311,6 @@ class Field(object):
'related': None, # sequence of field names, for related fields
'related_sudo': True, # whether related fields should be read as admin
'company_dependent': False, # whether ``self`` is company-dependent (property field)
- 'sparse': None, # the name of the corresponding serialized field, if any
'default': None, # default(recs) returns the default value
'string': None, # field label
@@ -415,8 +402,8 @@ class Field(object):
""" Return whether ``self`` can retrieve parameters from ``field``. """
return isinstance(field, type(self))
- def _setup_attrs(self, model, name):
- """ Determine field parameter attributes. """
+ def _get_attrs(self, model, name):
+ """ Return the field parameter attributes as a dictionary. """
# determine all inherited field attributes
attrs = {}
if not (self.args.get('automatic') or self.args.get('manual')):
@@ -448,14 +435,12 @@ class Field(object):
if not attrs.get('readonly'):
attrs['inverse'] = self._inverse_company_dependent
attrs['search'] = self._search_company_dependent
- if attrs.get('sparse'):
- # by default, sparse fields are not stored and not copied
- attrs['store'] = False
- attrs['copy'] = attrs.get('copy', False)
- attrs['compute'] = self._compute_sparse
- if not attrs.get('readonly'):
- attrs['inverse'] = self._inverse_sparse
+ return attrs
+
+ def _setup_attrs(self, model, name):
+ """ Initialize the field parameter attributes. """
+ attrs = self._get_attrs(model, name)
self.set_all_attrs(attrs)
# check for renamed attributes (conversion errors)
@@ -634,31 +619,6 @@ class Field(object):
return Property.search_multi(self.name, self.model_name, operator, value)
#
- # Sparse fields
- #
-
- def _compute_sparse(self, records):
- for record in records:
- values = record[self.sparse]
- record[self.name] = values.get(self.name)
- if self.relational:
- for record in records:
- record[self.name] = record[self.name].exists()
-
- def _inverse_sparse(self, records):
- for record in records:
- values = record[self.sparse]
- value = self.convert_to_read(record[self.name], record, use_name_get=False)
- if value:
- if values.get(self.name) != value:
- values[self.name] = value
- record[self.sparse] = values
- else:
- if self.name in values:
- values.pop(self.name)
- record[self.sparse] = values
-
- #
# Setup of field triggers
#
# The triggers of ``self`` are a collection of pairs ``(field, path)`` of
@@ -2404,23 +2364,6 @@ class Many2many(_RelationalMulti):
link(act[2])
-class Serialized(Field):
- """ Serialized fields provide the storage for sparse fields. """
- type = 'serialized'
- _slots = {
- 'prefetch': False, # not prefetched by default
- }
- column_type = ('text', 'text')
-
- def convert_to_column(self, value, record):
- return json.dumps(value)
-
- def convert_to_cache(self, value, record, validate=True):
- # cache format: dict
- value = value or {}
- return value if isinstance(value, dict) else json.loads(value)
-
-
class Id(Field):
""" Special case for field 'id'. """
type = 'integer'
diff --git a/odoo/models.py b/odoo/models.py
index 1832cb778d8..f352dae45cc 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -267,105 +267,21 @@ class BaseModel(object):
@api.model_cr_context
def _field_create(self):
- """ Reflect the models and its fields in the models 'ir.model' and
+ """ Reflect the model and its fields in the models 'ir.model' and
'ir.model.fields'. Also create entries in 'ir.model.data' if the key
'module' is passed to the context.
"""
- cr = self._cr
+ # reflect the model in 'ir.model'
+ self.env['ir.model']._reflect(self)
- # create/update the entries in 'ir.model' and 'ir.model.data'
- params = {
- 'model': self._name,
- 'name': self._description,
- 'info': next(cls.__doc__ for cls in type(self).mro() if cls.__doc__),
- 'state': 'manual' if self._custom else 'base',
- 'transient': self._transient,
- }
- cr.execute(""" UPDATE ir_model
- SET name=%(name)s, info=%(info)s, transient=%(transient)s
- WHERE model=%(model)s
- RETURNING id """, params)
- if not cr.rowcount:
- cr.execute(""" INSERT INTO ir_model (model, name, info, state, transient)
- VALUES (%(model)s, %(name)s, %(info)s, %(state)s, %(transient)s)
- RETURNING id """, params)
- model = self.env['ir.model'].browse(cr.fetchone()[0])
- self._context['todo'].append((10, model.modified, [list(params)]))
-
- if 'module' in self._context:
- xmlid = 'model_' + self._name.replace('.', '_')
- cr.execute("SELECT * FROM ir_model_data WHERE name=%s AND module=%s",
- (xmlid, self._context['module']))
- if not cr.rowcount:
- cr.execute(""" INSERT INTO ir_model_data (name, date_init, date_update, module, model, res_id)
- VALUES (%s, (now() at time zone 'UTC'), (now() at time zone 'UTC'), %s, %s, %s) """,
- (xmlid, self._context['module'], 'ir.model', model.id))
-
- # create/update the entries in 'ir.model.fields' and 'ir.model.data'
+ # reflect the fields in 'ir.model.fields'
+ cr = self.env.cr
cr.execute("SELECT * FROM ir_model_fields WHERE model=%s", (self._name,))
- cols = {rec['name']: rec for rec in cr.dictfetchall()}
-
- Fields = self.env['ir.model.fields']
+ existing = {row['name']: row for row in cr.dictfetchall()}
- # sparse fields should be created at the end, as they depend on their serialized field
- model_fields = sorted(self._fields.itervalues(), key=lambda field: bool(field.sparse))
- for field in model_fields:
- vals = {
- 'model_id': model.id,
- 'model': self._name,
- 'name': field.name,
- 'field_description': field.string,
- 'help': field.help or None,
- 'ttype': field.type,
- 'relation': field.comodel_name or None,
- 'index': bool(field.index),
- 'store': bool(field.store),
- 'copy': bool(field.copy),
- 'related': ".".join(field.related) if field.related else None,
- 'readonly': bool(field.readonly),
- 'required': bool(field.required),
- 'selectable': bool(field.search or field.store),
- 'translate': bool(field.translate),
- 'relation_field': field.inverse_name if field.type == 'one2many' else None,
- 'serialization_field_id': None,
- 'relation_table': field.relation if field.type == 'many2many' else None,
- 'column1': field.column1 if field.type == 'many2many' else None,
- 'column2': field.column2 if field.type == 'many2many' else None,
- }
- if field.sparse:
- # resolve link to serialization_field if specified by name
- serialization_field = Fields.search([('model', '=', vals['model']), ('name', '=', field.sparse)])
- if not serialization_field:
- raise UserError(_("Serialization field `%s` not found for sparse field `%s`!") % (field.sparse, field.name))
- vals['serialization_field_id'] = serialization_field.id
-
- if field.name not in cols:
- query = "INSERT INTO ir_model_fields (%s) VALUES (%s) RETURNING id" % (
- ",".join(vals),
- ",".join("%%(%s)s" % name for name in vals),
- )
- cr.execute(query, vals)
- field_id = cr.fetchone()[0]
- self._context['todo'].append((20, Fields.browse(field_id).modified, [list(vals)]))
-
- module = field._module or self._context.get('module')
- if module:
- xmlid = 'field_%s_%s' % (self._table, field.name)
- cr.execute("SELECT name FROM ir_model_data WHERE name=%s", (xmlid,))
- if cr.fetchone():
- xmlid = xmlid + "_" + str(field_id)
- cr.execute(""" INSERT INTO ir_model_data (name, date_init, date_update, module, model, res_id)
- VALUES (%s, (now() at time zone 'UTC'), (now() at time zone 'UTC'), %s, %s, %s) """,
- (xmlid, module, 'ir.model.fields', field_id))
-
- elif not all(cols[field.name][key] == vals[key] for key in vals):
- names = set(vals) - {'model', 'name'}
- query = "UPDATE ir_model_fields SET %s WHERE model=%%(model)s AND name=%%(name)s RETURNING id" % (
- ",".join("%s=%%(%s)s" % (name, name) for name in names),
- )
- cr.execute(query, vals)
- field_id = cr.fetchone()[0]
- self._context['todo'].append((20, Fields.browse(field_id).modified, [names]))
+ reflect = self.env['ir.model.fields']._reflect
+ for field in self._fields.itervalues():
+ reflect(field, existing)
self.invalidate_cache()