[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`;
This commit is contained in:
@@ -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
|
||||
""")
|
||||
@@ -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',
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import fields
|
||||
from . import models
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_sparse_fields
|
||||
@@ -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')
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
+134
-24
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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'}"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
+7
-64
@@ -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)
|
||||
@@ -633,31 +618,6 @@ class Field(object):
|
||||
Property = records.env['ir.property']
|
||||
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
|
||||
#
|
||||
@@ -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'
|
||||
|
||||
+9
-93
@@ -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()}
|
||||
existing = {row['name']: row for row in cr.dictfetchall()}
|
||||
|
||||
Fields = self.env['ir.model.fields']
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user