details: https://code.tryton.org/tryton/commit/6b8e5ff7a275
branch: default
user: Cédric Krier <[email protected]>
date: Wed Jun 10 19:10:53 2026 +0200
description:
Complete selection value in domain parser using contains matcher
Closes #14886
diffstat:
sao/src/common.js | 25 +++++++++++++----------
sao/tests/index.html | 1 +
sao/tests/sao.js | 23 ++++++++++++++++++++-
tryton/tryton/common/domain_parser.py | 17 +++++++++++++--
tryton/tryton/tests/test_common_domain_parser.py | 24 +++++++++++++++++++---
5 files changed, 70 insertions(+), 20 deletions(-)
diffs (217 lines):
diff -r a1a73f478a9e -r 6b8e5ff7a275 sao/src/common.js
--- a/sao/src/common.js Thu Jun 18 16:28:03 2026 +0200
+++ b/sao/src/common.js Wed Jun 10 19:10:53 2026 +0200
@@ -1588,6 +1588,15 @@
}
return results;
},
+ contains_without_diacritics: function(main, sub) {
+ main = main.normalize('NFD').toLowerCase();
+ sub = sub.normalize('NFD').toLowerCase();
+
+ main = main.replace(/[\u0300-\u036f]/g, '');
+ sub = sub.replace(/[\u0300-\u036f]/g, '');
+
+ return main.includes(sub);
+ },
complete_value: function(field, value) {
var complete_boolean = function() {
if ((value === null) || (value === undefined)) {
@@ -1599,18 +1608,15 @@
}
};
- var complete_selection = function() {
+ var complete_selection = () => {
var results = [];
var test_value = value !== null ? value : '';
if (value instanceof Array) {
test_value = value[value.length - 1] || '';
}
test_value = test_value.replace(/^%*|%*$/g, '');
- var i, len, svalue, test;
- for (i=0, len=field.selection.length; i<len; i++) {
- svalue = field.selection[i][0];
- test = field.selection[i][1].toLowerCase();
- if (test.startsWith(test_value.toLowerCase())) {
+ for(let [svalue, test] of field.selection) {
+ if (this.contains_without_diacritics(test, test_value)) {
if (value instanceof Array) {
results.push(value.slice(0, -1).concat([svalue]));
} else {
@@ -1628,11 +1634,8 @@
test_value = value[value.length - 1];
}
test_value = test_value.replace(/^%*|%*$/g, '');
- var i, len, svalue, test;
- for (i=0, len=field.selection.length; i<len; i++) {
- svalue = field.selection[i][0];
- test = field.selection[i][1].toLowerCase();
- if (test.startsWith(test_value.toLowerCase())) {
+ for (let [svalue, test] of field.selection) {
+ if (this.contains_without_diacritics(test, test_value)) {
if (value instanceof Array) {
results.push(value.slice(0, -1).concat([svalue]));
} else {
diff -r a1a73f478a9e -r 6b8e5ff7a275 sao/tests/index.html
--- a/sao/tests/index.html Thu Jun 18 16:28:03 2026 +0200
+++ b/sao/tests/index.html Wed Jun 10 19:10:53 2026 +0200
@@ -3,6 +3,7 @@
this repository contains the full copyright notices and license terms. -->
<html>
<head>
+ <meta charset="UTF-8">
<title>Sao Test Suite</title>
<script type="text/javascript"
src="../node_modules/jquery/dist/jquery.js"></script>
<script type="text/javascript"
src="../node_modules/bootstrap/dist/js/bootstrap.js"></script>
diff -r a1a73f478a9e -r 6b8e5ff7a275 sao/tests/sao.js
--- a/sao/tests/sao.js Thu Jun 18 16:28:03 2026 +0200
+++ b/sao/tests/sao.js Wed Jun 10 19:10:53 2026 +0200
@@ -2427,6 +2427,25 @@
});
});
+ QUnit.test('DomainParser.contains_without_diacritics', function() {
+ let parser = new Sao.common.DomainParser();
+ for (let [main, sub, expected] of [
+ ["Café au lait", "cafe", true],
+ ["Café au lait", "café", true],
+ ["Café au lait", "lait", true],
+ ["Café au lait", "the", false],
+ ["Héllò Wörld", "hello", true],
+ ["Héllò Wörld", "world", true],
+ ["Héllò Wörld", "hallo", false],
+ ]) {
+ QUnit.assert.strictEqual(
+ parser.contains_without_diacritics(main, sub), expected,
+ 'contains_without_diacritics('
+ + JSON.stringify(main) + ',' + JSON.stringify(sub) + ')');
+ }
+
+ });
+
QUnit.test('DomainParser.complete_value', function () {
var parser = new Sao.common.DomainParser();
var field;
@@ -2456,7 +2475,7 @@
],
};
[
- ['m', ['male']],
+ ['f', ['female']],
['test', []],
['', ['male', 'female']],
[null, ['male', 'female']],
@@ -2473,7 +2492,7 @@
],
};
[
- ['m', ['male']],
+ ['f', ['female']],
['test', []],
['', ['male', 'female', '']],
[null, ['male', 'female', '']],
diff -r a1a73f478a9e -r 6b8e5ff7a275 tryton/tryton/common/domain_parser.py
--- a/tryton/tryton/common/domain_parser.py Thu Jun 18 16:28:03 2026 +0200
+++ b/tryton/tryton/common/domain_parser.py Wed Jun 10 19:10:53 2026 +0200
@@ -6,6 +6,7 @@
import io
import locale
import math
+import unicodedata
from collections import OrderedDict
from decimal import Decimal
from shlex import shlex
@@ -426,6 +427,16 @@
lambda: value if value is not None else '')(), empty=_quote_empty)
+def contains_without_diacritics(main, sub):
+ main = unicodedata.normalize('NFD', main.lower())
+ sub = unicodedata.normalize('NFD', sub.lower())
+
+ main = ''.join(c for c in main if not unicodedata.combining(c))
+ sub = ''.join(c for c in sub if not unicodedata.combining(c))
+
+ return sub in main
+
+
def complete_value(field, value):
"Complete value for field"
@@ -444,7 +455,7 @@
test_value = value[-1] or ''
test_value = test_value.strip('%')
for svalue, test in field['selection']:
- if test.lower().startswith(test_value.lower()):
+ if contains_without_diacritics(test, test_value):
if isinstance(value, list):
yield value[:-1] + [svalue]
else:
@@ -454,9 +465,9 @@
test_value = value if value is not None else ''
if isinstance(value, list):
test_value = value[-1]
- test_value = test_value.strip('%')
+ test_value = test_value.strip('%').lower()
for svalue, test in field['selection']:
- if test.lower().startswith(test_value.lower()):
+ if contains_without_diacritics(test, test_value):
if isinstance(value, list):
yield value[:-1] + [svalue]
else:
diff -r a1a73f478a9e -r 6b8e5ff7a275
tryton/tryton/tests/test_common_domain_parser.py
--- a/tryton/tryton/tests/test_common_domain_parser.py Thu Jun 18 16:28:03
2026 +0200
+++ b/tryton/tryton/tests/test_common_domain_parser.py Wed Jun 10 19:10:53
2026 +0200
@@ -6,8 +6,9 @@
from tryton.common import untimezoned_date
from tryton.common.domain_parser import (
- DomainParser, complete_value, convert_value, format_value, group_operator,
- likify, operatorize, parenthesize, quote, rlist, split_target_value, udlex)
+ DomainParser, complete_value, contains_without_diacritics, convert_value,
+ format_value, group_operator, likify, operatorize, parenthesize, quote,
+ rlist, split_target_value, udlex)
class DomainParserTestCase(TestCase):
@@ -433,6 +434,21 @@
format_value(field, value), result,
msg="format_value(%r, %r)" % (field, value))
+ def test_contains_without_diacritics(self):
+ "Test contains without diacritics"
+ for main, sub, expected in [
+ ("Café au lait", "cafe", True),
+ ("Café au lait", "café", True),
+ ("Café au lait", "lait", True),
+ ("Café au lait", "the", False),
+ ("Héllò Wörld", "hello", True),
+ ("Héllò Wörld", "world", True),
+ ("Héllò Wörld", "hallo", False),
+ ]:
+ with self.subTest(main=main, sub=sub):
+ self.assertEqual(
+ contains_without_diacritics(main, sub), expected)
+
def test_complete_boolean(self):
"Test complete boolean"
field = {
@@ -457,7 +473,7 @@
],
}
for value, result in (
- ('m', ['male']),
+ ('f', ['female']),
('test', []),
('', ['male', 'female']),
(None, ['male', 'female']),
@@ -472,7 +488,7 @@
field_with_empty['selection'] = (field_with_empty['selection']
+ [('', '')])
for value, result in (
- ('m', ['male']),
+ ('f', ['female']),
('test', []),
('', ['male', 'female', '']),
(None, ['male', 'female', '']),