[FIX] web: domain field: quick save after debug edit
When a domain field value is edited via the debug textarea, no search_count is done for performance reasons. A single exception is done when saving the record. Then we check the validity of the domain created in the debug textarea in order to avoid to save an invalid domain in db (and get tracebacks,..). The problem is that a search_count can take a very long time to be executed if the domain is valid. Here we introduce a route /web/domain/validate in order to quickly check the validity of a domain and use it in domain field in order to fix the above mentionned performance issue. Note that the search_count is still done if it useful but does not have to be waited anymore. X-original-commit: 40288221c39ff8fba41cd6ac231ab33600dc0cd4 Part-of: odoo/odoo#128913 Co-authored-by: Oliver Dony <odo@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
co-authored by
Oliver Dony
parent
88eaa7362a
commit
afdb546e44
@@ -4,6 +4,7 @@ from . import action
|
||||
from . import binary
|
||||
from . import database
|
||||
from . import dataset
|
||||
from . import domain
|
||||
from . import export
|
||||
from . import home
|
||||
from . import pivot
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import Controller, request
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.misc import mute_logger
|
||||
|
||||
class Domain(Controller):
|
||||
|
||||
@http.route('/web/domain/validate', type='json', auth="user")
|
||||
def validate(self, model, domain):
|
||||
""" Parse `domain` and verify that it can be used to search on `model`
|
||||
:return: True when the domain is valid, otherwise False
|
||||
:raises ValidationError: if `model` is invalid
|
||||
"""
|
||||
Model = request.env.get(model)
|
||||
if Model is None:
|
||||
raise ValidationError(_('Invalid model: %s', model))
|
||||
try:
|
||||
# go through the motions of preparing the final SQL for the domain,
|
||||
# so that anything invalid will raise an exception.
|
||||
query = Model.sudo()._search(domain)
|
||||
sql, params = query.select()
|
||||
|
||||
# Execute the search in EXPLAIN mode, to have the query parser
|
||||
# verify it. EXPLAIN will make sure the query is never actually executed
|
||||
# An alternative to EXPLAIN would be a LIMIT 0 clause, but the semantics
|
||||
# of a falsy `limit` parameter when calling _search() do not permit it.
|
||||
with mute_logger('odoo.sql_db'):
|
||||
request.env.cr.execute(f"EXPLAIN {sql}", params)
|
||||
return True
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return False
|
||||
@@ -27,6 +27,7 @@ export class DomainField extends Component {
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
this.orm = useService("orm");
|
||||
this.addDialog = useOwnedDialogs();
|
||||
|
||||
@@ -50,8 +51,12 @@ export class DomainField extends Component {
|
||||
if (this.isDebugEdited) {
|
||||
const props = this.props;
|
||||
ev.detail.proms.push(
|
||||
this.checkProps(props).then(() => {
|
||||
if (!this.state.isValid) {
|
||||
this.quickValidityCheck(props).then((isValid) => {
|
||||
if (isValid) {
|
||||
this.isDebugEdited = false; // will allow the count to be loaded if needed
|
||||
} else {
|
||||
this.state.isValid = false;
|
||||
this.state.recordCount = 0;
|
||||
props.record.setInvalidField(props.name);
|
||||
}
|
||||
})
|
||||
@@ -157,6 +162,18 @@ export class DomainField extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
async quickValidityCheck(props) {
|
||||
const resModel = this.getResModel(props);
|
||||
if (!resModel) {
|
||||
return false;
|
||||
}
|
||||
const domain = this.getEvaluatedDomain(props);
|
||||
if (domain.isInvalid) {
|
||||
return false;
|
||||
}
|
||||
return this.rpc("/web/domain/validate", { model: resModel, domain });
|
||||
}
|
||||
|
||||
update(domain, isDebugEdited = false) {
|
||||
this.isDebugEdited = isDebugEdited;
|
||||
return this.props.record.update({ [this.props.name]: domain });
|
||||
|
||||
@@ -497,6 +497,9 @@ QUnit.module("Fields", (hooks) => {
|
||||
if (method === "search_count") {
|
||||
assert.step(JSON.stringify(args[0]));
|
||||
}
|
||||
if (route === "/web/domain/validate") {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -562,6 +565,9 @@ QUnit.module("Fields", (hooks) => {
|
||||
if (method === "write") {
|
||||
throw new Error("should not save");
|
||||
}
|
||||
if (route === "/web/domain/validate") {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -735,6 +741,9 @@ QUnit.module("Fields", (hooks) => {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(args[1].foo, rawDomain);
|
||||
}
|
||||
if (route === "/web/domain/validate") {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -942,4 +951,42 @@ QUnit.module("Fields", (hooks) => {
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
assert.containsOnce(target, ".modal", "the domain is invalid: the dialog is not closed");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"quick check on save if domain has been edited via the debug input",
|
||||
async function (assert) {
|
||||
patchWithCleanup(odoo, { debug: true });
|
||||
serverData.models.partner.fields.display_name.default = "[['id', '=', False]]";
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="display_name" widget="domain" options="{'model': 'partner'}"/>
|
||||
</form>`,
|
||||
mockRPC: (route, args) => {
|
||||
if (route === "/web/domain/validate") {
|
||||
assert.step(route);
|
||||
assert.deepEqual(args, {
|
||||
domain: [["id", "!=", false]],
|
||||
model: "partner",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
|
||||
"0 record(s)"
|
||||
);
|
||||
await editInput(target, ".o_domain_debug_input", "[['id', '!=', False]]");
|
||||
await click(target, "button.o_form_button_save");
|
||||
assert.verifySteps(["/web/domain/validate"]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
|
||||
"6 record(s)"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,5 +16,6 @@ from . import test_assets
|
||||
from . import test_assets_xml
|
||||
from . import test_login
|
||||
from . import test_web_search_read
|
||||
from . import test_domain
|
||||
from . import test_web_redirect
|
||||
from . import test_res_users
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class DomainTest(HttpCase):
|
||||
|
||||
def test_domain_validate(self):
|
||||
self.authenticate("demo", "demo")
|
||||
|
||||
with mute_logger('odoo.http'):
|
||||
resp = self.url_open(
|
||||
'/web/domain/validate',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({'params': {'model':'i', 'domain':[]}}),
|
||||
)
|
||||
self.assertEqual(resp.json()['error']['data']['message'], "Invalid model: i")
|
||||
|
||||
resp = self.url_open(
|
||||
'/web/domain/validate',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({'params': {'model':'res.users', 'domain':[]}}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'], True)
|
||||
|
||||
resp = self.url_open(
|
||||
'/web/domain/validate',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({'params': {'model':'res.users', 'domain':[('name', 'ilike', 'ad')]}}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'], True)
|
||||
|
||||
resp = self.url_open(
|
||||
'/web/domain/validate',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({'params': {'model':'res.users', 'domain':[('hop')]}}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'], False)
|
||||
Reference in New Issue
Block a user