[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:
Mathieu Duckerts-Antoine
2023-07-19 11:40:09 +02:00
co-authored by Oliver Dony
parent 88eaa7362a
commit afdb546e44
6 changed files with 144 additions and 2 deletions
+1
View File
@@ -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
+34
View File
@@ -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)"
);
}
);
});
+1
View File
@@ -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
+42
View File
@@ -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)