[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:
Raphael Collet
2017-01-19 16:29:29 +01:00
committed by GitHub
parent cb90aeade6
commit b0646f9d2c
16 changed files with 412 additions and 235 deletions
+12
View File
@@ -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
""")
+18
View File
@@ -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
+92
View File
@@ -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
+61
View File
@@ -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')
+32
View File
@@ -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>
-1
View File
@@ -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
View File
@@ -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)
-6
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
11 access_test_new_api_bar access_test_new_api_bar model_test_new_api_bar 1 1 1 1
12 access_test_new_api_related access_test_new_api_related model_test_new_api_related 1 1 1 1
13 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
14 access_test_new_api_compute_inverse access_test_new_api_compute_inverse model_test_new_api_compute_inverse 1 1 1 1
-12
View File
@@ -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
View File
@@ -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
View File
@@ -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()