Skip to content

Commit

Permalink
(core) Distinct style rules for summary columns
Browse files Browse the repository at this point in the history
Summary:
Summary columns now have their own conditional rules,
which are not shared with sister columns.

Test Plan: New test

Reviewers: alexmojaki

Reviewed By: alexmojaki

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3388
  • Loading branch information
berhalak committed Apr 27, 2022
1 parent 0829ae6 commit 995bf9b
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 9 deletions.
2 changes: 1 addition & 1 deletion app/common/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData";

// tslint:disable:object-literal-key-quotes

export const SCHEMA_VERSION = 28;
export const SCHEMA_VERSION = 29;

export const schema = {

Expand Down
4 changes: 2 additions & 2 deletions app/server/lib/initialDocSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = `
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT '');
INSERT INTO _grist_DocInfo VALUES(1,'','','',28,'','');
INSERT INTO _grist_DocInfo VALUES(1,'','','',29,'','');
CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL);
CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0);
Expand Down Expand Up @@ -42,7 +42,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = `
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT '');
INSERT INTO _grist_DocInfo VALUES(1,'','','',28,'','');
INSERT INTO _grist_DocInfo VALUES(1,'','','',29,'','');
CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0);
INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2);
CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL);
Expand Down
36 changes: 36 additions & 0 deletions sandbox/grist/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,3 +924,39 @@ def migration28(tdset):
doc_actions.append(actions.ModifyColumn(table.tableId, col.colId, {"type": "Attachments"}))

return tdset.apply_doc_actions(doc_actions)


@migration(schema_version=29)
def migration29(tdset):
# This migration is fixing an error on summary tables with conditional rules.
# On summary tables all formula columns with the same name were updated together,
# which caused a situation where some summary columns have rules from diffrent tables.
# This migration is removing those rules in such columns.

tables = {table.id: table
for table in actions.transpose_bulk_action(tdset.all_tables["_grist_Tables"])}
columns = {col.id: col
for col in actions.transpose_bulk_action(tdset.all_tables["_grist_Tables_column"])}
doc_actions = []

def is_valid_rule(parentId, rule_id):
# Valid rule should be an existing column,
rule_col = columns.get(rule_id)
# in the same table.
return rule_col and rule_col.parentId == parentId

for col in columns.values():
if col.rules:
# Parse rules (they are a json encoded array like '[15]')
rules = safe_parse(col.rules)
# Remove all conditional styles if anything about rules is invalid.
if not (
isinstance(rules, list) and
all(is_valid_rule(col.parentId, ruleId) for ruleId in rules)
):
doc_actions.append(actions.UpdateRecord('_grist_Tables_column', col.id, {
"rules": None,
"widgetOptions": summary._copy_widget_options(col.widgetOptions)
}))

return tdset.apply_doc_actions(doc_actions)
2 changes: 1 addition & 1 deletion sandbox/grist/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import actions

SCHEMA_VERSION = 28
SCHEMA_VERSION = 29

def make_column(col_id, col_type, formula='', isFormula=False):
return {
Expand Down
33 changes: 33 additions & 0 deletions sandbox/grist/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,39 @@ def _get_colinfo_dict(col_info, with_id=False):
return col_values


def skip_rules_update(col, col_values):
"""
Rules for summary tables can't be derived from source columns. This function
removes (and kips original) rules settings when updating summary tables.
"""

# Remove rules from updates.
col_values = {k: v for k, v in six.iteritems(col_values) if k != 'rules'}

try:
# New widgetOptions to use.
new_widgetOptions = json.loads(col_values.get('widgetOptions', ''))
except ValueError:
# If we are not updating widgetOptions (or they are
# not a valid json string, i.e. in tests), just return the original updates.
return col_values

try:
# Original widgetOptions (maybe with styling rules "ruleOptions").
widgetOptions = json.loads(col.widgetOptions or '')
except ValueError:
widgetOptions = {}

# Keep the original rulesOptions if any, and ignore any new one.
new_widgetOptions.pop("rulesOptions", "")
rulesOptions = widgetOptions.get('rulesOptions')
if rulesOptions:
new_widgetOptions['rulesOptions'] = rulesOptions

col_values['widgetOptions'] = json.dumps(new_widgetOptions)
return col_values


def _copy_widget_options(options):
"""Copies widgetOptions for a summary group-by column (omitting conditional formatting rules)"""
if not options:
Expand Down
39 changes: 39 additions & 0 deletions sandbox/grist/test_rules.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# -*- coding: utf-8 -*-
import json

from collections import namedtuple
from summary import skip_rules_update
import testutil
import test_engine

Expand Down Expand Up @@ -52,6 +56,41 @@ def field_remove_rule(self, field_id, rule_index):
rule = list(rules)[rule_index]
return self.apply_user_action(['RemoveColumn', 'Inventory', rule.colId])

def test_summary_updates(self):
Col = namedtuple('Col', 'widgetOptions')
col = Col(None)
# Should remove rules from update
self.assertEqual({}, skip_rules_update(col, {'rules': [15]}))
# Should leave col_updates untouched when there are no rules.
col_updates = {'type': 'Int'}
self.assertEqual(col_updates, skip_rules_update(col, col_updates))

# Should return same dict when not updating ruleOptions
col_updates = {'widgetOptions': '{"color": "red"}'}
self.assertEqual(col_updates, skip_rules_update(col, col_updates))
col = Col('{"color": "red"}')
self.assertEqual(col_updates, skip_rules_update(col, col_updates))

# Should remove ruleOptions from update
col_updates = {'widgetOptions': '{"rulesOptions": [{"color": "black"}], "color": "blue"}'}
self.assertEqual({'widgetOptions': '{"color": "blue"}'},
skip_rules_update(col, col_updates))
col_updates = {'widgetOptions': '{"rulesOptions": [], "color": "blue"}'}
self.assertEqual({'widgetOptions': '{"color": "blue"}'},
skip_rules_update(col, col_updates))

# Should preserve original ruleOptions
col = Col('{"rulesOptions": [{"color":"red"}], "color": "blue"}')
col_updates = {'widgetOptions': '{"rulesOptions": [{"color": "black"}], "color": "red"}'}
updated = skip_rules_update(col, col_updates)
self.assertEqual({"rulesOptions": [{"color": "red"}], "color": "red"},
json.loads(updated.get('widgetOptions')))
col_updates = {'widgetOptions': '{"color": "red"}'}
updated = skip_rules_update(col, col_updates)
self.assertEqual({"rulesOptions": [{"color": "red"}], "color": "red"},
json.loads(updated.get('widgetOptions')))


def test_simple_rules(self):
self.load_sample(self.sample)
# Mark all records with Stock = 0
Expand Down
12 changes: 7 additions & 5 deletions sandbox/grist/useractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,10 +748,6 @@ def _adjust_one_column_update(self, col, col_values, avoid_colid_set, rebuild_su
# Returns a list of (col, values) pairs (containing the input column but possibly more).
# Note that it may modify col_values in-place, and may reuse it for multiple results.

results = []
def add(cols, value_dict):
results.extend((c, value_dict) for c in cols)

# If changing label, sync it to colId unless untieColIdFromLabel flag is set.
if 'label' in col_values and not col_values.get('untieColIdFromLabel',col.untieColIdFromLabel):
col_values.setdefault('colId', col_values['label'])
Expand Down Expand Up @@ -780,13 +776,19 @@ def add(cols, value_dict):
col_values.setdefault('rules', None)
col_values.setdefault('displayCol', 0)

# Collect all updates for dependent summary columns.
results = []
def add(cols, value_dict):
results.extend((c, summary.skip_rules_update(c, value_dict)) for c in cols)

source_table = col.parentId.summarySourceTable
if source_table: # This is a summary-table column.
# Disallow isFormula changes.
if has_diff_value(col_values, 'isFormula', col.isFormula):
raise ValueError("Cannot change summary column '%s' between formula and data" % col.colId)

if col.isFormula:
# Don't update any sister helper columns.
if col.isFormula and not col.colId.startswith("gristHelper"):
# Get all same-named formula columns from other summary tables for the same source table,
# and apply the same changes to them.
add(self._get_sister_columns(source_table, col), col_values)
Expand Down
Binary file modified test/fixtures/docs/Hello.grist
Binary file not shown.
5 changes: 5 additions & 0 deletions test/nbrowser/gristUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1831,6 +1831,11 @@ export function hexToRgb(hex: string) {
export async function addColumn(name: string) {
await scrollIntoView(await driver.find('.active_section .mod-add-column'));
await driver.find('.active_section .mod-add-column').click();
// If we are on a summary table, we could be see a menu helper
const menu = (await driver.findAll('.grist-floating-menu'))[0];
if (menu) {
await menu.findContent("li", name).click();
}
await waitForServer();
await waitAppFocus(false);
await driver.sendKeys(name);
Expand Down

0 comments on commit 995bf9b

Please sign in to comment.