Add a canonicalize function for domains
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Add a canonicalize function for domains
|
||||
* Add menu to copy selected record from tree view to clipboard
|
||||
* Support PYSON comparison of timedelta
|
||||
* Support encoding timedelta into PYSON TimeDelta
|
||||
|
||||
@@ -2436,6 +2436,63 @@
|
||||
e => this.localize_domain(e, field_name, strip_target));
|
||||
}
|
||||
},
|
||||
|
||||
_sort_key: function(domain) {
|
||||
if (!domain.length) {
|
||||
return [0, domain];
|
||||
} else if (this.is_leaf(domain)) {
|
||||
if (domain[2] === null) {
|
||||
domain = domain.slice();
|
||||
domain[2] = Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
return [1, domain];
|
||||
} else if (~['AND', 'OR'].indexOf(domain)) {
|
||||
return [0, domain];
|
||||
} else {
|
||||
var content = domain.map(this._sort_key.bind(this));
|
||||
var nestedness = Math.max(...content.map(e => e[0]));
|
||||
return [nestedness + 1, content];
|
||||
}
|
||||
},
|
||||
|
||||
_domain_compare: function(d1, d2) {
|
||||
if ((d1 instanceof Array) && (d2 instanceof Array)) {
|
||||
var elem_comparison;
|
||||
var min_len = Math.min(d1.length, d2.length);
|
||||
for (var i = 0; i < min_len; i++) {
|
||||
elem_comparison = this._domain_compare(d1[i], d2[i]);
|
||||
if (elem_comparison != 0) {
|
||||
return elem_comparison;
|
||||
}
|
||||
}
|
||||
if (d1.length == d2.length) {
|
||||
return 0;
|
||||
} else {
|
||||
return d1.length < d2.length ? -1 : 1;
|
||||
}
|
||||
} else if (d1 == d2) {
|
||||
return 0;
|
||||
} else {
|
||||
return d1 < d2 ? -1 : 1;
|
||||
}
|
||||
},
|
||||
|
||||
sort: function(domain) {
|
||||
if (!domain.length) {
|
||||
return [];
|
||||
} else if (this.is_leaf(domain)) {
|
||||
return domain;
|
||||
} else if (~['AND', 'OR'].indexOf(domain)) {
|
||||
return domain;
|
||||
} else {
|
||||
var sorted_elements = domain.map(this.sort.bind(this));
|
||||
sorted_elements.sort(
|
||||
(d1, d2) => this._domain_compare(this._sort_key(d1), this._sort_key(d2))
|
||||
);
|
||||
return sorted_elements;
|
||||
}
|
||||
},
|
||||
|
||||
prepare_reference_domain: function(domain, reference) {
|
||||
|
||||
var value2reference = function(value) {
|
||||
@@ -2649,6 +2706,25 @@
|
||||
return this.simplify_nested(this.simplify_duplicate(domain));
|
||||
}
|
||||
},
|
||||
simplify_AND: function(domain) {
|
||||
if (this.is_leaf(domain)) {
|
||||
return domain;
|
||||
} else if (domain == 'OR') {
|
||||
return domain;
|
||||
} else {
|
||||
var simplified = [];
|
||||
for (const e of domain) {
|
||||
if (e == 'AND') {
|
||||
continue;
|
||||
}
|
||||
simplified.push(this.simplify_AND(e));
|
||||
}
|
||||
return simplified;
|
||||
}
|
||||
},
|
||||
canonicalize: function(domain) {
|
||||
return this.simplify_AND(this.sort(this.simplify(domain)));
|
||||
},
|
||||
merge: function(domain, domoperator) {
|
||||
if (jQuery.isEmptyObject(domain) ||
|
||||
~['AND', 'OR'].indexOf(domain)) {
|
||||
|
||||
@@ -3116,6 +3116,65 @@
|
||||
[]));
|
||||
});
|
||||
|
||||
QUnit.test('DomainInversion.sort', function() {
|
||||
var domain_inversion = new Sao.common.DomainInversion();
|
||||
var sort = domain_inversion.sort.bind(domain_inversion);
|
||||
var tests = [
|
||||
[[], []],
|
||||
[['AND'], ['AND']],
|
||||
[['OR'], ['OR']],
|
||||
[[['foo', '=', 1]], [['foo', '=', 1]]],
|
||||
[[['foo', '=', 1000], ['foo', '=', 0]],
|
||||
[['foo', '=', 0], ['foo', '=', 1000]]],
|
||||
[[['foo', '=', 1], ['foo', '=', null]],
|
||||
[['foo', '=', null], ['foo', '=', 1]]],
|
||||
[[['foo', '=', 1], []], [[], ['foo', '=', 1]]],
|
||||
[[[[[['foo', '=', 1]]]], [[['foo', '=', 1]]]],
|
||||
[[[['foo', '=', 1]]], [[[['foo', '=', 1]]]]]],
|
||||
[[[['foo', '=', 1], ['bar', '=', 2]],
|
||||
[['foo', '=', 1], ['bar', '=', 2], ['baz', '=', 3]]],
|
||||
[[['bar', '=', 2], ['baz', '=', 3], ['foo', '=', 1]],
|
||||
[['bar', '=', 2], ['foo', '=', 1]]]],
|
||||
[['OR', ['foo', '=', 1], ['foo', '=', null],
|
||||
['AND', ['bar', '=', 2], ['baz', '=', 3]]],
|
||||
['OR', ['foo', '=', null], ['foo', '=', 1],
|
||||
['AND', ['bar', '=', 2], ['baz', '=', 3]]]],
|
||||
];
|
||||
for (const [domain, expected] of tests) {
|
||||
QUnit.deepEqual(expected, sort(domain));
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('DomainInversion.canonicalize', function() {
|
||||
var domain_inversion = new Sao.common.DomainInversion();
|
||||
var canonicalize = domain_inversion.canonicalize.bind(domain_inversion);
|
||||
var tests = [
|
||||
[[], []],
|
||||
[['AND'], []],
|
||||
[['OR'], []],
|
||||
[[['foo', '=', 1], ['bar', '=', 2]],
|
||||
[['bar', '=', 2], ['foo', '=', 1]]],
|
||||
[[[['foo', '=', 1]]], [['foo', '=', 1]]],
|
||||
[['AND', ['OR', ['bar', '=', 2], ['baz', '=', 3]],
|
||||
['foo', '=', 1]],
|
||||
[['foo', '=', 1],
|
||||
['OR', ['bar', '=', 2], ['baz', '=', 3]]]],
|
||||
[['OR', ['AND', ['bar', '=', 2], ['baz', '=', 3]],
|
||||
['foo', '=', 1]],
|
||||
['OR', ['foo', '=', 1],
|
||||
[['bar', '=', 2], ['baz', '=', 3]]]],
|
||||
[['AND', ['AND', ['bar', '=', 2], ['baz', '=', 3]],
|
||||
['foo', '=', 1]],
|
||||
[['bar', '=', 2], ['baz', '=', 3], ['foo', '=', 1]]],
|
||||
[['OR', ['OR', ['bar', '=', 2], ['baz', '=', 3]],
|
||||
['foo', '=', 1]],
|
||||
['OR', ['bar', '=', 2], ['baz', '=', 3], ['foo', '=', 1]]],
|
||||
];
|
||||
for (const [domain, expected] of tests) {
|
||||
QUnit.deepEqual(expected, canonicalize(domain));
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('DomainParser.completion', function() {
|
||||
var compare = Sao.common.compare;
|
||||
var parser = new Sao.common.DomainParser({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
* Add a canonicalize function for domains
|
||||
* Support PYSON comparison of timedelta
|
||||
* Support encoding timedelta into PYSON TimeDelta
|
||||
* Transfer configuration, profiles and plugins from previous version
|
||||
|
||||
@@ -284,6 +284,34 @@ def localize_domain(domain, field_name=None, strip_target=False):
|
||||
for part in domain]
|
||||
|
||||
|
||||
def _sort_key(domain):
|
||||
if not domain:
|
||||
return (0, tuple())
|
||||
elif is_leaf(domain):
|
||||
if domain[2] is None:
|
||||
domain = list(domain).copy()
|
||||
domain[2] = float('-inf')
|
||||
return (1, tuple(domain))
|
||||
elif domain in ['AND', 'OR']:
|
||||
return (0, domain)
|
||||
else:
|
||||
content = tuple(_sort_key(e) for e in domain)
|
||||
nestedness = max(k[0] for k in content)
|
||||
return (nestedness + 1, content)
|
||||
|
||||
|
||||
def sort(domain):
|
||||
"Sort a domain"
|
||||
if not domain:
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
return domain
|
||||
elif domain in ['AND', 'OR']:
|
||||
return domain
|
||||
else:
|
||||
return sorted((sort(e) for e in domain), key=_sort_key)
|
||||
|
||||
|
||||
def bool_operator(domain):
|
||||
"Returns the boolean operator used by a domain"
|
||||
bool_op = 'AND'
|
||||
@@ -340,6 +368,16 @@ def simplify_duplicate(domain):
|
||||
return dedup_branches
|
||||
|
||||
|
||||
def simplify_AND(domain):
|
||||
"""Remove useless ANDs"""
|
||||
if is_leaf(domain):
|
||||
return domain
|
||||
elif domain == 'OR':
|
||||
return domain
|
||||
else:
|
||||
return [simplify_AND(e) for e in domain if e != 'AND']
|
||||
|
||||
|
||||
def simplify(domain):
|
||||
"""Remove duplicate expressions and useless OR/AND"""
|
||||
if is_leaf(domain):
|
||||
@@ -350,6 +388,15 @@ def simplify(domain):
|
||||
return simplify_nested(simplify_duplicate(domain))
|
||||
|
||||
|
||||
def canonicalize(domain):
|
||||
"""Returns the canonical version of a domain.
|
||||
|
||||
The canonical version of a domain is one where the domain is both
|
||||
simplified and sorted
|
||||
"""
|
||||
return simplify_AND(sort(simplify(domain)))
|
||||
|
||||
|
||||
def merge(domain, domoperator=None):
|
||||
if not domain or domain in ('AND', 'OR'):
|
||||
return []
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
* Add a canonicalize function for domains
|
||||
* Enforce record rules when reading only non SQL fields (#12428)
|
||||
* Support PYSON comparison of timedelta
|
||||
* Support encoding timedelta into PYSON TimeDelta
|
||||
|
||||
@@ -17,9 +17,9 @@ from trytond.tools import (
|
||||
reduce_ids, remove_forbidden_chars, rstrip_wildcard, slugify,
|
||||
sortable_values, strip_wildcard, timezone, unescape_wildcard)
|
||||
from trytond.tools.domain_inversion import (
|
||||
concat, domain_inversion, eval_domain, extract_reference_models,
|
||||
localize_domain, merge, parse, prepare_reference_domain, simplify,
|
||||
unique_value)
|
||||
canonicalize, concat, domain_inversion, eval_domain,
|
||||
extract_reference_models, localize_domain, merge, parse,
|
||||
prepare_reference_domain, simplify, sort, unique_value)
|
||||
from trytond.tools.immutabledict import ImmutableDict
|
||||
from trytond.tools.logging import format_args
|
||||
from trytond.tools.string_ import LazyString, StringPartitioned
|
||||
@@ -1099,6 +1099,59 @@ class DomainInversionTestCase(unittest.TestCase):
|
||||
extract_reference_models(domain, 'x'), {'model_A', 'model_B'})
|
||||
self.assertEqual(extract_reference_models(domain, 'y'), set())
|
||||
|
||||
def test_sort(self):
|
||||
"Test sorting domain"
|
||||
for (domain, expected) in [
|
||||
([], []),
|
||||
(['AND'], ['AND']),
|
||||
(['OR'], ['OR']),
|
||||
([('foo', '=', 1)], [('foo', '=', 1)]),
|
||||
([('foo', '=', 1000), ('foo', '=', 0)],
|
||||
[('foo', '=', 0), ('foo', '=', 1000)]),
|
||||
([('foo', '=', 1), ('foo', '=', None)],
|
||||
[('foo', '=', None), ('foo', '=', 1)]),
|
||||
([('foo', '=', 1), []], [[], ('foo', '=', 1)]),
|
||||
([[[[('foo', '=', 1)]]], [[('foo', '=', 1)]]],
|
||||
[[[('foo', '=', 1)]], [[[('foo', '=', 1)]]]]),
|
||||
([[('foo', '=', 1), ('bar', '=', 2)],
|
||||
[('foo', '=', 1), ('bar', '=', 2), ('baz', '=', 3)]],
|
||||
[[('bar', '=', 2), ('baz', '=', 3), ('foo', '=', 1)],
|
||||
[('bar', '=', 2), ('foo', '=', 1)]]),
|
||||
(['OR', ('foo', '=', 1), ('foo', '=', None),
|
||||
['AND', ('bar', '=', 2), ('baz', '=', 3)]],
|
||||
['OR', ('foo', '=', None), ('foo', '=', 1),
|
||||
['AND', ('bar', '=', 2), ('baz', '=', 3)]]),
|
||||
]:
|
||||
with self.subTest(domain=domain):
|
||||
self.assertEqual(sort(domain), expected)
|
||||
|
||||
def test_canonicalize(self):
|
||||
"Test domain canonicalization"
|
||||
for (domain, expected) in [
|
||||
([], []),
|
||||
(['AND'], []),
|
||||
(['OR'], []),
|
||||
([('foo', '=', 1), ('bar', '=', 2)],
|
||||
[('bar', '=', 2), ('foo', '=', 1)]),
|
||||
([[('foo', '=', 1)]], [('foo', '=', 1)]),
|
||||
(['AND', ['OR', ('bar', '=', 2), ('baz', '=', 3)],
|
||||
('foo', '=', 1)],
|
||||
[('foo', '=', 1),
|
||||
['OR', ('bar', '=', 2), ('baz', '=', 3)]]),
|
||||
(['OR', ['AND', ('bar', '=', 2), ('baz', '=', 3)],
|
||||
('foo', '=', 1)],
|
||||
['OR', ('foo', '=', 1),
|
||||
[('bar', '=', 2), ('baz', '=', 3)]]),
|
||||
(['AND', ['AND', ('bar', '=', 2), ('baz', '=', 3)],
|
||||
('foo', '=', 1)],
|
||||
[('bar', '=', 2), ('baz', '=', 3), ('foo', '=', 1)]),
|
||||
(['OR', ['OR', ('bar', '=', 2), ('baz', '=', 3)],
|
||||
('foo', '=', 1)],
|
||||
['OR', ('bar', '=', 2), ('baz', '=', 3), ('foo', '=', 1)]),
|
||||
]:
|
||||
with self.subTest(domain=domain):
|
||||
self.assertEqual(canonicalize(domain), expected)
|
||||
|
||||
|
||||
@unittest.skipUnless(barcode, "required barcode")
|
||||
class BarcodeTestCase(unittest.TestCase):
|
||||
|
||||
@@ -284,6 +284,34 @@ def localize_domain(domain, field_name=None, strip_target=False):
|
||||
for part in domain]
|
||||
|
||||
|
||||
def _sort_key(domain):
|
||||
if not domain:
|
||||
return (0, tuple())
|
||||
elif is_leaf(domain):
|
||||
if domain[2] is None:
|
||||
domain = list(domain).copy()
|
||||
domain[2] = float('-inf')
|
||||
return (1, tuple(domain))
|
||||
elif domain in ['AND', 'OR']:
|
||||
return (0, domain)
|
||||
else:
|
||||
content = tuple(_sort_key(e) for e in domain)
|
||||
nestedness = max(k[0] for k in content)
|
||||
return (nestedness + 1, content)
|
||||
|
||||
|
||||
def sort(domain):
|
||||
"Sort a domain"
|
||||
if not domain:
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
return domain
|
||||
elif domain in ['AND', 'OR']:
|
||||
return domain
|
||||
else:
|
||||
return sorted((sort(e) for e in domain), key=_sort_key)
|
||||
|
||||
|
||||
def bool_operator(domain):
|
||||
"Returns the boolean operator used by a domain"
|
||||
bool_op = 'AND'
|
||||
@@ -340,6 +368,16 @@ def simplify_duplicate(domain):
|
||||
return dedup_branches
|
||||
|
||||
|
||||
def simplify_AND(domain):
|
||||
"""Remove useless ANDs"""
|
||||
if is_leaf(domain):
|
||||
return domain
|
||||
elif domain == 'OR':
|
||||
return domain
|
||||
else:
|
||||
return [simplify_AND(e) for e in domain if e != 'AND']
|
||||
|
||||
|
||||
def simplify(domain):
|
||||
"""Remove duplicate expressions and useless OR/AND"""
|
||||
if is_leaf(domain):
|
||||
@@ -350,6 +388,15 @@ def simplify(domain):
|
||||
return simplify_nested(simplify_duplicate(domain))
|
||||
|
||||
|
||||
def canonicalize(domain):
|
||||
"""Returns the canonical version of a domain.
|
||||
|
||||
The canonical version of a domain is one where the domain is both
|
||||
simplified and sorted
|
||||
"""
|
||||
return simplify_AND(sort(simplify(domain)))
|
||||
|
||||
|
||||
def merge(domain, domoperator=None):
|
||||
if not domain or domain in ('AND', 'OR'):
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user