mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
16f297a250
Summary: Changes that move towards python 3 compatibility that are easy to review without much thought Test Plan: The tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2873
773 lines
32 KiB
Python
773 lines
32 KiB
Python
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.assertRaisesRegex(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.assertRaisesRegex(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.assertRaisesRegex(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_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],
|
|
])
|