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 78 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
108 changes: 70 additions & 38 deletions sandbox/grist/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
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 +31,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))
elif parent == ['Name', 'user']:
# 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 +81,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 +110,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 +128,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 +141,31 @@ 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
acl_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
return col_renames_dict.get((table_id, col_id))

new_acl_formula = predicate_formula.process_renames(acl_formula, _ACLEntityCollector(), renamer)
# No need to check for syntax errors, but this "if" statement must be present.
# See perform_dropdown_condition_renames for more info.
if new_acl_formula != acl_formula:
new_rule_record = {
"aclFormula": new_acl_formula,
"aclFormulaParsed": parse_predicate_formula_json(new_acl_formula)
}
rule_updates.append((rule_rec, new_rule_record))

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.

9 changes: 5 additions & 4 deletions sandbox/grist/codebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,11 @@ 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
make_formula_body function.
Returns a textbuilder.Replacer that would replace all dollar signs ("$") in the given
formula with "rec.". The Replacer tracks extra info we can later use to restore the
dollar signs back. To get the processed text, call .get_text() on the Replacer.
"""
formula_builder_text = textbuilder.Text(formula)
tmp_patches = textbuilder.make_regexp_patches(formula, DOLLAR_REGEX, 'DOLLAR')
Expand All @@ -150,7 +151,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
68 changes: 67 additions & 1 deletion sandbox/grist/dropdown_condition.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,76 @@
import json
import logging
import usertypes

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}, applies updates
to the affected dropdown condition formulas.
"""
updates = []

for col in useractions.docmodel.columns.all:

# Find all columns in the document that have dropdown conditions.
try:
widget_options = json.loads(col.widgetOptions)
dc_formula = widget_options["dropdownCondition"]["text"]
except (json.JSONDecodeError, KeyError):
continue

# Find out what table this column refers to and belongs to.
ref_table_id = usertypes.get_referenced_table_id(col.type)
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
# Dropdown conditions stay in widgetOptions, even when the current column type can't make
# use of them. Thus, attributes of "choice" do not make sense for columns other than Ref and
# RefList, but they may exist.
# We set ref_table_id to None in this case, so table_id will be None for stray choiceAttrs,
# therefore the subject will not be renamed.
# Columns of "rec" are still renamed accordingly.
return renames.get((table_id, subject.name))

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

# The data engine stops processing remaining formulas when it hits an exception. Parsing a
Copy link
Member

Choose a reason for hiding this comment

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

This sentence sounds wrong. Generally Grist formulas can have exceptions and that doesn't normally interfere with other formulas. So probably you mean something more specific -- it would be good to clarify.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I rephrased it a bit. Hope it's clearer now.

# formula could potentially raise SyntaxErrors, so we must be careful not to invoke it on a
# possibly syntactically wrong formula.
# Note that new_dc_formula was obtained from process_renames, where syntactically wrong formulas
# are left untouched. It is anticipated that rename-induced changes will not introduce new
# SyntaxErrors, so if the formula text is updated, the new version must be valid, hence safe
# to parse without error handling.
# This also serves as an optimization to avoid unnecessary parsing operations.
if new_dc_formula != dc_formula:
widget_options["dropdownCondition"]["text"] = new_dc_formula
widget_options["dropdownCondition"]["parsed"] = parse_predicate_formula_json(new_dc_formula)
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