diff --git a/app/client/aclui/AccessRules.ts b/app/client/aclui/AccessRules.ts index 89b40b65..212edc7f 100644 --- a/app/client/aclui/AccessRules.ts +++ b/app/client/aclui/AccessRules.ts @@ -1155,6 +1155,7 @@ class ObsRulePart extends Disposable { private _completions = Computed.create(this, (use) => [ ...use(this._ruleSet.accessRules.userAttrChoices).map(opt => opt.value), ...this._ruleSet.getValidColIds().map(colId => `rec.${colId}`), + ...this._ruleSet.getValidColIds().map(colId => `$${colId}`), ...this._ruleSet.getValidColIds().map(colId => `newRec.${colId}`), ]); diff --git a/sandbox/grist/acl_formula.py b/sandbox/grist/acl_formula.py index 4222aa5b..494f0b15 100644 --- a/sandbox/grist/acl_formula.py +++ b/sandbox/grist/acl_formula.py @@ -7,6 +7,7 @@ from collections import namedtuple import asttokens import six +from codebuilder import replace_dollar_attrs def parse_acl_formula(acl_formula): """ @@ -31,6 +32,7 @@ def parse_acl_formula(acl_formula): if isinstance(acl_formula, six.binary_type): acl_formula = acl_formula.decode('utf8') try: + acl_formula = replace_dollar_attrs(acl_formula) tree = ast.parse(acl_formula, mode='eval') result = _TreeConverter().visit(tree) for part in tokenize.generate_tokens(io.StringIO(acl_formula).readline): diff --git a/sandbox/grist/codebuilder.py b/sandbox/grist/codebuilder.py index 9c1accf6..de77cfc2 100644 --- a/sandbox/grist/codebuilder.py +++ b/sandbox/grist/codebuilder.py @@ -122,6 +122,26 @@ def make_formula_body(formula, default_value, assoc_value=None): return final_formula +def replace_dollar_attrs(formula): + """ + Translates formula "$" expression into rec. expression. This is extracted from the + make_formula_body function. + """ + formula_builder_text = textbuilder.Text(formula) + tmp_patches = textbuilder.make_regexp_patches(formula, DOLLAR_REGEX, 'DOLLAR') + tmp_formula = textbuilder.Replacer(formula_builder_text, tmp_patches) + atok = asttokens.ASTTokens(tmp_formula.get_text(), parse=True) + patches = [] + for node in ast.walk(atok.tree): + if isinstance(node, ast.Name) and node.id.startswith('DOLLAR'): + input_pos = tmp_formula.map_back_offset(node.first_token.startpos) + m = DOLLAR_REGEX.match(formula, input_pos) + if m: + patches.append(textbuilder.make_patch(formula, m.start(0), m.end(0), 'rec.')) + final_formula = textbuilder.Replacer(formula_builder_text, patches) + return final_formula.get_text() + + def _create_syntax_error_code(builder, input_text, err): """ Returns the text for a function that raises the given SyntaxError and includes the offending diff --git a/sandbox/grist/test_acl_formula.py b/sandbox/grist/test_acl_formula.py index 55826ea2..62c51e39 100644 --- a/sandbox/grist/test_acl_formula.py +++ b/sandbox/grist/test_acl_formula.py @@ -33,6 +33,15 @@ class TestACLFormula(unittest.TestCase): ['List', ['Const', 'sally@'], ['Const', 'xie@']] ]]) + self.assertEqual(parse_acl_formula( + "$office == 'Seattle' and user.email in ['sally@', 'xie@']"), + ['And', + ['Eq', ['Attr', ['Name', 'rec'], 'office'], ['Const', 'Seattle']], + ['In', + ['Attr', ['Name', 'user'], 'email'], + ['List', ['Const', 'sally@'], ['Const', 'xie@']] + ]]) + self.assertEqual(parse_acl_formula( "user.IsAdmin or rec.assigned is None or (not newRec.HasDuplicates and rec.StatusIndex <= newRec.StatusIndex)"), ['Or', @@ -44,6 +53,17 @@ class TestACLFormula(unittest.TestCase): ] ]) + self.assertEqual(parse_acl_formula( + "user.IsAdmin or $assigned is None or (not newRec.HasDuplicates and $StatusIndex <= newRec.StatusIndex)"), + ['Or', + ['Attr', ['Name', 'user'], 'IsAdmin'], + ['Is', ['Attr', ['Name', 'rec'], 'assigned'], ['Const', None]], + ['And', + ['Not', ['Attr', ['Name', 'newRec'], 'HasDuplicates']], + ['LtE', ['Attr', ['Name', 'rec'], 'StatusIndex'], ['Attr', ['Name', 'newRec'], 'StatusIndex']] + ] + ]) + self.assertEqual(parse_acl_formula( "r.A <= n.A + 1 or r.A >= n.A - 1 or r.B < n.B * 2.5 or r.B > n.B / 2.5 or r.C % 2 != 0"), ['Or', @@ -71,6 +91,13 @@ class TestACLFormula(unittest.TestCase): ['IsNot', ['Attr', ['Name', 'rec'], 'A'], ['Const', False]] ]) + self.assertEqual(parse_acl_formula( + "$A is True or $A is not False"), + ['Or', + ['Is', ['Attr', ['Name', 'rec'], 'A'], ['Const', True]], + ['IsNot', ['Attr', ['Name', 'rec'], 'A'], ['Const', False]] + ]) + self.assertEqual(parse_acl_formula( "user.Office.City == 'Seattle' and user.Status.IsActive"), ['And', @@ -171,7 +198,7 @@ class TestACLFormulaUserActions(test_engine.EngineTestCase): "aclFormula": ["not user.IsGood", ""], }]) self.assertPartialOutActions(out_actions, { "stored": [ - [ 'BulkUpdateRecord', '_grist_ACLRules', [2, 3], { + ['BulkUpdateRecord', '_grist_ACLRules', [2, 3], { "aclFormula": ["not user.IsGood", ""], "aclFormulaParsed": ['["Not", ["Attr", ["Name", "user"], "IsGood"]]', ''], }],