mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Implementing row conditional formatting
Summary: Conditional formatting can now be used for whole rows. Related fix: - Font styles weren't applicable for summary columns. - Checkbox and slider weren't using colors properly Test Plan: Existing and new tests Reviewers: paulfitz, georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3547
This commit is contained in:
@@ -89,6 +89,7 @@ class MetaTableExtras(object):
|
||||
usedByFields = _record_set('_grist_Views_section_field', 'displayCol')
|
||||
ruleUsedByCols = _record_ref_list_set('_grist_Tables_column', 'rules')
|
||||
ruleUsedByFields = _record_ref_list_set('_grist_Views_section_field', 'rules')
|
||||
ruleUsedByTables = _record_ref_list_set('_grist_Views_section', 'rules')
|
||||
|
||||
def tableId(rec, table):
|
||||
return rec.parentId.tableId
|
||||
@@ -101,10 +102,16 @@ class MetaTableExtras(object):
|
||||
|
||||
def numRuleColUsers(rec, table):
|
||||
"""
|
||||
Returns the number of cols and fields using this col as a rule col
|
||||
Returns the number of cols and fields using this col as a rule
|
||||
"""
|
||||
return len(rec.ruleUsedByCols) + len(rec.ruleUsedByFields)
|
||||
|
||||
def numRuleTableUsers(rec, table):
|
||||
"""
|
||||
Returns the number of tables using this col as a rule
|
||||
"""
|
||||
return len(rec.ruleUsedByTables)
|
||||
|
||||
def recalcOnChangesToSelf(rec, table):
|
||||
"""
|
||||
Whether the column is a trigger-formula column that depends on itself, used for
|
||||
@@ -115,8 +122,11 @@ class MetaTableExtras(object):
|
||||
def setAutoRemove(rec, table):
|
||||
"""Marks the col for removal if it's a display/rule helper col with no more users."""
|
||||
as_display = rec.colId.startswith('gristHelper_Display') and rec.numDisplayColUsers == 0
|
||||
as_rule = rec.colId.startswith('gristHelper_Conditional') and rec.numRuleColUsers == 0
|
||||
table.docmodel.setAutoRemove(rec, as_display or as_rule)
|
||||
as_col_rule = rec.colId.startswith('gristHelper_ConditionalRule') and rec.numRuleColUsers == 0
|
||||
as_row_rule = (
|
||||
rec.colId.startswith('gristHelper_RowConditionalRule') and rec.numRuleTableUsers == 0
|
||||
)
|
||||
table.docmodel.setAutoRemove(rec, as_display or as_col_rule or as_row_rule)
|
||||
|
||||
|
||||
class _grist_Views(object):
|
||||
|
||||
@@ -28,6 +28,11 @@ log = logger.Logger(__name__, logger.INFO)
|
||||
# This should make it at least barely possible to share documents by people who are not all on the
|
||||
# same Grist version (even so, it will require more work). It should also make it somewhat safe to
|
||||
# upgrade and then open the document with a previous version.
|
||||
#
|
||||
# After each migration you probably should run these commands:
|
||||
# ./test/upgradeDocument public_samples/*.grist
|
||||
# UPDATE_REGRESSION_DATA=1 GREP_TESTS=DocRegressionTests ./test/testrun.sh server
|
||||
# ./test/upgradeDocument test/fixtures/docs/Hello.grist
|
||||
|
||||
all_migrations = {}
|
||||
|
||||
@@ -1081,3 +1086,9 @@ def migration31(tdset):
|
||||
actions.UpdateRecord('_grist_ACLResources', resource.id, {'tableId': new_name})
|
||||
)
|
||||
return tdset.apply_doc_actions(doc_actions)
|
||||
|
||||
@migration(schema_version=32)
|
||||
def migration32(tdset):
|
||||
return tdset.apply_doc_actions([
|
||||
add_column('_grist_Views_section', 'rules', 'RefList:_grist_Tables_column'),
|
||||
])
|
||||
|
||||
@@ -15,7 +15,7 @@ import six
|
||||
|
||||
import actions
|
||||
|
||||
SCHEMA_VERSION = 31
|
||||
SCHEMA_VERSION = 32
|
||||
|
||||
def make_column(col_id, col_type, formula='', isFormula=False):
|
||||
return {
|
||||
@@ -194,6 +194,8 @@ def schema_create_actions():
|
||||
make_column("linkTargetColRef", "Ref:_grist_Tables_column"),
|
||||
# embedId is deprecated as of version 12. Do not remove or reuse.
|
||||
make_column("embedId", "Text"),
|
||||
# Points to formula columns that hold conditional formatting rules for this view section.
|
||||
make_column("rules", "RefList:_grist_Tables_column"),
|
||||
]),
|
||||
# The fields of a view section.
|
||||
actions.AddTable("_grist_Views_section_field", [
|
||||
|
||||
84
sandbox/grist/test_rules_grid.py
Normal file
84
sandbox/grist/test_rules_grid.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import test_engine
|
||||
|
||||
|
||||
class TestGridRules(test_engine.EngineTestCase):
|
||||
# Helper for rules action
|
||||
def add_empty(self):
|
||||
return self.apply_user_action(['AddEmptyRule', "Table1", 0, 0])
|
||||
|
||||
|
||||
def set_rule(self, rule_index, formula):
|
||||
rules = self.engine.docmodel.tables.lookupOne(tableId='Table1').rawViewSectionRef.rules
|
||||
rule = list(rules)[rule_index]
|
||||
return self.apply_user_action(['UpdateRecord', '_grist_Tables_column',
|
||||
rule.id, {"formula": formula}])
|
||||
|
||||
|
||||
def remove_rule(self, rule_index):
|
||||
rules = self.engine.docmodel.tables.lookupOne(tableId='Table1').rawViewSectionRef.rules
|
||||
rule = list(rules)[rule_index]
|
||||
return self.apply_user_action(['RemoveColumn', 'Table1', rule.colId])
|
||||
|
||||
|
||||
def test_simple_rules(self):
|
||||
self.apply_user_action(['AddEmptyTable', None])
|
||||
self.apply_user_action(['AddRecord', "Table1", None, {"A": 1}])
|
||||
self.apply_user_action(['AddRecord', "Table1", None, {"A": 2}])
|
||||
self.apply_user_action(['AddRecord', "Table1", None, {"A": 3}])
|
||||
out_actions = self.add_empty()
|
||||
self.assertPartialOutActions(out_actions, {"stored": [
|
||||
["AddColumn", "Table1", "gristHelper_RowConditionalRule",
|
||||
{"formula": "", "isFormula": True, "type": "Any"}],
|
||||
["AddRecord", "_grist_Tables_column", 5,
|
||||
{"colId": "gristHelper_RowConditionalRule", "formula": "", "isFormula": True,
|
||||
"label": "gristHelper_RowConditionalRule", "parentId": 1, "parentPos": 5.0,
|
||||
"type": "Any",
|
||||
"widgetOptions": ""}],
|
||||
["UpdateRecord", "_grist_Views_section", 2, {"rules": ["L", 5]}],
|
||||
]})
|
||||
out_actions = self.set_rule(0, "$A == 1")
|
||||
self.assertPartialOutActions(out_actions, {"stored": [
|
||||
["ModifyColumn", "Table1", "gristHelper_RowConditionalRule",
|
||||
{"formula": "$A == 1"}],
|
||||
["UpdateRecord", "_grist_Tables_column", 5, {"formula": "$A == 1"}],
|
||||
["BulkUpdateRecord", "Table1", [1, 2, 3],
|
||||
{"gristHelper_RowConditionalRule": [True, False, False]}],
|
||||
]})
|
||||
|
||||
# Replace this rule with another rule to mark A = 2
|
||||
out_actions = self.set_rule(0, "$A == 2")
|
||||
self.assertPartialOutActions(out_actions, {"stored": [
|
||||
["ModifyColumn", "Table1", "gristHelper_RowConditionalRule",
|
||||
{"formula": "$A == 2"}],
|
||||
["UpdateRecord", "_grist_Tables_column", 5, {"formula": "$A == 2"}],
|
||||
["BulkUpdateRecord", "Table1", [1, 2],
|
||||
{"gristHelper_RowConditionalRule": [False, True]}],
|
||||
]})
|
||||
|
||||
# Add another rule A = 3
|
||||
self.add_empty()
|
||||
out_actions = self.set_rule(1, "$A == 3")
|
||||
self.assertPartialOutActions(out_actions, {"stored": [
|
||||
["ModifyColumn", "Table1", "gristHelper_RowConditionalRule2",
|
||||
{"formula": "$A == 3"}],
|
||||
["UpdateRecord", "_grist_Tables_column", 6, {"formula": "$A == 3"}],
|
||||
["BulkUpdateRecord", "Table1", [1, 2, 3],
|
||||
{"gristHelper_RowConditionalRule2": [False, False, True]}],
|
||||
]})
|
||||
|
||||
# Remove the last rule
|
||||
out_actions = self.remove_rule(1)
|
||||
self.assertPartialOutActions(out_actions, {"stored": [
|
||||
["RemoveRecord", "_grist_Tables_column", 6],
|
||||
["UpdateRecord", "_grist_Views_section", 2, {"rules": ["L", 5]}],
|
||||
["RemoveColumn", "Table1", "gristHelper_RowConditionalRule2"]
|
||||
]})
|
||||
|
||||
# Remove last rule
|
||||
out_actions = self.remove_rule(0)
|
||||
self.assertPartialOutActions(out_actions, {"stored": [
|
||||
["RemoveRecord", "_grist_Tables_column", 5],
|
||||
["UpdateRecord", "_grist_Views_section", 2, {"rules": None}],
|
||||
["RemoveColumn", "Table1", "gristHelper_RowConditionalRule"]
|
||||
]})
|
||||
@@ -200,6 +200,7 @@ def allowed_summary_change(key, updated, original):
|
||||
allowed_to_change = {'widget', 'dateFormat', 'timeFormat', 'isCustomDateFormat', 'alignment',
|
||||
'fillColor', 'textColor', 'isCustomTimeFormat', 'isCustomDateFormat',
|
||||
'numMode', 'numSign', 'decimals', 'maxDecimals', 'currency',
|
||||
'fontBold', 'fontItalic', 'fontUnderline', 'fontStrikethrough',
|
||||
'rulesOptions'}
|
||||
# Helper function to remove protected keys from dictionary.
|
||||
def trim(options):
|
||||
@@ -456,7 +457,7 @@ class UserActions(object):
|
||||
table_id == "_grist_Views_section"
|
||||
and any(rec.isRaw for i, rec in self._bulk_action_iter(table_id, row_ids))
|
||||
):
|
||||
allowed_fields = {"title", "options", "sortColRefs"}
|
||||
allowed_fields = {"title", "options", "sortColRefs", "rules"}
|
||||
has_summary_section = any(rec.tableRef.summarySourceTable
|
||||
for i, rec in self._bulk_action_iter(table_id, row_ids))
|
||||
if has_summary_section:
|
||||
@@ -1637,23 +1638,26 @@ class UserActions(object):
|
||||
Adds empty conditional style rule to a field or column.
|
||||
"""
|
||||
assert table_id, "table_id is required"
|
||||
assert field_ref or col_ref, "field_ref or col_ref is required"
|
||||
assert not field_ref or not col_ref, "can't set both field_ref and col_ref"
|
||||
|
||||
col_name = "gristHelper_ConditionalRule"
|
||||
|
||||
if field_ref:
|
||||
field_or_col = self._docmodel.view_fields.table.get_record(field_ref)
|
||||
rule_owner = self._docmodel.view_fields.table.get_record(field_ref)
|
||||
elif col_ref:
|
||||
rule_owner = self._docmodel.columns.table.get_record(col_ref)
|
||||
else:
|
||||
field_or_col = self._docmodel.columns.table.get_record(col_ref)
|
||||
col_name = "gristHelper_RowConditionalRule"
|
||||
rule_owner = self._docmodel.get_table_rec(table_id).rawViewSectionRef
|
||||
|
||||
col_info = self.AddHiddenColumn(table_id, 'gristHelper_ConditionalRule', {
|
||||
col_info = self.AddHiddenColumn(table_id, col_name, {
|
||||
"type": "Any",
|
||||
"isFormula": True,
|
||||
"formula": ''
|
||||
})
|
||||
new_rule = col_info['colRef']
|
||||
existing_rules = field_or_col.rules._get_encodable_row_ids() if field_or_col.rules else []
|
||||
existing_rules = rule_owner.rules._get_encodable_row_ids() if rule_owner.rules else []
|
||||
updated_rules = existing_rules + [new_rule]
|
||||
self._docmodel.update([field_or_col], rules=[encode_object(updated_rules)])
|
||||
self._docmodel.update([rule_owner], rules=[encode_object(updated_rules)])
|
||||
|
||||
|
||||
#----------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user