diff options
| author | Raphael Collet <rco@openerp.com> | 2017-01-19 16:29:29 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-01-19 16:29:29 +0100 |
| commit | b0646f9d2c983481f20bea31f8f6427bbfccb245 (patch) | |
| tree | f3e22dd4dc6ab5c09331af50e30c71616fcd0048 | |
| parent | cb90aeade61c8b19ee0313a948ef2fbbf44b2380 (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__.py | 12 | ||||
| -rw-r--r-- | addons/base_sparse_field/__manifest__.py | 18 | ||||
| -rw-r--r-- | addons/base_sparse_field/models/__init__.py | 4 | ||||
| -rw-r--r-- | addons/base_sparse_field/models/fields.py | 92 | ||||
| -rw-r--r-- | addons/base_sparse_field/models/models.py | 61 | ||||
| -rw-r--r-- | addons/base_sparse_field/tests/__init__.py | 3 | ||||
| -rw-r--r-- | addons/base_sparse_field/tests/test_sparse_fields.py | 40 | ||||
| -rw-r--r-- | addons/base_sparse_field/views/views.xml | 32 | ||||
| -rw-r--r-- | odoo/addons/base/base.sql | 1 | ||||
| -rw-r--r-- | odoo/addons/base/ir/ir_model.py | 158 | ||||
| -rw-r--r-- | odoo/addons/base/ir/ir_model_view.xml | 6 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/ir.model.access.csv | 1 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/models.py | 12 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/tests/test_new_fields.py | 34 | ||||
| -rw-r--r-- | odoo/fields.py | 71 | ||||
| -rw-r--r-- | odoo/models.py | 102 |
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() |
