[FIX] core: calling read_group / name_search over xmlrpc
Also non-browser jsonrpc (as it goes through a similar process): for internal performance reasons, name_search and read_group have been converted to a *lazy* name_get, so the "display name" is not unnecessarily computed. However this is an issue for the RPC endpoints (/xmlrpc and /jsonrpc) as they have no support for `lazy` and thus tend to blow up and / or do the wrong thing when trying to output a lazy: * xmlrpc has no way to handle lazy at all and straight blows up * jsonrpc falls back to `json_default` so they try to stringify the lazy, which might have worked except *Problematically* both endpoints delegate the actual work to `dispatch_rpc` which handles dispatching between various services and ultimately creates a *new* cursor before calling model methods (`object` service and `execute`/`execute_kw`). This means by the time the result is serialized to be output, the lazy's cursor has long been closed, and thus any access to an unevaluated `lazy` errors out when trying to fetch the underlying item. This also means we can't just add a hook to serialize the lazy in the xmlrpc marshaller, though we do have to do that. We *also* (for both xmlrpc and jsonrpc) have to force evluation of lazy values before our cursor is closed, meaning it has to be done right after the method is invoked, iterating the entire response. Related to task 2170343 closes odoo/odoo#49286 X-original-commit: e2b5a359c1d5eccbe725c1c3169b4130d7bca49b Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
This commit is contained in:
@@ -7,6 +7,7 @@ from werkzeug.wrappers import Response
|
||||
from odoo.http import Controller, dispatch_rpc, request, route
|
||||
from odoo.service import wsgi_server
|
||||
from odoo.fields import Date, Datetime
|
||||
from odoo.tools import lazy
|
||||
|
||||
|
||||
class OdooMarshaller(xmlrpc.client.Marshaller):
|
||||
@@ -28,6 +29,11 @@ class OdooMarshaller(xmlrpc.client.Marshaller):
|
||||
self.dump_unicode(value, write)
|
||||
dispatch[date] = dump_date
|
||||
|
||||
def dump_lazy(self, value, write):
|
||||
v = value._value
|
||||
return self.dispatch[type(v)](self, v, write)
|
||||
dispatch[lazy] = dump_lazy
|
||||
|
||||
|
||||
# monkey-patch xmlrpc.client's marshaller
|
||||
xmlrpc.client.Marshaller = OdooMarshaller
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tools
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
@@ -25,3 +26,41 @@ class TestXMLRPC(common.HttpCase):
|
||||
self.assertIsInstance(ids, list)
|
||||
ids = o.execute(db_name, self.admin_uid, 'admin', 'ir.model', 'search', [], {})
|
||||
self.assertIsInstance(ids, list)
|
||||
|
||||
def test_xmlrpc_read_group(self):
|
||||
groups = self.xmlrpc_object.execute(
|
||||
common.get_db_name(), self.admin_uid, 'admin',
|
||||
'res.partner', 'read_group', [], ['is_company', 'color'], ['parent_id']
|
||||
)
|
||||
|
||||
def test_xmlrpc_name_search(self):
|
||||
self.xmlrpc_object.execute(
|
||||
common.get_db_name(), self.admin_uid, 'admin',
|
||||
'res.partner', 'name_search', "admin"
|
||||
)
|
||||
|
||||
def test_jsonrpc_read_group(self):
|
||||
self._json_call(
|
||||
common.get_db_name(), self.admin_uid, 'admin',
|
||||
'res.partner', 'read_group', [], ['is_company', 'color'], ['parent_id']
|
||||
)
|
||||
|
||||
def test_jsonrpc_name_search(self):
|
||||
# well that's some sexy sexy call right there
|
||||
self._json_call(
|
||||
common.get_db_name(),
|
||||
self.admin_uid, 'admin',
|
||||
'res.partner', 'name_search', 'admin'
|
||||
)
|
||||
|
||||
def _json_call(self, *args):
|
||||
self.opener.post("http://%s:%s/jsonrpc" % (common.HOST, odoo.tools.config['http_port']), json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': None,
|
||||
'method': 'call',
|
||||
'params': {
|
||||
'service': 'object',
|
||||
'method': 'execute',
|
||||
'args': args
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
|
||||
from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
|
||||
from odoo.tests.common import TransactionCase, users, warmup, tagged
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tools import mute_logger, json_default
|
||||
|
||||
|
||||
class TestPerformance(SavepointCaseWithUserDemo):
|
||||
@@ -447,7 +447,7 @@ class TestPerformance(SavepointCaseWithUserDemo):
|
||||
self.assertEqual(res['value'], exp['value'])
|
||||
# now serialize to json, which should force evaluation
|
||||
with self.assertQueryCount(__system__=1, demo=1):
|
||||
json.dumps(result)
|
||||
json.dumps(result, default=json_default)
|
||||
|
||||
|
||||
@tagged('bacon_and_eggs')
|
||||
|
||||
@@ -15,6 +15,7 @@ from odoo.tools.translate import translate, translate_sql_constraint
|
||||
from odoo.tools.translate import _
|
||||
|
||||
from . import security
|
||||
from ..tools import traverse_containers, lazy
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -155,7 +156,12 @@ def execute_cr(cr, uid, obj, method, *args, **kw):
|
||||
recs = odoo.api.Environment(cr, uid, {}).get(obj)
|
||||
if recs is None:
|
||||
raise UserError(_("Object %s doesn't exist") % obj)
|
||||
return odoo.api.call_kw(recs, method, args, kw)
|
||||
result = odoo.api.call_kw(recs, method, args, kw)
|
||||
# force evaluation of lazy values before the cursor is closed, as it would
|
||||
# error afterwards if the lazy isn't already evaluated (and cached)
|
||||
for l in traverse_containers(result, lazy):
|
||||
_0 = l._value
|
||||
return result
|
||||
|
||||
|
||||
def execute_kw(db, uid, obj, method, args, kw=None):
|
||||
|
||||
@@ -4,8 +4,9 @@ import calendar
|
||||
from datetime import date, datetime, time
|
||||
import pytz
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from . import ustr
|
||||
|
||||
from . import ustr
|
||||
from .func import lazy
|
||||
|
||||
def get_month(date):
|
||||
''' Compute the month dates range on which the 'date' parameter belongs to.
|
||||
@@ -198,16 +199,17 @@ def subtract(value, *args, **kwargs):
|
||||
"""
|
||||
return value - relativedelta(*args, **kwargs)
|
||||
|
||||
|
||||
def json_default(obj):
|
||||
"""
|
||||
Properly serializes date and datetime objects.
|
||||
"""
|
||||
from odoo import fields
|
||||
if isinstance(obj, datetime):
|
||||
return fields.Datetime.to_string(obj)
|
||||
if isinstance(obj, date):
|
||||
if isinstance(obj, datetime):
|
||||
return fields.Datetime.to_string(obj)
|
||||
return fields.Date.to_string(obj)
|
||||
if isinstance(obj, lazy):
|
||||
return obj._value
|
||||
return ustr(obj)
|
||||
|
||||
|
||||
|
||||
@@ -242,14 +242,3 @@ class lazy(object):
|
||||
def __aenter__(self): return self._value.__aenter__()
|
||||
def __aexit__(self, exc_type, exc_value, traceback):
|
||||
return self._value.__aexit__(exc_type, exc_value, traceback)
|
||||
|
||||
|
||||
# patch serialization of lazy
|
||||
def default(self, o):
|
||||
if isinstance(o, lazy):
|
||||
return o._value
|
||||
return json_encoder_default(self, o)
|
||||
|
||||
|
||||
json_encoder_default = JSONEncoder.default
|
||||
JSONEncoder.default = default
|
||||
|
||||
@@ -1507,3 +1507,21 @@ def get_diff(data_from, data_to, custom_style=False):
|
||||
numlines=3,
|
||||
)
|
||||
return handle_style(diff, custom_style)
|
||||
|
||||
|
||||
def traverse_containers(val, type_):
|
||||
""" Yields atoms filtered by specified type_ (or type tuple), traverses
|
||||
through standard containers (non-string mappings or sequences) *unless*
|
||||
they're selected by the type filter
|
||||
"""
|
||||
if isinstance(val, type_):
|
||||
yield val
|
||||
elif isinstance(val, (str, bytes)):
|
||||
return
|
||||
elif isinstance(val, Mapping):
|
||||
for k, v in val.items():
|
||||
yield from traverse_containers(k, type_)
|
||||
yield from traverse_containers(v, type_)
|
||||
elif isinstance(val, collections.abc.Sequence):
|
||||
for v in val:
|
||||
yield from traverse_containers(v, type_)
|
||||
|
||||
Reference in New Issue
Block a user