Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update dropdown conditions on column rename #1038

Merged
merged 81 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
88a857b
Update dropdown conditions on column rename
SleepyLeslie Jun 12, 2024
3014237
Handle `rec.` and `$` column refs
SleepyLeslie Jun 13, 2024
a773272
Reuse the same logic for ACL formulas
SleepyLeslie Jun 13, 2024
df10e29
Add comments and cleanup
SleepyLeslie Jun 13, 2024
6c9477e
Add test for dollar signs in ACL formulas
SleepyLeslie Jun 14, 2024
1c37047
Add tests for dropdown condition formula renames
SleepyLeslie Jun 14, 2024
d9d5100
Add comments to new test case
SleepyLeslie Jun 14, 2024
270c7a9
Use ModifyColumn instead of UpdateRecord
SleepyLeslie Jun 14, 2024
b9ea716
Improve the test cases
SleepyLeslie Jun 15, 2024
9847b84
Handle wrong/unsupported syntax in pred formulas
SleepyLeslie Jun 15, 2024
0a47c00
Improve code style
SleepyLeslie Jun 25, 2024
c4a48c1
Apply suggestions from code review
SleepyLeslie Jun 28, 2024
063d412
Update docstring for get_dollar_replacer
SleepyLeslie Jun 28, 2024
c5d0b4a
Handle RefList columns
SleepyLeslie Jun 28, 2024
faa51ff
Handle DC formulas on non-reference columns
SleepyLeslie Jul 3, 2024
52d196f
Drop the _ prefix from useractions._docmodel
SleepyLeslie Jul 3, 2024
d8dae4b
(core) Makes EE frontend behave as core if EE isn't activated
Spoffy Jun 13, 2024
4ebd73b
(core) Update documentation of certain functions
dsagal Jun 12, 2024
df11bc6
(core) Restoring GRIST_DEFAULT_PRODUCT functionality
berhalak Jun 14, 2024
fd04bbd
Update README to rebrand grist-electron (#1039)
SleepyLeslie Jun 13, 2024
5000a57
(core) Adding fixSiteProducts that changes orgs from teamFree to Free…
berhalak Jun 18, 2024
cde015c
(core) Disable formula timing UI for non-owners
dsagal Jun 18, 2024
72ba84c
(core) Removing dry option from fixSiteProducts
berhalak Jun 18, 2024
68b9005
automated update to translation keys (#1053)
github-actions[bot] Jun 18, 2024
a9c3d73
HomeDBManager refactoration: extract method related to Users manageme…
fflorent Jun 18, 2024
85429a3
(core) Port allocation fix in TestServer
berhalak Jun 24, 2024
9b1cc5f
(core) Enabling telemetry on /api/version endpoint
berhalak Jun 26, 2024
32d988e
supervisor: new file
jordigh Jun 13, 2024
c444496
FlexServer: add new admin restart endpoint
jordigh Jun 8, 2024
4d59833
Dockerfile: use docker-runner.mjs as new entrypoint
jordigh Jun 13, 2024
1c8a866
Dockerfile: remove mention of docker-runner.mjs
jordigh Jun 19, 2024
da16ab9
Add some database documentation (#937)
fflorent Jun 20, 2024
98b08b7
README: Rewrite boot page section to reflect new admin page
jordigh Jun 20, 2024
db8bb18
Translated using Weblate (Slovak)
h0r0m Jun 23, 2024
aaf772a
Translated using Weblate (Portuguese (Brazil))
Jun 24, 2024
993d202
Translated using Weblate (Spanish)
Jun 24, 2024
c1bfccc
Translated using Weblate (German)
Jun 24, 2024
8752b16
Translated using Weblate (Slovak)
h0r0m Jun 24, 2024
a8cf19a
Add publiccode.yml (#1056)
fflorent Jun 25, 2024
9464fb4
Improve session ID security (#1059)
SleepyLeslie Jun 25, 2024
4dfc31b
Translated using Weblate (Slovenian)
fprijate Jun 25, 2024
7b6fae9
Translated using Weblate (Slovak)
h0r0m Jun 25, 2024
8ee7492
automated update to translation keys (#1065)
github-actions[bot] Jun 26, 2024
c00dd77
build.sh: add some diagnostic output
jordigh Jun 19, 2024
09f485c
.grist-ee-version: start referencing the intended enterprise version
jordigh Jun 20, 2024
e7c37e6
checkout-ext-directory: new helper script
jordigh Jun 19, 2024
848b1aa
tsconfig-ext: add /app, /test, and /stubs/app directories
jordigh Jun 19, 2024
a457bec
workflows: build the grist-oss, grist, and grist-ee images
jordigh Jun 21, 2024
9e6551c
README: Mention the two possible docker images
jordigh Jun 21, 2024
6f02e7c
workflows: fix syntax error
jordigh Jun 26, 2024
913d5a4
Makes docker images default to non-root execution (#1031)
Spoffy Jun 27, 2024
b260b82
workflows: update the latest branch conditionally
jordigh Jun 26, 2024
41d992e
workflows: ensure we also use the experimental image we just built
jordigh Jun 27, 2024
6f97e18
Translated using Weblate (Italian)
ricpol Jun 28, 2024
5b7fc3d
Translated using Weblate (Slovak)
h0r0m Jun 28, 2024
49344cb
Translated using Weblate (Portuguese (Brazil))
Jun 29, 2024
652a229
Translated using Weblate (Spanish)
Jun 29, 2024
ba95ef4
Translated using Weblate (German)
Jun 29, 2024
28a22ca
Create user last connection datetime (#935)
CamilleLegeron Jul 1, 2024
18a39cb
workflows: Do not use `ext/` director to run tests
jordigh Jun 29, 2024
55d13bd
tsconfig-ext: revert bc52f65b2648ff7987383537fc06c6a388d29ce2
jordigh Jun 29, 2024
511a4a3
Translated using Weblate (French)
hexaltation Jul 1, 2024
419403a
Translated using Weblate (Slovak)
h0r0m Jul 1, 2024
9e0ba09
(core) Billing updates
berhalak Jun 27, 2024
74e57a3
(core) Bundling save funciton in the field editor
berhalak Jul 4, 2024
e1d0224
log periodic per-document statistics about snapshot generation
paulfitz Jul 1, 2024
af8c261
add a getSnapshotProgress implementation to DocStorageManager
paulfitz Jul 2, 2024
3f6f0e9
docstrings, moment import, fix log format
paulfitz Jul 3, 2024
5865134
Enable external contributors to create previews (#1068)
SleepyLeslie Jul 3, 2024
89f5fc0
Add authorization header in webhooks stored in secrets table (#941)
CamilleLegeron Jul 4, 2024
530ff8f
Move HomeDBManager to gen-server/lib/homedb (#1076)
fflorent Jul 5, 2024
d1e70e3
Fix linting issues
SleepyLeslie Jul 8, 2024
5c6eece
Merge main
SleepyLeslie Jul 8, 2024
49b55c2
Remove redundant error checking
SleepyLeslie Jul 8, 2024
78ace36
Replace ValueError with more accurate SyntaxError
SleepyLeslie Jul 8, 2024
421eafb
Revise error handling for invalid formulas
SleepyLeslie Jul 8, 2024
e73f0dc
Separate column type parsing into usertypes
SleepyLeslie Jul 8, 2024
ca67ec2
Use map_back_patch to replace map_back_offset
SleepyLeslie Jul 8, 2024
b4f415f
Address various PR remarks
SleepyLeslie Jul 9, 2024
06d50a4
Make docmodel private and add a getter for it
SleepyLeslie Jul 12, 2024
94263cf
Address more PR remarks
SleepyLeslie Jul 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 76 additions & 37 deletions sandbox/grist/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
# It now retains only the minimum needed to keep new documents openable by old code,
# and to produce the ActionBundles expected by other code.

import ast
import asttokens
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
import json
import logging

from acl_formula import parse_acl_grist_entities
from predicate_formula import parse_predicate_formula_json
import action_obj
import textbuilder
import predicate_formula
from predicate_formula import NamedEntity, parse_predicate_formula_json, TreeConverter

log = logging.getLogger(__name__)

Expand All @@ -32,6 +34,40 @@ class Permissions(object):
ALL_SET = frozenset([ALL])


def parse_acl_formulas(col_values):
"""
Populates `aclFormulaParsed` by parsing `aclFormula` for all `col_values`.
"""
if 'aclFormula' not in col_values:
return

col_values['aclFormulaParsed'] = [parse_predicate_formula_json(v)
for v
in col_values['aclFormula']]


class _ACLEntityCollector(TreeConverter):
def __init__(self):
self.entities = [] # NamedEntity list

def visit_Attribute(self, node):
parent = self.visit(node.value)

# We recognize a couple of specific patterns for entities that may be affected by renames.
if parent == ['Name', 'rec'] or parent == ['Name', 'newRec']:
# rec.COL refers to the column from the table that the rule is on.
self.entities.append(NamedEntity('recCol', node.last_token.startpos, node.attr, None))
if parent == ['Name', 'user']:
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
# user.ATTR is a user attribute.
self.entities.append(NamedEntity('userAttr', node.last_token.startpos, node.attr, None))
elif parent[0] == 'Attr' and parent[1] == ['Name', 'user']:
# user.ATTR.COL is a column from the lookup table of the UserAttribute ATTR.
self.entities.append(
NamedEntity('userAttrCol', node.last_token.startpos, node.attr, parent[2]))

return ["Attr", parent, node.attr]


def acl_read_split(action_group):
"""
Returns an ActionBundle containing actions from the given action_group, all in one envelope.
Expand All @@ -48,20 +84,20 @@ def acl_read_split(action_group):
return bundle


def prepare_acl_table_renames(docmodel, useractions, table_renames_dict):
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
def prepare_acl_table_renames(useractions, table_renames_dict):
"""
Given a dict of table renames of the form {table_id: new_table_id}, returns a callback
that will apply updates to the affected ACL rules and resources.
"""
# If there are ACLResources that refer to the renamed table, prepare updates for those.
resource_updates = []
for resource_rec in docmodel.aclResources.all:
for resource_rec in useractions._docmodel.aclResources.all:
if resource_rec.tableId in table_renames_dict:
resource_updates.append((resource_rec, {'tableId': table_renames_dict[resource_rec.tableId]}))

# Collect updates for any ACLRules with UserAttributes that refer to the renamed table.
rule_updates = []
for rule_rec in docmodel.aclRules.all:
for rule_rec in useractions._docmodel.aclRules.all:
if rule_rec.userAttributes:
try:
rule_info = json.loads(rule_rec.userAttributes)
Expand All @@ -77,14 +113,14 @@ def do_renames():
return do_renames


def prepare_acl_col_renames(docmodel, useractions, col_renames_dict):
def perform_acl_rule_renames(useractions, col_renames_dict):
"""
Given a dict of column renames of the form {(table_id, col_id): new_col_id}, returns a callback
that will apply updates to the affected ACL rules and resources.
"""
# Collect updates for ACLResources that refer to the renamed columns.
resource_updates = []
for resource_rec in docmodel.aclResources.all:
for resource_rec in useractions._docmodel.aclResources.all:
t = resource_rec.tableId
if resource_rec.colIds and resource_rec.colIds != '*':
new_col_ids = ','.join((col_renames_dict.get((t, c)) or c)
Expand All @@ -95,7 +131,7 @@ def prepare_acl_col_renames(docmodel, useractions, col_renames_dict):
# Collect updates for any ACLRules with UserAttributes that refer to the renamed column.
rule_updates = []
user_attr_tables = {} # Maps name of user attribute to its lookup table
for rule_rec in docmodel.aclRules.all:
for rule_rec in useractions._docmodel.aclRules.all:
if rule_rec.userAttributes:
try:
rule_info = json.loads(rule_rec.userAttributes)
Expand All @@ -108,32 +144,35 @@ def prepare_acl_col_renames(docmodel, useractions, col_renames_dict):
log.warning("Error examining aclRule: %s", e)

# Go through again checking if anything in ACL formulas is affected by the rename.
for rule_rec in docmodel.aclRules.all:
if rule_rec.aclFormula:
formula = rule_rec.aclFormula
patches = []

for entity in parse_acl_grist_entities(rule_rec.aclFormula):
if entity.type == 'recCol':
table_id = docmodel.aclResources.table.get_record(int(rule_rec.resource)).tableId
elif entity.type == 'userAttrCol':
table_id = user_attr_tables.get(entity.extra)
else:
continue
col_id = entity.name
new_col_id = col_renames_dict.get((table_id, col_id))
if not new_col_id:
continue
patch = textbuilder.make_patch(
formula, entity.start_pos, entity.start_pos + len(entity.name), new_col_id)
patches.append(patch)

replacer = textbuilder.Replacer(textbuilder.Text(formula), patches)
txt = replacer.get_text()
rule_updates.append((rule_rec, {'aclFormula': txt,
'aclFormulaParsed': parse_predicate_formula_json(txt)}))

def do_renames():
useractions.doBulkUpdateFromPairs('_grist_ACLResources', resource_updates)
useractions.doBulkUpdateFromPairs('_grist_ACLRules', rule_updates)
return do_renames
for rule_rec in useractions._docmodel.aclRules.all:

if not rule_rec.aclFormula:
continue
formula = rule_rec.aclFormula

def renamer(subject):
if subject.type == 'recCol':
table_id = useractions._docmodel.aclResources.table.get_record(int(rule_rec.resource)).tableId
elif subject.type == 'userAttrCol':
table_id = user_attr_tables.get(subject.extra)
else:
return None
col_id = subject.name
new_col_id = col_renames_dict.get((table_id, col_id))
if not new_col_id:
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
return None
return new_col_id

new_acl_formula = predicate_formula.process_renames(formula, _ACLEntityCollector(), renamer)
new_rule_record = {"aclFormula": new_acl_formula}
try:
new_rule_record["aclFormulaParsed"] = parse_predicate_formula_json(new_acl_formula)
except SyntaxError:
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worries me if this can happen -- wouldn't it mean that aclFormulaParsed remains un-fixed and broken (even though aclFormula will appear fixed)? Are there situations when either SyntaxError or ValueError with "Unsupported syntax" could legitimately happen?

If not, might it be better to not catch the errors at all?

Copy link
Collaborator Author

@SleepyLeslie SleepyLeslie Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this can result in inconsistency. The new process_renames function doesn't do anything to a formula with syntax errors, so nothing will actually happen.

Even if aclFormula is updated, it will still be broken if it originally was. The parsed version will stay obsolete and broken until the text version is fixed by the user, when the parsed version is re-generated and fixed thereof.

Error checking here seems redundant, but not catching errors here will make the data engine stop processing remaining formulas whenever it hits a problematic one. Thus, syntax errors must be explicitly ignored.

I revised this logic so that it is still (hopefully) logically correct, but avoids try-except. This is admittedly complicated and error-prone, so I added some detailed comments to explain my code and possible gotchas for future maintainers.

Also: I have a test case for this behavior (don't process syntactically wrong formulas), so we should be safe.

except ValueError as e:
if not str(e).startswith("Unsupported syntax"):
raise e
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
rule_updates.append((rule_rec, new_rule_record))
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved

useractions.doBulkUpdateFromPairs('_grist_ACLResources', resource_updates)
useractions.doBulkUpdateFromPairs('_grist_ACLRules', rule_updates)
50 changes: 0 additions & 50 deletions sandbox/grist/acl_formula.py

This file was deleted.

4 changes: 2 additions & 2 deletions sandbox/grist/codebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def make_formula_body(formula, default_value, assoc_value=None):
return final_formula


def replace_dollar_attrs(formula):
def get_dollar_replacer(formula):
"""
Translates formula "$" expression into rec. expression. This is extracted from the
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
make_formula_body function.
Expand All @@ -150,7 +150,7 @@ def replace_dollar_attrs(formula):
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()
return final_formula


def _create_syntax_error_code(builder, input_text, err):
Expand Down
67 changes: 66 additions & 1 deletion sandbox/grist/dropdown_condition.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,75 @@
import ast
import asttokens
import json
import logging
import textbuilder

from predicate_formula import parse_predicate_formula_json
from predicate_formula import NamedEntity, parse_predicate_formula_json, TreeConverter
import predicate_formula

log = logging.getLogger(__name__)

class _DCEntityCollector(TreeConverter):
def __init__(self):
self.entities = []

def visit_Attribute(self, node):
parent = self.visit(node.value)

if parent == ["Name", "choice"]:
self.entities.append(NamedEntity("choiceAttr", node.last_token.startpos, node.attr, None))
elif parent == ["Name", "rec"]:
self.entities.append(NamedEntity("recCol", node.last_token.startpos, node.attr, None))

return ["Attr", parent, node.attr]


def perform_dropdown_condition_renames(useractions, renames):
"""
Given a dict of column renames of the form {(table_id, col_id): new_col_id}, applys updates
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
to the affected dropdown condition formulas.
"""
updates = []

for col in useractions._engine.docmodel.columns.all:
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved

patches = []

# Find all columns in the document that has dropdown conditions.
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
try:
widget_options = json.loads(col.widgetOptions)
dc_formula = widget_options["dropdownCondition"]["text"]
except:
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
continue

# Find out what table this column refers to and belongs to.
ref_table_id = col.type.lstrip("Ref:")
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
self_table_id = col.parentId.tableId

def renamer(subject):
table_id = ref_table_id if subject.type == "choiceAttr" else self_table_id
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
if (table_id, subject.name) in renames:
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
return renames[(table_id, subject.name)]
else:
return None

new_dc_formula = predicate_formula.process_renames(dc_formula, _DCEntityCollector(), renamer)

widget_options["dropdownCondition"] = {"text": new_dc_formula}
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
try:
# Parse the new dropdown condition formula if it is syntactically correct.
widget_options["dropdownCondition"]["parsed"] = parse_predicate_formula_json(new_dc_formula)
except SyntaxError:
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
pass
except ValueError as e:
if not str(e).startswith("Unsupported syntax"):
raise e
updates.append((col, {"widgetOptions": json.dumps(widget_options)}))

# Update the dropdown condition in the database.
useractions.doBulkUpdateFromPairs('_grist_Tables_column', updates)


def parse_dropdown_conditions(col_values):
"""
Parses any unparsed dropdown conditions in `col_values`.
Expand Down
Loading
Loading