gristlabs_grist-core/sandbox/grist/test_useractions.py

919 lines
38 KiB
Python
Raw Normal View History

import types
import logger
import useractions
import testutil
import test_engine
from test_engine import Table, Column, View, Section, Field
log = logger.Logger(__name__, logger.INFO)
class TestUserActions(test_engine.EngineTestCase):
sample = testutil.parse_test_sample({
"SCHEMA": [
[1, "Address", [
[21, "city", "Text", False, "", "", ""],
]]
],
"DATA": {
"Address": [
["id", "city" ],
[11, "New York" ],
[12, "Colombia" ],
[13, "New Haven" ],
[14, "West Haven" ]],
}
})
starting_table = Table(1, "Address", primaryViewId=0, summarySourceTable=0, columns=[
Column(21, "city", "Text", isFormula=False, formula="", summarySourceCol=0)
])
#----------------------------------------------------------------------
def test_conversions(self):
# Test the sequence of user actions as used for transform-based conversions. This is actually
# not exactly what the client emits, but more like what the client should ideally emit.
# Our sample has a Schools.city text column; we'll convert it to Ref:Address.
self.load_sample(self.sample)
# Add a new table for Schools so that we get the associated views and fields.
self.apply_user_action(['AddTable', 'Schools', [{'id': 'city', 'type': 'Text'}]])
self.apply_user_action(['BulkAddRecord', 'Schools', [1,2,3,4], {
'city': ['New York', 'Colombia', 'New York', '']
}])
self.assertPartialData("_grist_Tables", ["id", "tableId"], [
[1, "Address"],
[2, "Schools"],
])
self.assertPartialData("_grist_Tables_column",
["id", "colId", "parentId", "parentPos", "widgetOptions"], [
[21, "city", 1, 1.0, ""],
[22, "manualSort", 2, 2.0, ""],
[23, "city", 2, 3.0, ""],
])
self.assertPartialData("_grist_Views_section_field", ["id", "colRef", "widgetOptions"], [
[1, 23, ""]
])
self.assertPartialData("Schools", ["id", "city"], [
[1, "New York" ],
[2, "Colombia" ],
[3, "New York" ],
[4, "" ],
])
# Our sample has a text column city.
out_actions = self.add_column('Schools', 'grist_Transform',
isFormula=True, formula='return $city', type='Text')
self.assertPartialOutActions(out_actions, { "stored": [
['AddColumn', 'Schools', 'grist_Transform', {
'type': 'Text', 'isFormula': True, 'formula': 'return $city',
}],
['AddRecord', '_grist_Tables_column', 24, {
'widgetOptions': '', 'parentPos': 4.0, 'isFormula': True, 'parentId': 2, 'colId':
'grist_Transform', 'formula': 'return $city', 'label': 'grist_Transform',
'type': 'Text'
}],
["AddRecord", "_grist_Views_section_field", 2, {
"colRef": 24, "parentId": 1, "parentPos": 2.0
}],
["BulkUpdateRecord", "Schools", [1, 2, 3],
{"grist_Transform": ["New York", "Colombia", "New York"]}],
]})
out_actions = self.update_record('_grist_Tables_column', 24,
type='Ref:Address',
formula='return Address.lookupOne(city=$city).id')
self.assertPartialOutActions(out_actions, { "stored": [
['ModifyColumn', 'Schools', 'grist_Transform', {
'formula': 'return Address.lookupOne(city=$city).id', 'type': 'Ref:Address'}],
['UpdateRecord', '_grist_Tables_column', 24, {
'formula': 'return Address.lookupOne(city=$city).id', 'type': 'Ref:Address'}],
["BulkUpdateRecord", "Schools", [1, 2, 3, 4], {"grist_Transform": [11, 12, 11, 0]}],
]})
# It seems best if TypeTransform sets widgetOptions on grist_Transform column, so that they
# can be copied in CopyFromColumn; rather than updating them after the copy is done.
self.update_record('_grist_Views_section_field', 1, widgetOptions="hello")
self.update_record('_grist_Tables_column', 24, widgetOptions="world")
out_actions = self.apply_user_action(
['CopyFromColumn', 'Schools', 'grist_Transform', 'city', None])
self.assertPartialOutActions(out_actions, { "stored": [
['ModifyColumn', 'Schools', 'city', {'type': 'Ref:Address'}],
['UpdateRecord', 'Schools', 4, {'city': 0}],
['UpdateRecord', '_grist_Views_section_field', 1, {'widgetOptions': ''}],
['UpdateRecord', '_grist_Tables_column', 23, {
'type': 'Ref:Address', 'widgetOptions': 'world'
}],
['BulkUpdateRecord', 'Schools', [1, 2, 3], {'city': [11, 12, 11]}],
["BulkUpdateRecord", "Schools", [1, 2, 3], {"grist_Transform": [0, 0, 0]}],
]})
out_actions = self.update_record('_grist_Tables_column', 23,
widgetOptions='{"widget":"Reference","visibleCol":"city"}')
self.assertPartialOutActions(out_actions, { "stored": [
['UpdateRecord', '_grist_Tables_column', 23, {
'widgetOptions': '{"widget":"Reference","visibleCol":"city"}'}],
]})
out_actions = self.remove_column('Schools', 'grist_Transform')
self.assertPartialOutActions(out_actions, { "stored": [
['RemoveRecord', '_grist_Views_section_field', 2],
['RemoveRecord', '_grist_Tables_column', 24],
['RemoveColumn', 'Schools', 'grist_Transform'],
]})
#----------------------------------------------------------------------
def test_create_section_existing_view(self):
# Test that CreateViewSection works for an existing view.
self.load_sample(self.sample)
self.assertTables([self.starting_table])
# Create a view + section for the initial table.
self.apply_user_action(["CreateViewSection", 1, 0, "record", None])
# Verify that we got a new view, with one section, and three fields.
self.assertViews([View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[
Field(1, colRef=21),
])
]) ])
# Create a new section for the same view, check that only a section is added.
self.apply_user_action(["CreateViewSection", 1, 1, "record", None])
self.assertTables([self.starting_table])
self.assertViews([View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[
Field(1, colRef=21),
]),
Section(2, parentKey="record", tableRef=1, fields=[
Field(2, colRef=21),
])
]) ])
# Create another section for the same view, this time summarized.
self.apply_user_action(["CreateViewSection", 1, 1, "record", [21]])
summary_table = Table(2, "GristSummary_7_Address", 0, summarySourceTable=1, columns=[
Column(22, "city", "Text", isFormula=False, formula="", summarySourceCol=21),
Column(23, "group", "RefList:Address", isFormula=True,
formula="table.getSummarySourceGroup(rec)", summarySourceCol=0),
Column(24, "count", "Int", isFormula=True, formula="len($group)", summarySourceCol=0),
])
self.assertTables([self.starting_table, summary_table])
# Check that we still have one view, with sections for different tables.
view = View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[
Field(1, colRef=21),
]),
Section(2, parentKey="record", tableRef=1, fields=[
Field(2, colRef=21),
]),
Section(3, parentKey="record", tableRef=2, fields=[
Field(3, colRef=22),
Field(4, colRef=24),
]),
])
self.assertTables([self.starting_table, summary_table])
self.assertViews([view])
# Try to create a summary table for an invalid column, and check that it fails.
with self.assertRaises(ValueError):
self.apply_user_action(["CreateViewSection", 1, 1, "record", [23]])
self.assertTables([self.starting_table, summary_table])
self.assertViews([view])
#----------------------------------------------------------------------
def test_creates_section_new_table(self):
# Test that CreateViewSection works for adding a new table.
self.load_sample(self.sample)
self.assertTables([self.starting_table])
self.assertViews([])
# When we create a section/view for new table, we get both a primary view, and the new view we
# are creating.
self.apply_user_action(["CreateViewSection", 0, 0, "record", None])
new_table = Table(2, "Table1", primaryViewId=1, summarySourceTable=0, columns=[
Column(22, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0),
Column(23, "A", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(24, "B", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(25, "C", "Any", isFormula=True, formula="", summarySourceCol=0),
])
primary_view = View(1, sections=[
Section(1, parentKey="record", tableRef=2, fields=[
Field(1, colRef=23),
Field(2, colRef=24),
Field(3, colRef=25),
])
])
new_view = View(2, sections=[
Section(2, parentKey="record", tableRef=2, fields=[
Field(4, colRef=23),
Field(5, colRef=24),
Field(6, colRef=25),
])
])
self.assertTables([self.starting_table, new_table])
self.assertViews([primary_view, new_view])
# Create another section in an existing view for a new table.
self.apply_user_action(["CreateViewSection", 0, 2, "record", None])
new_table2 = Table(3, "Table2", primaryViewId=3, summarySourceTable=0, columns=[
Column(26, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0),
Column(27, "A", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(28, "B", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(29, "C", "Any", isFormula=True, formula="", summarySourceCol=0),
])
primary_view2 = View(3, sections=[
Section(3, parentKey="record", tableRef=3, fields=[
Field(7, colRef=27),
Field(8, colRef=28),
Field(9, colRef=29),
])
])
new_view.sections.append(
Section(4, parentKey="record", tableRef=3, fields=[
Field(10, colRef=27),
Field(11, colRef=28),
Field(12, colRef=29),
])
)
# Check that we have a new table, only the primary view as new view; and a new section.
self.assertTables([self.starting_table, new_table, new_table2])
self.assertViews([primary_view, new_view, primary_view2])
# Check that we can't create a summary of a table grouped by a column that doesn't exist yet.
with self.assertRaises(ValueError):
self.apply_user_action(["CreateViewSection", 0, 2, "record", [31]])
self.assertTables([self.starting_table, new_table, new_table2])
self.assertViews([primary_view, new_view, primary_view2])
# But creating a new table and showing totals for it is possible though dumb.
self.apply_user_action(["CreateViewSection", 0, 2, "record", []])
# We expect a new table.
new_table3 = Table(4, "Table3", primaryViewId=4, summarySourceTable=0, columns=[
Column(30, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0),
Column(31, "A", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(32, "B", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(33, "C", "Any", isFormula=True, formula="", summarySourceCol=0),
])
# A summary of it.
summary_table = Table(5, "GristSummary_6_Table3", 0, summarySourceTable=4, columns=[
Column(34, "group", "RefList:Table3", isFormula=True,
formula="table.getSummarySourceGroup(rec)", summarySourceCol=0),
Column(35, "count", "Int", isFormula=True, formula="len($group)", summarySourceCol=0),
])
# The primary view of the new table.
primary_view3 = View(4, sections=[
Section(5, parentKey="record", tableRef=4, fields=[
Field(13, colRef=31),
Field(14, colRef=32),
Field(15, colRef=33),
])
])
# And a new view section for the summary.
new_view.sections.append(Section(6, parentKey="record", tableRef=5, fields=[
Field(16, colRef=35)
]))
self.assertTables([self.starting_table, new_table, new_table2, new_table3, summary_table])
self.assertViews([primary_view, new_view, primary_view2, primary_view3])
#----------------------------------------------------------------------
def init_views_sample(self):
# Add a new table and a view, to get some Views/Sections/Fields, and TableView/TabBar items.
self.apply_user_action(['AddTable', 'Schools', [
{'id': 'city', 'type': 'Text'},
{'id': 'state', 'type': 'Text'},
{'id': 'size', 'type': 'Numeric'},
]])
self.apply_user_action(['BulkAddRecord', 'Schools', [1,2,3,4], {
'city': ['New York', 'Colombia', 'New York', ''],
'state': ['NY', 'NY', 'NY', ''],
'size': [1000, 2000, 3000, 4000],
}])
# Add a new view; a second section (summary) to it; and a third view.
self.apply_user_action(['CreateViewSection', 1, 0, 'detail', None])
self.apply_user_action(['CreateViewSection', 1, 2, 'record', [3]])
self.apply_user_action(['CreateViewSection', 1, 0, 'chart', None])
self.apply_user_action(['CreateViewSection', 0, 2, 'record', None])
# Verify the new structure of tables and views.
self.assertTables([
Table(1, "Schools", 1, 0, columns=[
Column(1, "manualSort", "ManualSortPos", False, "", 0),
Column(2, "city", "Text", False, "", 0),
Column(3, "state", "Text", False, "", 0),
Column(4, "size", "Numeric", False, "", 0),
]),
Table(2, "GristSummary_7_Schools", 0, 1, columns=[
Column(5, "state", "Text", False, "", 3),
Column(6, "group", "RefList:Schools", True, "table.getSummarySourceGroup(rec)", 0),
Column(7, "count", "Int", True, "len($group)", 0),
Column(8, "size", "Numeric", True, "SUM($group.size)", 0),
]),
Table(3, 'Table1', 4, 0, columns=[
Column(9, "manualSort", "ManualSortPos", False, "", 0),
Column(10, "A", "Any", True, "", 0),
Column(11, "B", "Any", True, "", 0),
Column(12, "C", "Any", True, "", 0),
]),
])
self.assertViews([
View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[
Field(1, colRef=2),
Field(2, colRef=3),
Field(3, colRef=4),
]),
]),
View(2, sections=[
Section(2, parentKey="detail", tableRef=1, fields=[
Field(4, colRef=2),
Field(5, colRef=3),
Field(6, colRef=4),
]),
Section(3, parentKey="record", tableRef=2, fields=[
Field(7, colRef=5),
Field(8, colRef=7),
Field(9, colRef=8),
]),
Section(6, parentKey='record', tableRef=3, fields=[
Field(15, colRef=10),
Field(16, colRef=11),
Field(17, colRef=12),
]),
]),
View(3, sections=[
Section(4, parentKey="chart", tableRef=1, fields=[
Field(10, colRef=2),
Field(11, colRef=3),
]),
]),
View(4, sections=[
Section(5, parentKey='record', tableRef=3, fields=[
Field(12, colRef=10),
Field(13, colRef=11),
Field(14, colRef=12),
]),
]),
])
self.assertTableData('_grist_TableViews', data=[
["id", "tableRef", "viewRef"],
[1, 1, 2],
[2, 1, 3],
])
self.assertTableData('_grist_TabBar', cols="subset", data=[
["id", "viewRef"],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
])
self.assertTableData('_grist_Pages', cols="subset", data=[
["id", "viewRef"],
[1, 1],
[2, 2],
[3, 3],
[4, 4]
])
#----------------------------------------------------------------------
def test_view_remove(self):
# Add a couple of tables and views, to trigger creation of some related items.
self.init_views_sample()
# Remove a view. Ensure related items, sections, fields get removed.
self.apply_user_action(["BulkRemoveRecord", "_grist_Views", [2,3]])
# Verify the new structure of tables and views.
self.assertTables([
Table(1, "Schools", 1, 0, columns=[
Column(1, "manualSort", "ManualSortPos", False, "", 0),
Column(2, "city", "Text", False, "", 0),
Column(3, "state", "Text", False, "", 0),
Column(4, "size", "Numeric", False, "", 0),
]),
# Note that the summary table is gone.
Table(3, 'Table1', 4, 0, columns=[
Column(9, "manualSort", "ManualSortPos", False, "", 0),
Column(10, "A", "Any", True, "", 0),
Column(11, "B", "Any", True, "", 0),
Column(12, "C", "Any", True, "", 0),
]),
])
self.assertViews([
View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[
Field(1, colRef=2),
Field(2, colRef=3),
Field(3, colRef=4),
]),
]),
View(4, sections=[
Section(5, parentKey='record', tableRef=3, fields=[
Field(12, colRef=10),
Field(13, colRef=11),
Field(14, colRef=12),
]),
]),
])
self.assertTableData('_grist_TableViews', data=[
["id", "tableRef", "viewRef"],
])
self.assertTableData('_grist_TabBar', cols="subset", data=[
["id", "viewRef"],
[1, 1],
[4, 4],
])
self.assertTableData('_grist_Pages', cols="subset", data=[
["id", "viewRef"],
[1, 1],
[4, 4],
])
#----------------------------------------------------------------------
def test_view_rename(self):
# Add a couple of tables and views, to trigger creation of some related items.
self.init_views_sample()
# Verify the new structure of tables and views.
self.assertTableData('_grist_Tables', cols="subset", data=[
[ 'id', 'tableId', 'primaryViewId' ],
[ 1, 'Schools', 1],
[ 2, 'GristSummary_7_Schools', 0],
[ 3, 'Table1', 4],
])
self.assertTableData('_grist_Views', cols="subset", data=[
[ 'id', 'name', 'primaryViewTable' ],
[ 1, 'Schools', 1],
[ 2, 'New page', 0],
[ 3, 'New page', 0],
[ 4, 'Table1', 3],
])
# Update the names in a few views, and ensure that primary ones cause tables to get renamed.
self.apply_user_action(['BulkUpdateRecord', '_grist_Views', [2,3,4],
{'name': ['A', 'B', 'C']}])
self.assertTableData('_grist_Tables', cols="subset", data=[
[ 'id', 'tableId', 'primaryViewId' ],
[ 1, 'Schools', 1],
[ 2, 'GristSummary_7_Schools', 0],
[ 3, 'C', 4],
])
self.assertTableData('_grist_Views', cols="subset", data=[
[ 'id', 'name', 'primaryViewTable' ],
[ 1, 'Schools', 1],
[ 2, 'A', 0],
[ 3, 'B', 0],
[ 4, 'C', 3]
])
#----------------------------------------------------------------------
def test_section_removes(self):
# Add a couple of tables and views, to trigger creation of some related items.
self.init_views_sample()
# Remove a couple of sections. Ensure their fields get removed.
self.apply_user_action(['BulkRemoveRecord', '_grist_Views_section', [3,6]])
self.assertViews([
View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[
Field(1, colRef=2),
Field(2, colRef=3),
Field(3, colRef=4),
]),
]),
View(2, sections=[
Section(2, parentKey="detail", tableRef=1, fields=[
Field(4, colRef=2),
Field(5, colRef=3),
Field(6, colRef=4),
]),
]),
View(3, sections=[
Section(4, parentKey="chart", tableRef=1, fields=[
Field(10, colRef=2),
Field(11, colRef=3),
]),
]),
View(4, sections=[
Section(5, parentKey='record', tableRef=3, fields=[
Field(12, colRef=10),
Field(13, colRef=11),
Field(14, colRef=12),
]),
]),
])
#----------------------------------------------------------------------
def test_schema_consistency_check(self):
# Verify that schema consistency check actually runs, but only when schema is affected.
self.init_views_sample()
# Replace the engine's assert_schema_consistent() method with a mocked version.
orig_method = self.engine.assert_schema_consistent
count_calls = [0]
def override(self): # pylint: disable=unused-argument
count_calls[0] += 1
# pylint: disable=not-callable
orig_method()
self.engine.assert_schema_consistent = types.MethodType(override, self.engine)
# Do a non-sschema action to ensure it doesn't get called.
self.apply_user_action(['UpdateRecord', '_grist_Views', 2, {'name': 'A'}])
self.assertEqual(count_calls[0], 0)
# Do a schema action to ensure it gets called: this causes a table rename.
self.apply_user_action(['UpdateRecord', '_grist_Views', 4, {'name': 'C'}])
self.assertEqual(count_calls[0], 1)
self.assertTableData('_grist_Tables', cols="subset", data=[
[ 'id', 'tableId', 'primaryViewId' ],
[ 1, 'Schools', 1],
[ 2, 'GristSummary_7_Schools', 0],
[ 3, 'C', 4],
])
# Do another schema and non-schema action.
self.apply_user_action(['UpdateRecord', 'Schools', 1, {'city': 'Seattle'}])
self.assertEqual(count_calls[0], 1)
self.apply_user_action(['UpdateRecord', '_grist_Tables_column', 2, {'colId': 'city2'}])
self.assertEqual(count_calls[0], 2)
#----------------------------------------------------------------------
def test_new_column_conversions(self):
self.init_views_sample()
self.apply_user_action(['AddColumn', 'Schools', None, {}])
self.assertTableData('_grist_Tables_column', cols="subset", data=[
["id", "colId", "type", "isFormula", "formula"],
[1, "manualSort", "ManualSortPos",False, ""],
[2, "city", "Text", False, ""],
[3, "state", "Text", False, ""],
[4, "size", "Numeric", False, ""],
[13, "A", "Any", True, ""],
], rows=lambda r: r.parentId.id == 1)
self.assertTableData('Schools', cols="subset", data=[
["id", "city", "A"],
[1, "New York", None],
[2, "Colombia", None],
[3, "New York", None],
[4, "", None],
])
# Check that typing in text into the column produces a text column.
out_actions = self.apply_user_action(['UpdateRecord', 'Schools', 3, {"A": "foo"}])
self.assertTableData('_grist_Tables_column', cols="subset", rows="subset", data=[
["id", "colId", "type", "isFormula", "formula"],
[13, "A", "Text", False, ""],
])
self.assertTableData('Schools', cols="subset", data=[
["id", "city", "A" ],
[1, "New York", "" ],
[2, "Colombia", "" ],
[3, "New York", "foo" ],
[4, "", "" ],
])
# Undo, and check that typing in a number produces a numeric column.
self.apply_undo_actions(out_actions.undo)
out_actions = self.apply_user_action(['UpdateRecord', 'Schools', 3, {"A": " -17.6"}])
self.assertTableData('_grist_Tables_column', cols="subset", rows="subset", data=[
["id", "colId", "type", "isFormula", "formula"],
[13, "A", "Numeric", False, ""],
])
self.assertTableData('Schools', cols="subset", data=[
["id", "city", "A" ],
[1, "New York", 0.0 ],
[2, "Colombia", 0.0 ],
[3, "New York", -17.6 ],
[4, "", 0.0 ],
])
# Undo, and set a formula for the new column instead.
self.apply_undo_actions(out_actions.undo)
self.apply_user_action(['UpdateRecord', '_grist_Tables_column', 13, {'formula': 'len($city)'}])
self.assertTableData('_grist_Tables_column', cols="subset", rows="subset", data=[
["id", "colId", "type", "isFormula", "formula"],
[13, "A", "Any", True, "len($city)"],
])
self.assertTableData('Schools', cols="subset", data=[
["id", "city", "A" ],
[1, "New York", 8 ],
[2, "Colombia", 8 ],
[3, "New York", 8 ],
[4, "", 0 ],
])
# Convert the formula column to non-formula.
self.apply_user_action(['UpdateRecord', '_grist_Tables_column', 13, {'isFormula': False}])
self.assertTableData('_grist_Tables_column', cols="subset", rows="subset", data=[
["id", "colId", "type", "isFormula", "formula"],
[13, "A", "Numeric", False, "len($city)"],
])
self.assertTableData('Schools', cols="subset", data=[
["id", "city", "A" ],
[1, "New York", 8 ],
[2, "Colombia", 8 ],
[3, "New York", 8 ],
[4, "", 0 ],
])
# Add some more formula columns of type 'Any'.
self.apply_user_action(['AddColumn', 'Schools', None, {"formula": "1"}])
self.apply_user_action(['AddColumn', 'Schools', None, {"formula": "'x'"}])
self.apply_user_action(['AddColumn', 'Schools', None, {"formula": "$city == 'New York'"}])
self.apply_user_action(['AddColumn', 'Schools', None, {"formula": "$city=='New York' or '-'"}])
self.assertTableData('_grist_Tables_column', cols="subset", data=[
["id", "colId", "type", "isFormula", "formula"],
[1, "manualSort", "ManualSortPos",False, ""],
[2, "city", "Text", False, ""],
[3, "state", "Text", False, ""],
[4, "size", "Numeric", False, ""],
[13, "A", "Numeric", False, "len($city)"],
[14, "B", "Any", True, "1"],
[15, "C", "Any", True, "'x'"],
[16, "D", "Any", True, "$city == 'New York'"],
[17, "E", "Any", True, "$city=='New York' or '-'"],
], rows=lambda r: r.parentId.id == 1)
self.assertTableData('Schools', cols="subset", data=[
["id", "city", "A", "B", "C", "D", "E"],
[1, "New York", 8, 1, "x", True, True],
[2, "Colombia", 8, 1, "x", False, '-' ],
[3, "New York", 8, 1, "x", True, True],
[4, "", 0, 1, "x", False, '-' ],
])
# Convert all these formulas to non-formulas, and see that their types get guessed OK.
# TODO: We should also guess Int, Bool, Reference, ReferenceList, Date, and DateTime.
# TODO: It is possibly better if B became Int, and D became Bool.
self.apply_user_action(['BulkUpdateRecord', '_grist_Tables_column', [14,15,16,17],
{'isFormula': [False, False, False, False]}])
self.assertTableData('_grist_Tables_column', cols="subset", data=[
["id", "colId", "type", "isFormula", "formula"],
[1, "manualSort", "ManualSortPos",False, ""],
[2, "city", "Text", False, ""],
[3, "state", "Text", False, ""],
[4, "size", "Numeric", False, ""],
[13, "A", "Numeric", False, "len($city)"],
[14, "B", "Numeric", False, "1"],
[15, "C", "Text", False, "'x'"],
[16, "D", "Text", False, "$city == 'New York'"],
[17, "E", "Text", False, "$city=='New York' or '-'"],
], rows=lambda r: r.parentId.id == 1)
self.assertTableData('Schools', cols="subset", data=[
["id", "city", "A", "B", "C", "D", "E"],
[1, "New York", 8, 1.0, "x", "True", 'True'],
[2, "Colombia", 8, 1.0, "x", "False", '-' ],
[3, "New York", 8, 1.0, "x", "True", 'True'],
[4, "", 0, 1.0, "x", "False", '-' ],
])
#----------------------------------------------------------------------
def test_useraction_failures(self):
# Verify that when a useraction fails, we revert any changes already applied.
self.load_sample(self.sample)
# Simple failure: bad action (last argument should be a dict). It shouldn't cause any actions
# in the first place, just raise an exception about the argument being an int.
with self.assertRaisesRegexp(AttributeError, r"'int'"):
self.apply_user_action(['AddColumn', 'Address', "A", 17])
# Do some successful actions, just to make sure we know what they look like.
self.engine.apply_user_actions([useractions.from_repr(ua) for ua in (
['AddColumn', 'Address', "B", {"isFormula": True}],
['UpdateRecord', 'Address', 11, {"city": "New York2"}],
)])
# More complicated: here some actions should succeed, but get reverted when a later one fails.
with self.assertRaisesRegexp(AttributeError, r"'int'"):
self.engine.apply_user_actions([useractions.from_repr(ua) for ua in (
['UpdateRecord', 'Address', 11, {"city": "New York3"}],
['AddColumn', 'Address', "C", {"isFormula": True}],
['AddColumn', 'Address', "D", 17]
)])
with self.assertRaisesRegexp(Exception, r"non-existent record #77"):
self.engine.apply_user_actions([useractions.from_repr(ua) for ua in (
['UpdateRecord', 'Address', 11, {"city": "New York4"}],
['UpdateRecord', 'Address', 77, {"city": "Chicago"}],
)])
# Make sure that no columns got added except the intentionally successful one.
self.assertTableData('_grist_Tables_column', cols="subset", data=[
["id", "colId", "type", "isFormula", "formula"],
[21, "city", "Text", False, ""],
[22, "B", "Any", True, ""],
], rows=lambda r: r.parentId.id == 1)
# Make sure that no columns got added here either, and the only change to "New York" is the
# one in the successful user-action.
self.assertTableData('Address', cols="all", data=[
["id", "city" , "B" ],
[11, "New York2" , None ],
[12, "Colombia" , None ],
[13, "New Haven" , None ],
[14, "West Haven", None ],
])
#----------------------------------------------------------------------
def test_acl_principal_actions(self):
# Test the AddUser, RemoveUser, AddInstance and RemoveInstance actions.
self.load_sample(self.sample)
# Add two users
out_actions = self.apply_user_action(['AddUser', 'jake@grist.com', 'Jake', ['i001', 'i002']])
self.assertPartialOutActions(out_actions, { "stored": [
["AddRecord", "_grist_ACLPrincipals", 1, {
"type": "user",
"userEmail": "jake@grist.com",
"userName": "Jake"
}],
["BulkAddRecord", "_grist_ACLPrincipals", [2, 3], {
"instanceId": ["i001", "i002"],
"type": ["instance", "instance"]
}],
["BulkAddRecord", "_grist_ACLMemberships", [1, 2], {
"child": [2, 3],
"parent": [1, 1]
}]
]})
out_actions = self.apply_user_action(['AddUser', 'steve@grist.com', 'Steve', ['i003']])
self.assertPartialOutActions(out_actions, { "stored": [
["AddRecord", "_grist_ACLPrincipals", 4, {
"type": "user",
"userEmail": "steve@grist.com",
"userName": "Steve"
}],
["AddRecord", "_grist_ACLPrincipals", 5, {
"instanceId": "i003",
"type": "instance"
}],
["AddRecord", "_grist_ACLMemberships", 3, {
"child": 5,
"parent": 4
}]
]})
self.assertTableData('_grist_ACLPrincipals', cols="subset", data=[
["id", "type", "userEmail", "userName", "groupName", "instanceId"],
[1, "user", "jake@grist.com", "Jake", "", ""],
[2, "instance", "", "", "", "i001"],
[3, "instance", "", "", "", "i002"],
[4, "user", "steve@grist.com", "Steve", "", ""],
[5, "instance", "", "", "", "i003"],
])
self.assertTableData('_grist_ACLMemberships', cols="subset", data=[
["id", "parent", "child"],
[1, 1, 2],
[2, 1, 3],
[3, 4, 5]
])
# Add an instance to a non-existent user
with self.assertRaisesRegexp(ValueError, "Cannot find existing user with email null@grist.com"):
self.apply_user_action(['AddInstance', 'null@grist.com', 'i003'])
# Add an instance to an existing user
out_actions = self.apply_user_action(['AddInstance', 'jake@grist.com', 'i004'])
self.assertPartialOutActions(out_actions, { "stored": [
["AddRecord", "_grist_ACLPrincipals", 6, {
"instanceId": "i004",
"type": "instance"
}],
["AddRecord", "_grist_ACLMemberships", 4, {
"child": 6,
"parent": 1
}]
]})
self.assertTableData('_grist_ACLPrincipals', cols="subset", data=[
["id", "type", "userEmail", "userName", "groupName", "instanceId"],
[1, "user", "jake@grist.com", "Jake", "", ""],
[2, "instance", "", "", "", "i001"],
[3, "instance", "", "", "", "i002"],
[4, "user", "steve@grist.com", "Steve", "", ""],
[5, "instance", "", "", "", "i003"],
[6, "instance", "", "", "", "i004"],
])
self.assertTableData('_grist_ACLMemberships', cols="subset", data=[
["id", "parent", "child"],
[1, 1, 2],
[2, 1, 3],
[3, 4, 5],
[4, 1, 6]
])
# Remove a non-existent instance from a user
with self.assertRaisesRegexp(ValueError, "Cannot find existing instance id i000"):
self.apply_user_action(['RemoveInstance', 'i000'])
# Remove an instance from a user
out_actions = self.apply_user_action(['RemoveInstance', 'i002'])
self.assertPartialOutActions(out_actions, { "stored": [
["RemoveRecord", "_grist_ACLMemberships", 2],
["RemoveRecord", "_grist_ACLPrincipals", 3]
]})
self.assertTableData('_grist_ACLPrincipals', cols="subset", data=[
["id", "type", "userEmail", "userName", "groupName", "instanceId"],
[1, "user", "jake@grist.com", "Jake", "", ""],
[2, "instance", "", "", "", "i001"],
[4, "user", "steve@grist.com", "Steve", "", ""],
[5, "instance", "", "", "", "i003"],
[6, "instance", "", "", "", "i004"],
])
self.assertTableData('_grist_ACLMemberships', cols="subset", data=[
["id", "parent", "child"],
[1, 1, 2],
[3, 4, 5],
[4, 1, 6]
])
# Remove a non-existent user
with self.assertRaisesRegexp(ValueError, "Cannot find existing user with email null@grist.com"):
self.apply_user_action(['RemoveUser', 'null@grist.com'])
# Remove an existing user
out_actions = self.apply_user_action(['RemoveUser', 'jake@grist.com'])
self.assertPartialOutActions(out_actions, { "stored": [
["BulkRemoveRecord", "_grist_ACLMemberships", [1, 4]],
["BulkRemoveRecord", "_grist_ACLPrincipals", [2, 6, 1]]
]})
self.assertTableData('_grist_ACLPrincipals', cols="subset", data=[
["id", "type", "userEmail", "userName", "groupName", "instanceId"],
[4, "user", "steve@grist.com", "Steve", "", ""],
[5, "instance", "", "", "", "i003"],
])
self.assertTableData('_grist_ACLMemberships', cols="subset", data=[
["id", "parent", "child"],
[3, 4, 5]
])
# Remove the only instance of an existing user, removing that user
out_actions = self.apply_user_action(['RemoveInstance', 'i003'])
self.assertPartialOutActions(out_actions, { "stored": [
["RemoveRecord", "_grist_ACLMemberships", 3],
["BulkRemoveRecord", "_grist_ACLPrincipals", [4, 5]]
]})
self.assertTableData('_grist_ACLPrincipals', cols="subset", data=[
["id", "type", "userEmail", "userName", "groupName", "instanceId"]
])
self.assertTableData('_grist_ACLMemberships', cols="subset", data=[
["id", "parent", "child"]
])
#----------------------------------------------------------------------
def test_pages_remove(self):
# Test that orphan pages get fixed after removing a page
self.init_views_sample()
# Moves page 2 to children of page 1.
self.apply_user_action(['BulkUpdateRecord', '_grist_Pages', [2], {'indentation': [1]}])
self.assertTableData('_grist_Pages', cols='subset', data=[
['id', 'indentation'],
[ 1, 0],
[ 2, 1],
[ 3, 0],
[ 4, 0],
])
# Verify that removing page 1 fixes page 2 indentation.
self.apply_user_action(['RemoveRecord', '_grist_Pages', 1])
self.assertTableData('_grist_Pages', cols='subset', data=[
['id', 'indentation'],
[ 2, 0],
[ 3, 0],
[ 4, 0],
])
# Removing last page should not fail
# Verify that removing page 1 fixes page 2 indentation.
self.apply_user_action(['RemoveRecord', '_grist_Pages', 4])
self.assertTableData('_grist_Pages', cols='subset', data=[
['id', 'indentation'],
[ 2, 0],
[ 3, 0],
])
# Removing a page that has no children should do nothing
self.apply_user_action(['RemoveRecord', '_grist_Pages', 2])
self.assertTableData('_grist_Pages', cols='subset', data=[
['id', 'indentation'],
[ 3, 0],
])