From 017688cc29e71270762c828760feb062bbe9ba24 Mon Sep 17 00:00:00 2001 From: Olivier LAURENT Date: Mon, 31 Aug 2015 17:42:13 +0200 Subject: [PATCH] [FIX] models: old api, prevent infinite recursion in stored function fields The risk was introduced by b7f1b9c. IF _store_set_values() recall another _create() or _write(), the recomputation mechanism enter in an infinite recursion trying to reevaluate for each call exactly the same fields for the same records than the previous one This revision replaces the loop of _store_set_values() by 2 nested loops: - that not breaks the entire consumption of recompute_old queue (Tested thanks to revision a922d39), - that allows to clear the queue before each recomputations bundle fixing thereby the recursion Closes #7558 --- openerp/addons/test_new_api/__openerp__.py | 4 +- openerp/addons/test_new_api/demo_data.xml | 2 + .../addons/test_new_api/ir.model.access.csv | 2 + openerp/addons/test_new_api/models.py | 50 +++++++++++++++++++ openerp/addons/test_new_api/tests/__init__.py | 1 + .../tests/test_no_infinite_recursion.py | 31 ++++++++++++ openerp/models.py | 42 +++++++++------- 7 files changed, 113 insertions(+), 19 deletions(-) create mode 100644 openerp/addons/test_new_api/tests/test_no_infinite_recursion.py diff --git a/openerp/addons/test_new_api/__openerp__.py b/openerp/addons/test_new_api/__openerp__.py index 9c859de4445..89355ac19ee 100644 --- a/openerp/addons/test_new_api/__openerp__.py +++ b/openerp/addons/test_new_api/__openerp__.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- { - 'name': 'Test New API', + 'name': 'Test API', 'version': '1.0', 'category': 'Tests', - 'description': """A module to test the new API.""", + 'description': """A module to test the API.""", 'author': 'OpenERP SA', 'maintainer': 'OpenERP SA', 'website': 'http://www.openerp.com', diff --git a/openerp/addons/test_new_api/demo_data.xml b/openerp/addons/test_new_api/demo_data.xml index acb53a7b07f..1715b024cd7 100644 --- a/openerp/addons/test_new_api/demo_data.xml +++ b/openerp/addons/test_new_api/demo_data.xml @@ -26,5 +26,7 @@ This is a much longer message + + diff --git a/openerp/addons/test_new_api/ir.model.access.csv b/openerp/addons/test_new_api/ir.model.access.csv index 5b62045ae08..2bbd1adf12d 100644 --- a/openerp/addons/test_new_api/ir.model.access.csv +++ b/openerp/addons/test_new_api/ir.model.access.csv @@ -3,3 +3,5 @@ access_category,test_new_api_category,test_new_api.model_test_new_api_category,, access_discussion,test_new_api_discussion,test_new_api.model_test_new_api_discussion,,1,1,1,1 access_message,test_new_api_message,test_new_api.model_test_new_api_message,,1,1,1,1 access_mixed,test_new_api_mixed,test_new_api.model_test_new_api_mixed,,1,1,1,1 +access_test_function_noinfiniterecursion,access_test_function_noinfiniterecursion,model_test_old_api_function_noinfiniterecursion,,1,1,1,1 +access_test_function_counter,access_test_function_counter,model_test_old_api_function_counter,,1,1,1,1 diff --git a/openerp/addons/test_new_api/models.py b/openerp/addons/test_new_api/models.py index 4399cb0ed49..173935a8985 100644 --- a/openerp/addons/test_new_api/models.py +++ b/openerp/addons/test_new_api/models.py @@ -19,9 +19,17 @@ # ############################################################################## +import datetime from openerp.exceptions import AccessError + +############################################################################## +# +# OLD API +# +############################################################################## from openerp.osv import osv, fields + class res_partner(osv.Model): _inherit = 'res.partner' @@ -41,6 +49,48 @@ class res_partner(osv.Model): } +class TestFunctionCounter(osv.Model): + _name = 'test_old_api.function_counter' + + def _compute_cnt(self, cr, uid, ids, fname, arg, context=None): + res = {} + for cnt in self.browse(cr, uid, ids, context=context): + res[cnt.id] = cnt.access and cnt.cnt + 1 or 0 + return res + + _columns = { + 'access': fields.datetime('Datetime Field'), + 'cnt': fields.function( + _compute_cnt, type='integer', string='Function Field', store=True), + } + + +class TestFunctionNoInfiniteRecursion(osv.Model): + _name = 'test_old_api.function_noinfiniterecursion' + + def _compute_f1(self, cr, uid, ids, fname, arg, context=None): + res = {} + for tf in self.browse(cr, uid, ids, context=context): + res[tf.id] = 'create' in tf.f0 and 'create' or 'write' + cntobj = self.pool['test_old_api.function_counter'] + cnt_id = self.pool['ir.model.data'].xmlid_to_res_id( + cr, uid, 'test_new_api.c1') + cntobj.write( + cr, uid, cnt_id, {'access': datetime.datetime.now()}, + context=context) + return res + + _columns = { + 'f0': fields.char('Char Field'), + 'f1': fields.function( + _compute_f1, type='char', string='Function Field', store=True), + } + +############################################################################## +# +# NEW API +# +############################################################################## from openerp import models, fields, api, _ diff --git a/openerp/addons/test_new_api/tests/__init__.py b/openerp/addons/test_new_api/tests/__init__.py index 790ad9c9a59..f4803bd4837 100644 --- a/openerp/addons/test_new_api/tests/__init__.py +++ b/openerp/addons/test_new_api/tests/__init__.py @@ -5,3 +5,4 @@ from . import test_new_fields from . import test_onchange from . import test_field_conversions from . import test_attributes +from . import test_no_infinite_recursion diff --git a/openerp/addons/test_new_api/tests/test_no_infinite_recursion.py b/openerp/addons/test_new_api/tests/test_no_infinite_recursion.py new file mode 100644 index 00000000000..68ef3fcf3b9 --- /dev/null +++ b/openerp/addons/test_new_api/tests/test_no_infinite_recursion.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from openerp.tests import common + + +class test_no_infinite_recursion(common.TransactionCase): + + def setUp(self): + super(test_no_infinite_recursion, self).setUp() + self.tstfct = self.registry['test_old_api.function_noinfiniterecursion'] + + def test_00_create_and_update(self): + """ + Check that computing old api function field does not cycle infinitely + See https://github.com/odoo/odoo/pull/7558 + """ + cr, uid, context, tstfct = self.cr, self.uid, {}, self.tstfct + + vals = { + 'f0': 'Test create', + } + idnew = tstfct.create(cr, uid, vals, context=context) + tst = tstfct.browse(cr, uid, idnew, context=context) + + self.assertEqual(tst.f1, 'create') + + vals = { + 'f0': 'Test write', + } + tstfct.write(cr, uid, idnew, vals, context=context) + + self.assertEqual(tst.f1, 'write') diff --git a/openerp/models.py b/openerp/models.py index 35944ff4873..b0c7762bbd7 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -4007,18 +4007,22 @@ class BaseModel(object): done = {} recs.env.recompute_old.extend(result) - for order, model_name, ids_to_update, fields_to_recompute in sorted(recs.env.recompute_old): - key = (model_name, tuple(fields_to_recompute)) - done.setdefault(key, {}) - # avoid to do several times the same computation - todo = [] - for id in ids_to_update: - if id not in done[key]: - done[key][id] = True - if id not in deleted_related[model_name]: - todo.append(id) - self.pool[model_name]._store_set_values(cr, user, todo, fields_to_recompute, context) - recs.env.clear_recompute_old() + while recs.env.recompute_old: + sorted_recompute_old = sorted(recs.env.recompute_old) + recs.env.clear_recompute_old() + for __, model_name, ids_to_update, fields_to_recompute in \ + sorted_recompute_old: + key = (model_name, tuple(fields_to_recompute)) + done.setdefault(key, {}) + # avoid to do several times the same computation + todo = [] + for id in ids_to_update: + if id not in done[key]: + done[key][id] = True + if id not in deleted_related[model_name]: + todo.append(id) + self.pool[model_name]._store_set_values( + cr, user, todo, fields_to_recompute, context) # recompute new-style fields if recs.env.recompute and context.get('recompute', True): @@ -4275,11 +4279,15 @@ class BaseModel(object): if recs.env.recompute and context.get('recompute', True): done = [] - for order, model_name, ids, fields2 in sorted(recs.env.recompute_old): - if not (model_name, ids, fields2) in done: - self.pool[model_name]._store_set_values(cr, user, ids, fields2, context) - done.append((model_name, ids, fields2)) - recs.env.clear_recompute_old() + while recs.env.recompute_old: + sorted_recompute_old = sorted(recs.env.recompute_old) + recs.env.clear_recompute_old() + for __, model_name, ids, fields2 in sorted_recompute_old: + if not (model_name, ids, fields2) in done: + self.pool[model_name]._store_set_values( + cr, user, ids, fields2, context) + done.append((model_name, ids, fields2)) + # recompute new-style fields recs.recompute()