Files
MyManagingTools/tests/test_datagrid.py

2106 lines
62 KiB
Python

from collections import OrderedDict
from datetime import datetime
import pytest
from components.datagrid.DataGrid import *
from helpers import matches, extract_popup_content, extract_table_values, Empty, to_array, search_elements_by_name, get_selected, \
get_context_menu
TEST_GRID_ID = "testing_grid_id"
@pytest.fixture()
def dg():
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25]
})
return DataGrid(df, id=TEST_GRID_ID)
@pytest.fixture()
def dg2():
"""
More elements
:return:
"""
df = pd.DataFrame({
'Name': ['Alice', 'Bob', 'Charlie', 'David'],
'Age': [20, 25, 30, 35],
'City': ['New York', 'Los Angeles', 'Chicago', 'Houston'],
'Student': [True, False, True, False],
'Date': [datetime(2020, 1, 10), datetime(2022, 10, 1), datetime(2007, 1, 1), datetime(2000, 12, 31), ]
})
return DataGrid(df, id=TEST_GRID_ID)
@pytest.fixture()
def grid_id(dg):
return dg.get_grid_id()
def test_i_can_manage_instance():
datagrid_1 = DataGrid(id="id1")
datagrid_2 = DataGrid(id="id2")
datagrid_3 = DataGrid(id="id1")
assert datagrid_1 is not datagrid_2
assert datagrid_1 is datagrid_3
def test_i_can_render_empty_data_grid():
grid_id = "testing_grid"
dg = DataGrid(id=grid_id)
expected = Div(
Div(), # container for keyboard handlers
Div(
Div(id=f"tt_datagrid-{grid_id}"), # tooltip
Div(id=f"fa_datagrid-{grid_id}"), # filter component
Div(name="dt-controls"), # reset filter, resize, ...
cls="flex justify-between"),
Div(
Div(id=f"tsm_datagrid-{grid_id}"), # selection management
Div(id=f"tdd_datagrid-{grid_id}"), # drag and drop indicator
Div(id=f"tcdd_datagrid-{grid_id}"), # cell drop down
Div(), # navigation keyboards
Div(
Div(id=f"th_datagrid-{grid_id}"),
Div(id=f"tb_datagrid-{grid_id}"),
Div(id=f"tf_datagrid-{grid_id}"),
),
Script(),
id=f"t_datagrid-{grid_id}",
cls="dt-table"
)
)
res = dg.__ft__()
assert matches(res, expected)
def test_test_there_is_no_filter_input_when_gs_filter_input_is_false():
grid_id = "testing_grid"
grid_setting = {DG_FILTER_INPUT: False}
dg = DataGrid(id=grid_id, grid_settings=grid_setting)
expected = Div(
Div(), # container for keyboard handlers
Div(
Div(id=f"tt_datagrid-{grid_id}"), # tooltip
Div(Empty), # filter component
Div(name="dt-controls"), # reset filter, resize, ...
cls="flex justify-between"),
Div(
Div(id=f"tsm_datagrid-{grid_id}"), # selection management
Div(id=f"tdd_datagrid-{grid_id}"), # drag and drop indicator
Div(id=f"tcdd_datagrid-{grid_id}"), # cell drop down
Div(), # navigation keyboards
Div(
Div(id=f"th_datagrid-{grid_id}"),
Div(id=f"tb_datagrid-{grid_id}"),
Div(id=f"tf_datagrid-{grid_id}")),
Script(),
id=f"t_datagrid-{grid_id}",
cls="dt-table"
)
)
res = dg.__ft__()
assert matches(res, expected)
def test_there_is_no_filter_button_in_header_when_gs_filter_input_is_false():
grid_id = "testing_grid"
grid_setting = {DG_FILTER_INPUT: False}
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25]
})
dg = DataGrid(df, id=grid_id, grid_settings=grid_setting)
header = dg.mk_table_header()
expected = Div(
Div(
Div(Div("Name")), # name
Div(Div()), # sort
Div(), # column clickable extension
Div(Empty), # No filtering icon
Div(Empty), # No filtering icon
Div(), # resize handle
cls='dt-cell dt-draggable dt-resizable', data_col='name'),
Div(
Div(Div("Age")), # name
Div(Div()), # sort
Div(), # column clickable extension
Div(Empty), # No filtering icon
Div(Empty), # No filtering icon
Div(), # resize handle
cls='dt-cell dt-draggable dt-resizable', data_col='age'),
)
assert matches(header, expected)
def test_i_can_generate_grid_html(dg, grid_id):
resize_attr = {"hx-on::after-settle": f"setColumnsWidths('{grid_id}', false);"}
expected = Div(
Div(
Div(
Div(Div("Name")),
Div(Div(), hx_post=f"/{DATAGRID_PATH}/sort"), # sort
Div(cls="dt-flex-grow"), # column clickable extension
Div(cls='icon-24 dt-filter-icon icon-btn'),
Div(cls="dt-filter-popup"),
Div(cls="dt-resize-handle"),
data_col="name"
),
Div(
Div(Div("Age")),
Div(Div(), hx_post=f"/{DATAGRID_PATH}/sort"), # sort
Div(cls="dt-flex-grow"), # column clickable extension
Div(cls='icon-24 dt-filter-icon icon-btn'),
Div(cls="dt-filter-popup"),
Div(cls="dt-resize-handle"),
data_col="age"
),
cls="dt-row dt-header",
id=f"th_{grid_id}"),
Div(
Div(
Div(Div("Alice"), data_col="name"),
Div(Div("20"), data_col="age"),
),
Div(
Div(Div("Bob"), data_col="name"),
Div(Div("25"), data_col="age"),
),
cls="dt-body",
id=f"tb_{grid_id}",
**resize_attr,
),
Div(
Div(
Div(Div(), data_col="name"),
Div(Div(), data_col="age"),
),
id=f"tf_{grid_id}")
)
res = dg.mk_table()
table = search_elements_by_name(res, None, {"class": "dt-inner-table"})[0]
# as_ft = debug_print(res)
matches(table, expected)
def test_i_can_render_boolean():
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25],
'Is_Good': [True, False]
})
dg = DataGrid(df)
_grid_id = dg.get_grid_id()
expected = Div(
Div(
Div(Div("Alice")),
Div(Div("20")),
Div(Div(NotStr('<svg name="CheckboxChecked16Regular"'))),
),
Div(
Div(Div("Bob")),
Div(Div("25")),
Div(Div(NotStr('<svg name="CheckboxUnchecked16Regular"'))),
),
cls="dt-body",
id=f"tb_{_grid_id}")
res = dg.mk_table_body()
matches(res, expected)
def test_i_can_render_when_width_is_set():
grid_id = "testing_grid"
grid_setting = {DG_COLUMNS: {
"age": {
"index": 0,
"title": "Age",
"type": DG_DATATYPE_NUMBER,
"width": "150px"
},
"name": {
"index": 1,
"title": "Name",
"type": DG_DATATYPE_STRING,
}
}}
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25]
})
dg = DataGrid(df, id=grid_id, grid_settings=grid_setting)
header = dg.mk_table_header()
expected = Div(
Div(
Div(Div("Age")),
cls='dt-cell dt-draggable dt-resizable dt-fixed-col', data_col='age', style="width:150px;"),
Div(
Div(Div("Name")),
cls='dt-cell dt-draggable dt-resizable', data_col='name')
)
assert matches(header, expected)
def test_i_can_compute_choices_values():
grid_settings = {DG_COLUMNS: {
"name": {
"index": 0,
"title": "Name",
},
"choice1": {
"index": 3, # Safe index should be used instead of this value
"title": "Choice1",
"type": DG_DATATYPE_CHOICE,
"values": ["A", "B", "C"],
},
"choice2": {
"index": 4, # Safe index should be used instead of this value
"title": "Choice2",
"type": DG_DATATYPE_CHOICE,
"values": ["Alpha", "Beta", "Gamma"],
}
}}
df = pd.DataFrame({
'Name': ['Alice', 'Bob', 'Charlie', 'Dave'],
'Choice1': ['A', 'B', 'C', None],
'Choice2': ['Gamma', 'Alpha', None, 'Beta'],
})
dg = DataGrid(df, grid_settings=grid_settings)
assert dg.get_state().choice_columns_values == {
1: {0: [True, False, False], 1: [False, True, False], 2: [False, False, True], 3: [False, False, False]},
2: {0: [False, False, True], 1: [True, False, False], 2: [False, False, False], 3: [False, True, False]},
}
#
# def test_i_can_render_choice():
# grid_settings = {DG_COLUMNS: {
# "name": {
# "index": 0,
# "title": "Name",
# },
# "choice": {
# "index": 1,
# "title": "Choice",
# "type": DG_DATATYPE_CHOICE,
# "values": ["A", "B", "C"],
# }
# }}
#
# df = pd.DataFrame({
# 'Name': ['Alice', 'Bob', 'Charlie'],
# })
#
# dg = DataGrid(df, grid_settings=grid_settings)
#
# _grid_id = dg.get_grid_id()
# expected_header = Div(
# Div(
# Div(Div("Name")),
# Div(Div(), hx_post=f"/{DATAGRID_PATH}/sort"), # sort
# Div(cls="dt-flex-grow"), # column clickable extension
# Div(cls='icon-24 dt-filter-icon icon-btn'),
# Div(cls="dt-filter-popup"),
# Div(cls="dt-resize-handle"),
# data_col="name"
# ),
# Div(
# Div(Div("Choice")),
# Div(Div(), hx_post=f"/{DATAGRID_PATH}/sort"), # sort
# Div(cls="dt-flex-grow"), # column clickable extension
# Div(cls='icon-24 dt-filter-icon icon-btn'),
# Div(cls="dt-filter-popup"),
# Div(cls="dt-resize-handle"),
# data_col="choice"
# ),
# cls="dt-row dt-header",
# id=f"th_{_grid_id}")
#
# res_header = dg.mk_table_header()
# matches(res_header, expected_header)
# def test_i_can_update_choice_value_column():
# assert False
def test_there_is_no_header_when_gs_header_is_false():
grid_id = "testing_grid"
grid_setting = {DG_TABLE_HEADER: False}
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25]
})
dg = DataGrid(df, id=grid_id, grid_settings=grid_setting)
header = dg.mk_table_header()
assert matches(header, Div(cls="dt-row dt-header hidden"))
def test_there_is_no_footer_when_gs_footer_is_false():
grid_id = "testing_grid"
grid_setting = {DG_TABLE_FOOTER: False}
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25]
})
dg = DataGrid(df, id=grid_id, grid_settings=grid_setting)
footer = dg.mk_table_footer()
assert matches(footer, Div(Empty))
def test_i_can_render_when_some_columns_are_not_visible(dg, grid_id):
grid_id = "testing_grid"
grid_setting = {DG_COLUMNS: {
"name": {VISIBLE_KEY: False, INDEX_KEY: 0, "agg_func": DG_AGGREGATE_SUM},
"age": {INDEX_KEY: 1, "agg_func": DG_AGGREGATE_SUM},
}}
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25]
})
dg = DataGrid(df, id=grid_id, grid_settings=grid_setting)
table_header = dg.mk_table_header()
table_body = dg.mk_table_body()
table_footer = dg.mk_table_footer()
expected_header = Div(
Div(cls="dt-header-hidden", hx_post=f"/{DATAGRID_PATH}/on_state_changed", ),
Div(data_col="age", data_tooltip="Age"),
)
assert matches(table_header, expected_header)
expected_body = Div(
Div(Div(cls="dt-col-hidden"), Div(data_col="age", data_tooltip="20"), ), # first row
Div(Div(cls="dt-col-hidden"), Div(data_col="age", data_tooltip="25"), ), # second row
)
assert matches(table_body, expected_body)
expected_footer = Div(
Div(Div(cls="dt-col-hidden"), Div(data_col="age"))
)
assert matches(table_footer, expected_footer)
def test_i_can_hide_a_column(dg, grid_id):
dg.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["name"], "true", "false")
table_header = dg.mk_table_header()
table_body = dg.mk_table_body()
table_footer = dg.mk_table_footer()
expected_header = Div(
Div(cls="dt-header-hidden", hx_post=f"/{DATAGRID_PATH}/on_state_changed", ),
Div(data_col="age", data_tooltip="Age"),
)
assert matches(table_header, expected_header)
expected_body = Div(
Div(Div(cls="dt-col-hidden"), Div(data_col="age", data_tooltip="20"), ), # first row
Div(Div(cls="dt-col-hidden"), Div(data_col="age", data_tooltip="25"), ), # second row
)
assert matches(table_body, expected_body)
expected_footer = Div(
Div(Div(cls="dt-col-hidden"), Div(data_col="age"))
)
assert matches(table_footer, expected_footer)
def test_i_can_generate_sorting_capabilities(dg, grid_id):
name_attrs = {
"hx_post": f"/{DATAGRID_PATH}/sort",
"hx_vals": f'{{"g_id": "{grid_id}", "c_id":"name"}}',
"hx_target": f"#tb_{grid_id}",
}
age_attrs = {
"hx_post": f"/{DATAGRID_PATH}/sort",
"hx_vals": f'{{"g_id": "{grid_id}", "c_id":"age"}}',
"hx_target": f"#tb_{grid_id}",
}
expected = Div(
Div(
Div(Div("Name")),
Div(
Div(id=f"tsi_datagrid-testing_grid_id_name"),
**name_attrs)),
Div(
Div(Div("Age")),
Div(
Div(id=f"tsi_datagrid-testing_grid_id_age"),
**age_attrs))
)
res = dg.mk_table_header()
# as_ft = debug_print(res[0])
matches(res, expected)
def test_i_can_sort(dg):
state = dg.get_state()
assert state.sorted == []
assert dg.get_sort_icons() == {}
dg.sort("name") # asc
assert state.sorted == [SortDefinition(column_id='name', direction=1)]
assert "name" in dg.get_sort_icons()
assert matches(dg.mk_table_body(), Div(
Div(
Div(Div("Alice")),
Div(Div("20")),
),
Div(
Div(Div("Bob")),
Div(Div("25")),
),
))
dg.sort("name") # desc
assert state.sorted == [SortDefinition(column_id='name', direction=2)]
assert "name" in dg.get_sort_icons()
assert matches(dg.mk_table_body(), Div(
Div(
Div(Div("Bob")),
Div(Div("25")),
),
Div(
Div(Div("Alice")),
Div(Div("20")),
),
))
dg.sort("name") # back to default
assert state.sorted == [SortDefinition(column_id='name', direction=0)]
assert "name" not in dg.get_sort_icons()
assert matches(dg.mk_table_body(), Div(
Div(
Div(Div("Alice")),
Div(Div("20")),
),
Div(
Div(Div("Bob")),
Div(Div("25")),
),
))
dg.sort("name") # asc again
assert state.sorted == [SortDefinition(column_id='name', direction=1)]
assert "name" in dg.get_sort_icons()
assert matches(dg.mk_table_body(), Div(
Div(
Div(Div("Alice")),
Div(Div("20")),
),
Div(
Div(Div("Bob")),
Div(Div("25")),
),
))
def test_i_can_filter_when_found(dg, grid_id):
dg.filter(FILTER_INPUT_CID, 'Ali') # filter is not case-sensitive !
expected = Div(
Div(
Div(
Div(Span('Ali', cls='dt-highlight-1'),
Span('ce'))
),
Div(Div("20"))
),
id=f"tb_{grid_id}"
)
res = dg.mk_table_body()
matches(res, expected)
table_content = extract_table_values(res, header=False)
assert len(table_content) == 1
def test_i_can_filter_when_not_found(dg, grid_id):
dg.filter(FILTER_INPUT_CID, 'not found')
expected = Div(id=f"tb_{grid_id}") # empty grid
res = dg.mk_table_body()
matches(res, expected)
def test_i_can_filter_a_column(dg):
dg.filter('name', ['Alice'])
expected = Div(
Div(
Div(Div('Alice')),
Div(Div("20"))
),
)
res = dg.mk_table_body()
matches(res, expected)
table_content = extract_table_values(res, header=False)
assert len(table_content) == 1
def test_i_can_filter_an_int_column(dg):
dg.filter('age', ['20']) # not that we filter as string
expected = Div(
Div(
Div(Div('Alice')),
Div(Div("20"))
),
)
res = dg.mk_table_body()
matches(res, expected)
table_content = extract_table_values(res, header=False)
assert len(table_content) == 1
def test_i_can_reset_filter(dg):
expected = Div(
Div(
Div(Div("Alice")),
Div(Div("20")),
),
Div(
Div(Div("Bob")),
Div(Div("25")),
),
)
dg.filter(FILTER_INPUT_CID, 'ali')
dg.reset_filter(None)
res = dg.mk_table_body()
matches(res, expected)
def test_compute_grid_settings():
df = pd.DataFrame({
'Name': ['Bob', 'Alice'],
'Age': [25, 20]
})
settings = DataGrid.compute_grid_settings(df)
assert settings[DG_COLUMNS] == {'age': {'index': 1, 'safe_index': 1, 'title': 'Age', 'type': 'number'},
'name': {'index': 0, 'safe_index': 0, 'title': 'Name', 'type': 'string'}}
settings = DataGrid.compute_grid_settings(df, grid_setting={})
assert settings[DG_COLUMNS] == {'age': {'index': 1, 'safe_index': 1, 'title': 'Age', 'type': 'number'},
'name': {'index': 0, 'safe_index': 0, 'title': 'Name', 'type': 'string'}}
settings = DataGrid.compute_grid_settings(df, grid_setting={DG_COLUMNS: {}})
assert settings[DG_COLUMNS] == {}
def test_i_can_compute_safe_index():
grid_settings = {DG_COLUMNS: {'age': {'index': 15},
'name': {'index': 10, },
}}
settings = DataGrid.compute_grid_settings(None, grid_setting=grid_settings)
assert settings[DG_COLUMNS] == {'age': {'index': 15, 'safe_index': 1, 'title': 'Age'},
'name': {'index': 10, 'safe_index': 0, 'title': 'Name'}, }
def test_i_can_filter_the_columns_using_compute_grid_settings():
df = pd.DataFrame({
'Name': ['Bob', 'Alice'],
'Age': [25, 20]
})
settings = DataGrid.compute_grid_settings(df, columns=["age"])
assert settings[DG_COLUMNS] == {'age': {'safe_index': 0, 'title': 'Age'}}
def test_i_can_add_new_columns_when_compute_grid_settings():
df = pd.DataFrame({
'Name': ['Bob', 'Alice'],
'Age': [25, 20]
})
settings = DataGrid.compute_grid_settings(df, columns=["new_column", "age"])
assert settings[DG_COLUMNS] == {'new_column': {'safe_index': 0, 'title': 'New Column'},
'age': {'safe_index': 1, 'title': 'Age'}}
def test_i_can_add_new_columns_when_compute_grid_settings_from_nothing():
settings = DataGrid.compute_grid_settings(None, columns=["new_column", "age"])
assert settings[DG_COLUMNS] == {'new_column': {'safe_index': 0, 'title': 'New Column'},
'age': {'safe_index': 1, 'title': 'Age'}}
def test_i_can_rearrange_columns_using_compute_grid_settings():
df = pd.DataFrame({
'Name': ['Bob', 'Alice'],
'Age': [25, 20]
})
settings = DataGrid.compute_grid_settings(df, columns=["age", "other_column_1", "other_column_2"])
assert settings[DG_COLUMNS] == {
'age': {'safe_index': 0, 'title': 'Age'},
'other_column_1': {'safe_index': 1, 'title': 'Other Column 1'},
'other_column_2': {'safe_index': 2, 'title': 'Other Column 2'}}
def test_i_can_init_with_simplified_grid_settings():
grid_settings = {DG_COLUMNS: ["Age", "Other Column 1", "Other Column 2"]}
dg = DataGrid(grid_settings=grid_settings)
settings = dg._grid_settings
assert settings[DG_COLUMNS] == {'age': {'index': 0, 'safe_index': 0, 'title': 'Age'},
'other_column_1': {'index': 1, 'safe_index': 1, 'title': 'Other Column 1'},
'other_column_2': {'index': 2, 'safe_index': 2, 'title': 'Other Column 2'}}
def test_i_can_update_dataframe():
df = pd.DataFrame({
'Name': ['Bob', 'Alice'],
'Age': [25, 20]
})
expected = pd.DataFrame({
'name': ['Bob', 'Alice'],
'age': [25, 20]
})
grid_settings = DataGrid.compute_grid_settings(df)
df = DataGrid._get_updated_dataframe(df, grid_settings)
assert df.equals(expected)
def test_i_can_update_dataframe_when_changing_column_order():
df = pd.DataFrame({
'Name': ['Bob', 'Alice'],
'Age': [25, 20]
})
expected = pd.DataFrame({
'age': [25, 20],
'name': ['Bob', 'Alice'],
})
grid_settings = {
DG_COLUMNS: {
'age': {'index': 0, 'title': 'Age'},
'name': {'index': 1, 'title': 'Name'}
}
}
df = DataGrid._get_updated_dataframe(df, grid_settings)
assert df.equals(expected)
def test_i_can_update_dataframe_when_changing_column_order_and_adding_new_columns():
df = pd.DataFrame({
'Name': ['Bob', 'Alice'],
'Age': [25, 20]
})
expected = pd.DataFrame({
'age': [25, 20],
'new_column': [None, None],
'name': ['Bob', 'Alice'],
})
grid_settings = {
DG_COLUMNS: {
'age': {'index': 0, 'title': 'Age'},
'new_column': {'index': 1, 'title': 'New Column'},
'name': {'index': 2, 'title': 'Name'}
}
}
df = DataGrid._get_updated_dataframe(df, grid_settings)
assert df.equals(expected)
def test_i_can_import_file_when_none():
dg = DataGrid()
dg.import_file(None, None)
assert dg._df is None
assert dg._grid_settings[DG_COLUMNS] == {}
def test_i_can_reset_df_when_import_none(dg):
assert dg._df is not None
assert dg._grid_settings[DG_COLUMNS] != {}
dg.import_file(None, None)
assert dg._df is None
assert dg._grid_settings[DG_COLUMNS] == {}
def test_i_can_import_a_file(excel_file_content):
dg = DataGrid()
dg.import_file("file_name", excel_file_content)
assert dg._df is not None
assert dg._grid_settings[DG_COLUMNS] == {
'column_1': {'index': 0, 'safe_index': 0, 'title': 'Column 1', 'type': 'string'},
'column_2': {'index': 1, 'safe_index': 1, 'title': 'Column 2', 'type': 'string'}}
def test_i_can_reimport_a_file(dg, excel_file_content):
dg.import_file("file_name", excel_file_content)
assert dg._df is not None
assert dg._grid_settings[DG_COLUMNS] == {
'column_1': {'index': 0, 'safe_index': 0, 'title': 'Column 1', 'type': 'string'},
'column_2': {'index': 1, 'safe_index': 1, 'title': 'Column 2', 'type': 'string'}}
def test_grid_setting_is_kept_on_import(dg, excel_file_content):
grid_settings = {DG_FILTER_INPUT: False}
dg = DataGrid(grid_settings=grid_settings)
dg.import_file("file_name", excel_file_content)
assert dg._grid_settings[DG_FILTER_INPUT] is False
def test_i_can_get_filter_content(dg2):
res = dg2.mk_filter_popup_content('name')
checkboxes = extract_popup_content(res, filter_input=False)
assert checkboxes == OrderedDict({
'Alice': False,
'Bob': False,
'Charlie': False,
'David': False})
dg2.filter('name', ['Bob', 'Charlie'])
res = dg2.mk_filter_popup_content('name')
checkboxes = extract_popup_content(res, filter_input=False)
assert checkboxes == OrderedDict({
'Alice': False,
'Bob': True,
'Charlie': True,
'David': False})
def test_i_can_get_filter_content_when_multiple_filter_set(dg2):
dg2.filter('name', ['Bob', 'Charlie', 'David'])
res = dg2.mk_filter_popup_content('age')
checkboxes = extract_popup_content(res, filter_input=False)
assert checkboxes == OrderedDict({
'25': False,
'30': False,
'35': False,
})
# second filter
dg2.filter('age', ["25", "30"])
res = dg2.mk_filter_popup_content('city')
checkboxes = extract_popup_content(res, filter_input=False)
assert checkboxes == OrderedDict({
'Los Angeles': False,
'Chicago': False,
})
# previous filters are remembered
res = dg2.mk_filter_popup_content('name')
checkboxes = extract_popup_content(res, filter_input=False)
assert checkboxes == OrderedDict({
'Alice': False,
'Bob': True,
'Charlie': True,
'David': True,
})
res = dg2.mk_filter_popup_content('age')
checkboxes = extract_popup_content(res, filter_input=False)
assert checkboxes == OrderedDict({
'25': True,
'30': True,
'35': False,
})
res = dg2.mk_filter_popup_content('city')
checkboxes = extract_popup_content(res, filter_input=False)
assert checkboxes == OrderedDict({
'Los Angeles': False,
'Chicago': False,
})
def test_i_can_import_data(dg):
new_data = pd.DataFrame({
'City': ['New York', 'Los Angeles'],
'Population': [8_331_237, 3_965_117]
})
dg.import_dataframe(new_data)
table = dg.mk_table()
content = extract_table_values(table)
assert content == OrderedDict({'City': ['New York', 'Los Angeles'],
'Population': ['8331237', '3965117']})
assert dg._grid_settings[DG_COLUMNS] == {
'city': {'index': 0, 'safe_index': 0, 'title': 'City', 'type': 'string'},
'population': {'index': 1, 'safe_index': 1, 'title': 'Population', 'type': 'number'}
}
def test_i_can_import_data_when_none():
dg = DataGrid()
dg.import_dataframe(None)
assert dg._df is None
assert dg._grid_settings[DG_COLUMNS] == {}
def test_i_can_import_data_when_empty():
dg = DataGrid()
dg.import_dataframe(pd.DataFrame())
assert dg._df.empty
assert dg._grid_settings[DG_COLUMNS] == {}
def test_grid_setting_is_kept_when_no_data():
grid_settings = {
DG_COLUMNS: {
"name": {
"index": 0,
"title": "Name",
"type": "string",
},
}
}
dg = DataGrid(grid_settings=grid_settings) # df is None
assert dg._grid_settings[DG_COLUMNS] == grid_settings[DG_COLUMNS]
def test_grid_settings_is_kept_after_load_data(dg):
grid_settings = {
DG_COLUMNS: {
"name": {
"index": 0,
"title": "Name",
"type": "string",
},
}
}
df = pd.DataFrame({"name": ["Alice", "Bob"],
"age": [20, 25]})
dg = DataGrid(grid_settings=grid_settings)
dg.import_dataframe(df, reset=False)
assert dg._grid_settings[DG_COLUMNS] == {
"name": {
"index": 0,
'safe_index': 0,
"title": "Name",
"type": "string",
}
}
def test_i_can_apply_style_depending_on_column_type():
grid_settings = {
DG_COLUMNS: {
"name": {
"index": 5,
"title": "Name",
"type": DG_DATATYPE_STRING,
},
"age": {
"index": 10,
"title": "Age",
"type": DG_DATATYPE_NUMBER,
},
"male": {
"index": 15,
"title": "Male",
"type": DG_DATATYPE_BOOL,
},
}
}
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25],
'male': [True, False],
})
dg = DataGrid(df, grid_settings=grid_settings)
expected = Div(
Div(
Div(Div("Alice")),
Div(Div("20"), style="text-align: right;"),
Div(Div(), style="text-align: center; display: flex; justify-content: center;"),
),
Div(
Div(Div("Bob")),
Div(Div("25"), style="text-align: right;"),
Div(Div(), style="text-align: center; display: flex; justify-content: center;"),
)
)
actual = dg.mk_table_body()
assert matches(actual, expected)
def test_by_default_table_is_ready_only(dg):
# by default the grid is readonly
# The cell will be selected, but no input
for cell in [(0, 0), (1, 0), (0, 1), (1, 1)]:
res = dg.try_enter_edit_mode(*cell)
assert res is None
def test_i_can_try_edit_with_invalid_values(dg):
assert dg.try_enter_edit_mode(-1, 0) is None
assert dg.try_enter_edit_mode(0, 3) is None
dg.get_grid_settings()[DG_READ_ONLY] = False
assert dg.try_enter_edit_mode(-1, 0) is None
assert dg.try_enter_edit_mode(0, 3) is None
def test_i_can_try_edit_a_text_cell(dg2, grid_id):
# make the grid editable
dg2.get_grid_settings()[DG_READ_ONLY] = False
res = dg2.try_enter_edit_mode(0, 1)
expected = Div(
Input(name="new_value", value="Bob", cls="dt-cell-content"),
cls="dt-cell-is-editing"
)
assert matches(res, expected)
def test_i_can_edit_a_number_cell(dg2, grid_id):
# make the grid editable
dg2.get_grid_settings()[DG_READ_ONLY] = False
res = dg2.try_enter_edit_mode(1, 1)
expected = Div(
Input(name="new_value", value=25, cls="dt-cell-content"),
cls="dt-cell-is-editing"
)
assert matches(res, expected)
def test_i_can_edit_a_bool_cell(dg2, grid_id):
# 'Student': [True, False, True, False],
# make the grid editable
dg2.get_grid_settings()[DG_READ_ONLY] = False
res = dg2.try_enter_edit_mode(3, 1)
assert res is None # bool cell never enters in edit mode
def test_i_can_manage_click_when_read_only(dg, grid_id):
# The cell is selected, but no input
dg.get_grid_settings()[DG_READ_ONLY] = True
res = dg.manage_cell_click(0, 0)
input_elt = search_elements_by_name(res, "input")
assert input_elt is None
under_edition = search_elements_by_name(res, None, {"class": "dt-cell-is-editing"})
assert under_edition is None
def test_i_can_manage_click_for_text_when_not_read_only(dg, grid_id):
# the cell is selected and the input element is returned
dg.get_grid_settings()[DG_READ_ONLY] = False
res = dg.manage_cell_click(0, 0)
under_edition = search_elements_by_name(res, None, {"class": "dt-cell-is-editing"})
assert under_edition is not None
input_elt = search_elements_by_name(res, "input")
assert input_elt is not None
def test_i_can_manage_click_for_number_when_not_read_only(dg, grid_id):
# the cell is selected and the input element is returned
dg.get_grid_settings()[DG_READ_ONLY] = False
res = dg.manage_cell_click(1, 0)
under_edition = search_elements_by_name(res, None, {"class": "dt-cell-is-editing"})
assert under_edition is not None
input_elt = search_elements_by_name(res, "input")
assert input_elt is not None
def test_i_can_manage_click_for_bool_when_not_read_only(dg2, grid_id):
# the value is changed, the cell is selected, no input is returned
dg2.get_grid_settings()[DG_READ_ONLY] = False
old_value = dg2.get_table_value(3, 0)
res = dg2.manage_cell_click(3, 0)
under_edition = search_elements_by_name(res, None, {"class": "dt-cell-is-editing"})
assert under_edition is None
input_elt = search_elements_by_name(res, "input")
assert input_elt is None
assert dg2.get_table_value(3, 0) == (not old_value)
def test_i_can_manage_multiple_click_when_not_read_only(dg, grid_id):
dg.get_grid_settings()[DG_READ_ONLY] = False
dg.manage_cell_click(0, 0)
res = dg.manage_cell_click(1, 0)
expected = [Div(Div("Alice"), hx_swap_oob="innerHTML", id=f"tc_{grid_id}-0-0"), # reset the previous edition
Div(Div(cls="dt-cell-is-editing"), id=f"tc_{grid_id}-1-0"), # new input
Div(id=f"tsm_{grid_id}", )]
assert matches(res, expected)
def test_i_can_edit_one_when_the_column_is_editable():
grid_settings = {DG_COLUMNS: {
"name": {
"index": 0,
"title": "Name",
"type": "string",
},
"age": {
"index": 1,
"title": "Age",
"type": "number",
DG_READ_ONLY: False,
}}}
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25]
})
dg = DataGrid(df, grid_settings=grid_settings)
# I cannot modify a column that is readonly
res = dg.try_enter_edit_mode(0, 0)
assert res is None
# I can modify when it's not readonly
res = dg.try_enter_edit_mode(1, 0)
expected = Div(
Input(name="new_value", value=20, cls="dt-cell-content"),
cls="dt-cell-is-editing"
)
assert matches(res, expected)
def test_i_cannot_edit_bool_when_the_column_is_not_editable(dg2, grid_id):
dg2.get_grid_settings()[DG_READ_ONLY] = True
old_value = dg2.get_table_value(3, 0)
dg2.manage_cell_click(3, 0)
assert dg2.get_table_value(3, 0) == old_value
@pytest.mark.parametrize("mode, expected", [
(None, DG_SELECTION_MODE_CELL),
(DG_SELECTION_MODE_CELL, DG_SELECTION_MODE_CELL),
(DG_SELECTION_MODE_COLUMN, DG_SELECTION_MODE_COLUMN),
(DG_SELECTION_MODE_ROW, DG_SELECTION_MODE_ROW),
])
def test_i_can_set_selection_mode(mode, expected):
grid_settings = {
DG_SELECTION_MODE: mode
}
dg = DataGrid(grid_settings=grid_settings)
assert dg.get_state().selection_mode == expected
@pytest.mark.parametrize("read_only", [True, False])
def test_i_can_manage_cell_selection_when_selection_mode_is_cell(dg, grid_id, read_only):
# The cell is selected, but no input
dg.get_state().selection_mode = DG_SELECTION_MODE_CELL
dg.get_grid_settings()[DG_READ_ONLY] = read_only
res = dg.manage_cell_click(0, 0)
assert get_selected(res) == [
("cell", 0, 0),
]
@pytest.mark.parametrize("read_only", [True, False])
def test_i_can_manage_cell_selection_when_selection_mode_is_row(dg, grid_id, read_only):
# The cell is selected, but no input
dg.get_grid_settings()[DG_READ_ONLY] = read_only
dg.get_state().selection_mode = DG_SELECTION_MODE_ROW
res = dg.manage_cell_click(0, 0)
assert get_selected(res) == [
("row", 0),
("cell", 0, 0),
]
@pytest.mark.parametrize("read_only", [True, False])
def test_i_can_manage_cell_selection_when_selection_mode_is_column(dg, grid_id, read_only):
# The cell is selected, but no input
dg.get_grid_settings()[DG_READ_ONLY] = read_only
dg.get_state().selection_mode = DG_SELECTION_MODE_COLUMN
res = dg.manage_cell_click(0, 0)
assert get_selected(res) == [
("column", "name"),
("cell", 0, 0),
]
def test_i_can_manage_multiple_cells_selection_using_control_key(dg, grid_id):
dg.manage_cell_click(0, 0)
res = dg.manage_cell_click(1, 0, "ctrl-")
selected = get_selected(res)
assert selected == [("cell", 1, 0), ('cellx', 0, 0), ('cellx', 1, 0)]
assert dg.get_state().extra_selected == [('cell', 'tc_datagrid-testing_grid_id-0-0'),
('cell', 'tc_datagrid-testing_grid_id-1-0')]
def test_i_can_manage_multiple_cells_selection_in_colum_mode_using_control_key(dg, grid_id):
dg.get_state().selection_mode = DG_SELECTION_MODE_COLUMN
dg.manage_cell_click(0, 0)
res = dg.manage_cell_click(1, 0, "ctrl-")
selected = get_selected(res)
assert selected == [('column', "age"), ('cell', 1, 0), ('column', "name")]
assert dg.get_state().extra_selected == [('column', "name"), ('column', "age")]
def test_i_can_manage_multiple_cells_selection_in_row_mode_using_control_key(dg, grid_id):
dg.get_state().selection_mode = DG_SELECTION_MODE_ROW
dg.manage_cell_click(0, 0)
res = dg.manage_cell_click(0, 1, "ctrl-")
selected = get_selected(res)
assert selected == [('row', 1), ('cell', 0, 1), ('row', 0)]
assert dg.get_state().extra_selected == [('row', 'tr_datagrid-testing_grid_id-0'),
('row', 'tr_datagrid-testing_grid_id-1')]
def test_i_can_manage_multiple_cells_selection_using_shift_key(dg2, grid_id):
dg2.manage_cell_click(1, 0)
res = dg2.manage_cell_click(2, 2, "shift-")
selected = get_selected(res)
assert selected == [('cell', 2, 2),
('cellx', 1, 0),
('cellx', 1, 1),
('cellx', 1, 2),
('cellx', 2, 0),
('cellx', 2, 1),
('cellx', 2, 2)]
assert dg2.get_state().extra_selected == [('cell', 'tc_datagrid-testing_grid_id-1-0'),
('cell', 'tc_datagrid-testing_grid_id-1-1'),
('cell', 'tc_datagrid-testing_grid_id-1-2'),
('cell', 'tc_datagrid-testing_grid_id-2-0'),
('cell', 'tc_datagrid-testing_grid_id-2-1'),
('cell', 'tc_datagrid-testing_grid_id-2-2')]
def test_i_can_manage_multiple_cells_selection_in_colum_mode_using_shift_key(dg2, grid_id):
dg2.get_state().selection_mode = DG_SELECTION_MODE_COLUMN
dg2.manage_cell_click(1, 0)
res = dg2.manage_cell_click(3, 3, "shift-")
selected = get_selected(res)
assert selected == [('column', 'student'), ('cell', 3, 3), ('column', "age"), ('column', "city")]
assert dg2.get_state().extra_selected == [('column', "age"), ('column', "city"), ('column', "student")]
def test_i_can_manage_multiple_cells_selection_in_row_mode_using_shift_key(dg2, grid_id):
dg2.get_state().selection_mode = DG_SELECTION_MODE_ROW
dg2.manage_cell_click(1, 0)
res = dg2.manage_cell_click(3, 3, "shift-")
selected = get_selected(res)
assert selected == [('row', 3), ('cell', 3, 3), ('row', 0), ('row', 1), ('row', 2)]
assert dg2.get_state().extra_selected == [('row', 'tr_datagrid-testing_grid_id-0'),
('row', 'tr_datagrid-testing_grid_id-1'),
('row', 'tr_datagrid-testing_grid_id-2'),
('row', 'tr_datagrid-testing_grid_id-3')]
def test_i_can_manage_multiple_cells_selection_using_shift_key_when_the_table_is_sorted(dg2, grid_id):
dg2.sort("city")
# name age city student date
# 2 Charlie 30 Chicago True 2007-01-01
# 3 David 35 Houston False 2000-12-31
# 1 Bob 25 Los Angeles False 2022-10-01
# 0 Alice 20 New York True 2020-01-10
# I click on age=35 (pos=1,1) then on city='Los Angeles' (pos=2,2)
dg2.manage_cell_click(1, 3) # age=35 position is 1,1
res = dg2.manage_cell_click(2, 1, "shift-") # city=los angeles is position 2,2
selected = get_selected(res)
assert selected == [('cell', 2, 1),
('cellx', 1, 3),
('cellx', 1, 1),
('cellx', 2, 3),
('cellx', 2, 1)]
assert dg2.get_state().extra_selected == [('cell', 'tc_datagrid-testing_grid_id-1-3'),
('cell', 'tc_datagrid-testing_grid_id-1-1'),
('cell', 'tc_datagrid-testing_grid_id-2-3'),
('cell', 'tc_datagrid-testing_grid_id-2-1')]
def test_i_can_select_column(dg, grid_id):
res = dg.manage_column_click(1, '')
assert get_selected(res) == [
("column", "age"),
]
# without any modifier, the selection is replaced
res = dg.manage_column_click(0, '')
assert get_selected(res) == [
("column", "name"),
]
def test_i_can_select_multiple_columns_with_control_key(dg, grid_id):
dg.manage_column_click(1, '')
# I can add column using ctrl
res = dg.manage_column_click(0, "ctrl-")
assert get_selected(res) == [
("column", "age"),
("column", "name"),
]
# I can remove column using control
res = dg.manage_column_click(1, "ctrl-")
assert get_selected(res) == [
("column", "name"),
]
def test_i_can_select_multiple_columns_with_shift_key(dg2, grid_id):
res = dg2.manage_column_click(1, "shift-")
assert get_selected(res) == [
("column", "age"),
]
res = dg2.manage_column_click(3, "shift-")
assert get_selected(res) == [
("column", "age"),
("column", "city"),
("column", "student"),
]
res = dg2.manage_column_click(2, "shift-")
assert get_selected(res) == [
("column", "age"),
("column", "city"),
]
res = dg2.manage_column_click(0, "shift-")
assert get_selected(res) == [
("column", "name"),
("column", "age"),
]
def test_i_can_press_enter_when_readonly(dg, grid_id):
dg.get_grid_settings()[DG_READ_ONLY] = True
dg.manage_cell_click(0, 0) # select the top left
res = dg.manage_key_pressed("Enter", None)
expected = [Div(Div(selection_type="cell", element_id=f"tc_{grid_id}-0-1"),
id=f"tsm_{grid_id}")]
assert matches(res, expected)
res = dg.manage_key_pressed("Enter", None)
expected = [Div(Div(selection_type="cell", element_id=f"tc_{grid_id}-0-0"),
id=f"tsm_{grid_id}", )]
assert matches(res, expected)
def test_i_can_press_tab_when_readonly(dg, grid_id):
dg.get_grid_settings()[DG_READ_ONLY] = True
dg.manage_cell_click(0, 0) # select the top left
res = dg.manage_key_pressed("Tab", None)
expected = [Div(Div(selection_type="cell", element_id=f"tc_{grid_id}-1-0"),
id=f"tsm_{grid_id}")]
assert matches(res, expected)
res = dg.manage_key_pressed("Tab", None)
expected = [Div(Div(selection_type="cell", element_id=f"tc_{grid_id}-0-1"),
id=f"tsm_{grid_id}")]
assert matches(res, expected)
def test_i_can_press_enter_when_not_readonly(dg, grid_id):
dg.get_grid_settings()[DG_READ_ONLY] = False
dg.manage_cell_click(0, 0) # select the top left
res = dg.manage_key_pressed("Enter", "Alice")
expected = [
Div(
Div("Alice"),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-0-0"),
Div(
Div(Input()),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-0-1"),
Div(
Div(selection_type="cell", element_id=f"tc_{grid_id}-0-1"),
id=f"tsm_{grid_id}",
)]
assert matches(res, expected)
res = dg.manage_key_pressed("Enter", "Bob")
expected = [
Div(
Div("Bob"),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-0-1"),
Div(
Div(Input()),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-0-0"),
Div(
Div(selection_type="cell", element_id=f"tc_{grid_id}-0-0"),
id=f"tsm_{grid_id}",
)]
assert matches(res, expected)
def test_i_can_press_enter_on_a_bool_editable_cell(dg2, grid_id):
dg2.get_grid_settings()[DG_READ_ONLY] = False
dg2.get_state().selected = (3, 0)
res = dg2.manage_key_pressed("Enter", None)
expected = [
Div(
Div(
NotStr('<svg name="CheckboxUnchecked'),
cls="icon-bool",
),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-3-0"),
Div(
Div(selection_type="cell", element_id=f"tc_{grid_id}-3-1"),
id=f"tsm_{grid_id}",
)]
assert matches(res, expected)
# I can do it again
res = dg2.manage_key_pressed("Enter", None)
expected = [
Div(
Div(
NotStr('<svg name="CheckboxChecked'),
cls="icon-bool",
),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-3-1"),
Div(
Div(selection_type="cell", element_id=f"tc_{grid_id}-3-2"),
id=f"tsm_{grid_id}",
)]
assert matches(res, expected)
def test_i_can_press_tab_when_not_readonly(dg, grid_id):
dg.get_grid_settings()[DG_READ_ONLY] = False
dg.manage_cell_click(0, 0) # select the top left
res = dg.manage_key_pressed("Tab", "Alice")
expected = [
Div(
Div("Alice"),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-0-0"),
Div(
Div(Input()),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-1-0"),
Div(
Div(selection_type="cell", element_id=f"tc_{grid_id}-1-0"),
id=f"tsm_{grid_id}")
]
assert matches(res, expected)
res = dg.manage_key_pressed("Tab", "20")
expected = [
Div(
Div("20"),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-1-0",
),
Div(
Div(Input()),
hx_swap_oob="innerHTML",
id=f"tc_{grid_id}-0-1",
),
Div(
Div(selection_type="cell", element_id=f"tc_{grid_id}-0-1"),
id=f"tsm_{grid_id}")
]
assert matches(res, expected)
def test_i_can_escape_when_not_in_edition_nor_selection(dg):
dg.get_state().under_edition = None
result = dg.escape()
assert result is None
def test_i_can_manage_selection_on_escape_when_cell_selection_mode(dg, grid_id):
# The cell is selected, but no input
dg.get_grid_settings()[DG_READ_ONLY] = False
dg.get_state().selection_mode = DG_SELECTION_MODE_CELL
res = dg.manage_cell_click(0, 0) # entering edit mode
# sanity check that the input element is here, and it's selected
assert search_elements_by_name(res, "input") is not None
assert get_selected(res) == [("cell", 0, 0)]
res = dg.manage_key_pressed("Escape", None) # first escape : input is gone, but selection is still there
assert search_elements_by_name(res, "input") is None
assert get_selected(res) == [("cell", 0, 0)]
res = dg.manage_key_pressed("Escape", None) # second escape : input is gone, and no more selection
assert search_elements_by_name(res, "input") is None
assert get_selected(res) == []
def test_i_can_navigate_with_arrow_keys(dg, grid_id):
res = dg.manage_key_pressed("ArrowRight", None)
assert get_selected(res) == [("cell", 0, 0)]
# I cannot go left
res = dg.manage_key_pressed("ArrowLeft", None)
assert get_selected(res) == [("cell", 0, 0)]
# I cannot go up
res = dg.manage_key_pressed("ArrowUp", None)
assert get_selected(res) == [("cell", 0, 0)]
res = dg.manage_key_pressed("ArrowRight", None)
assert get_selected(res) == [("cell", 1, 0)]
# I cannot move right
res = dg.manage_key_pressed("ArrowRight", None)
assert get_selected(res) == [("cell", 1, 0)]
res = dg.manage_key_pressed("ArrowDown", None)
assert get_selected(res) == [("cell", 1, 1)]
# I cannot move down
res = dg.manage_key_pressed("ArrowDown", None)
assert get_selected(res) == [("cell", 1, 1)]
res = dg.manage_key_pressed("ArrowLeft", None)
assert get_selected(res) == [("cell", 0, 1)]
# I cannot move down
res = dg.manage_key_pressed("ArrowLeft", None)
assert get_selected(res) == [("cell", 0, 1)]
res = dg.manage_key_pressed("ArrowUp", None)
assert get_selected(res) == [("cell", 0, 0)]
def test_i_can_navigate_with_arrow_keys_when_the_grid_is_sorted(dg, grid_id):
dg.sort("name") # asc sort, so nothing is changed
dg.sort("name") #
res = dg.manage_key_pressed("ArrowRight", None)
assert get_selected(res) == [("cell", 0, 1)]
res = dg.manage_key_pressed("ArrowDown", None)
assert get_selected(res) == [("cell", 0, 0)]
# I cannot move down
res = dg.manage_key_pressed("ArrowDown", None)
assert get_selected(res) == [("cell", 0, 0)]
def test_i_can_navigate_with_arrow_keys_when_the_grid_is_filtered(dg, grid_id):
dg.filter(FILTER_INPUT_CID, 'Ali') # Apply filter to grid
dg.get_state().selected = (0, 0)
res = dg.manage_key_pressed("ArrowDow", None)
expected = [
Div(
Div(selection_type="cell", element_id=f"tc_{grid_id}-0-0"),
id=f"tsm_{grid_id}",
)]
assert matches(res, expected)
def test_i_can_navigate_with_arrow_keys_when_columns_are_hidden(dg2, grid_id):
dg2.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["age", "city", "student"], "true", "false")
dg2.get_state().selected = dg2.navigate(None)
assert dg2.get_state().selected == (0, 0)
dg2.get_state().selected = dg2.navigate("ArrowRight")
assert dg2.get_state().selected == (4, 0)
# it's safe to continue in this direction
dg2.get_state().selected = dg2.navigate("ArrowRight")
assert dg2.get_state().selected == (4, 0)
# I can go back
dg2.get_state().selected = dg2.navigate("ArrowLeft")
assert dg2.get_state().selected == (0, 0)
def test_i_cannot_navigate_when_all_columns_are_hidden(dg2, grid_id):
dg2.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["name", "age", "city", "student", "date"], "true", "false")
assert dg2.navigate(None) is None
def test_manage_grid_settings_i_can_manage_visible(dg, grid_id):
# sanity check
assert dg.get_grid_settings()[DG_COLUMNS] == {'age': {'index': 1,
'safe_index': 1,
'title': 'Age',
'type': 'number'},
'name': {'index': 0,
'safe_index': 0,
'title': 'Name',
'type': 'string'}}
# I can hide a column
dg.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["name"], "true", "false")
new_state = [(c.col_id, c.visible) for c in dg.get_state().columns]
assert new_state == [("name", False), ('age', True)]
# I can put it back
dg.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["name"], "false", "true")
new_state = [(c.col_id, c.visible) for c in dg.get_state().columns]
assert new_state == [("name", True), ('age', True)]
# I can remove multiple columns
dg.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["name", "age"], "true", "false")
new_state = [(c.col_id, c.visible) for c in dg.get_state().columns]
assert new_state == [("name", False), ('age', False)]
def test_manage_grid_settings_extra_selected_is_reset_on_visible_change(dg, grid_id):
dg.get_state().extra_selected = [(DG_SELECTION_MODE_COLUMN, 0)]
dg.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["name"], "something", "something")
assert dg.get_state().extra_selected == []
def test_selection_and_under_edition_is_reset_when_the_same_column_is_hidden(dg, grid_id):
dg.get_state().selected = (0, 0)
dg.get_state().under_edition = (0, 0)
dg.get_state().previous_under_edition = (0, 0)
dg.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["name"], "true", "False")
assert dg.get_state().selected is None
assert dg.get_state().under_edition is None
assert dg.get_state().previous_under_edition is None
def test_selection_and_under_edition_is_not_reset_when_other_column_is_hidden(dg, grid_id):
dg.get_state().selected = (0, 0)
dg.get_state().under_edition = (0, 0)
dg.get_state().previous_under_edition = (0, 0)
dg.manage_grid_state(VISIBLE_KEY, ["age"], "true", "False")
assert dg.get_state().selected == (0, 0)
assert dg.get_state().under_edition == (0, 0)
assert dg.get_state().previous_under_edition == (0, 0)
def test_context_menu_i_can_hide_a_selected_column(dg, grid_id):
res = dg.manage_column_click(0, '')
context_menus = get_context_menu(res)
assert context_menus == [{
"hx-post": "/on_state_changed",
"data_tooltip": "Hide column 'Name'",
"attribute": DG_STATE_COLUMN_VISIBILITY,
"col_ids": "name",
"old_value": True,
"new_value": False,
}]
assert dg.get_state().selected is None
def test_context_menu_i_can_hide_selected_columns(dg, grid_id):
dg.manage_column_click(0, '')
res = dg.manage_column_click(1, 'ctrl')
context_menus = get_context_menu(res)
assert context_menus == [{
"hx-post": "/on_state_changed",
"data_tooltip": "Hide columns 'Name', 'Age'",
"attribute": DG_STATE_COLUMN_VISIBILITY,
"col_ids": "name,age",
"old_value": True,
"new_value": False,
}]
assert dg.get_state().selected is None
def test_context_menu_i_can_show_a_hidden_column(dg2, grid_id):
# This scenario is misnamed, you cannot have context menu visible by clicking a hidden column
dg2.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["age"], "true", "false")
res = dg2.manage_column_click(1, '')
context_menus = get_context_menu(res)
assert context_menus == [{'hx-post': '/on_state_changed',
'data_tooltip': "Show column 'Age'",
'attribute': DG_STATE_COLUMN_VISIBILITY,
'col_ids': 'age',
'old_value': False,
'new_value': True}]
def test_context_menu_i_can_hide_and_show_columns(dg2, grid_id):
# More realistic scenario
dg2.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["age", "city", "student"], "true", "false")
dg2.manage_column_click(0, '')
res = dg2.manage_column_click(4, 'shift') # select all the columns
context_menus = get_context_menu(res)
assert context_menus == [{'hx-post': '/on_state_changed',
'data_tooltip': "Show columns 'Age', 'City', 'Student'",
'attribute': DG_STATE_COLUMN_VISIBILITY,
'col_ids': 'age,city,student',
'old_value': False,
'new_value': True},
{'hx-post': '/on_state_changed',
'data_tooltip': "Hide columns 'Name', 'Date'",
'attribute': DG_STATE_COLUMN_VISIBILITY,
'col_ids': 'name,date',
'old_value': True,
'new_value': False}]
def test_i_an_undo_redo(dg):
dg.set_table_value(0, 0, "New name 1")
dg.set_table_value(0, 1, "New name 2")
dg.set_table_value(1, 0, 99)
assert to_array(dg._df) == [['New name 1', 99], ['New name 2', 25]]
dg.history_redo() # nothing happen
assert to_array(dg._df) == [['New name 1', 99], ['New name 2', 25]]
dg.history_undo()
assert to_array(dg._df) == [['New name 1', 20], ['New name 2', 25]]
dg.history_undo()
assert to_array(dg._df) == [['New name 1', 20], ['Bob', 25]]
dg.history_undo()
assert to_array(dg._df) == [['Alice', 20], ['Bob', 25]]
dg.history_undo() # nothing happen
assert to_array(dg._df) == [['Alice', 20], ['Bob', 25]]
dg.history_redo()
assert to_array(dg._df) == [['New name 1', 20], ['Bob', 25]]
dg.history_redo()
assert to_array(dg._df) == [['New name 1', 20], ['New name 2', 25]]
dg.history_redo()
assert to_array(dg._df) == [['New name 1', 99], ['New name 2', 25]]
def test_i_can_reset_the_history(dg):
dg.set_table_value(0, 0, "New name 1")
dg.set_table_value(0, 1, "New name 2")
dg.set_table_value(1, 0, 99)
dg.history_undo()
dg.history_undo()
assert to_array(dg._df) == [['New name 1', 20], ['Bob', 25]]
dg.set_table_value(1, 1, 10)
dg.history_redo() # nothing happen (history is reset with the new value)
assert to_array(dg._df) == [['New name 1', 20], ['Bob', 10]]
dg.history_undo()
assert to_array(dg._df) == [['New name 1', 20], ['Bob', 25]]
dg.history_redo()
assert to_array(dg._df) == [['New name 1', 20], ['Bob', 10]]
def test_i_can_compute_settings_when_no_agg_func():
grid_settings = {
DG_COLUMNS: {
'A': {'index': 0},
'B': {'index': 1},
}
}
result = DataGrid.get_computed_settings(grid_settings)
expected = {'max_agg_func': 1, 'agg_func_conf': {'A': [None], 'B': [None]}}
assert dataclasses.asdict(result) == expected
def test_i_can_compute_settings():
grid_settings = {
DG_COLUMNS: {
'C': {'agg_func': 'max', 'index': 2},
'D': {'agg_func_2': 'mean', 'index': 3},
'A': {'agg_func': 'sum', 'agg_func_2': 'max', 'index': 0},
'B': {'agg_func_1': 'min', 'agg_func_2': 'mean', 'index': 1},
}
}
result = DataGrid.get_computed_settings(grid_settings)
expected = {'max_agg_func': 2,
'agg_func_conf': {'A': ['sum', 'max'],
'B': ['min', 'mean'],
'C': ['max', None],
'D': [None, 'mean']}}
assert dataclasses.asdict(result) == expected
@pytest.mark.parametrize("agg_func,expected_value", [
(DG_AGGREGATE_MAX, 25),
(DG_AGGREGATE_MIN, 20),
(DG_AGGREGATE_COUNT, 2),
(DG_AGGREGATE_SUM, 45),
(DG_AGGREGATE_MEAN, 22.5),
])
def test_i_can_aggregate_function(agg_func, expected_value):
df = pd.DataFrame({
'Name': ['Alice', 'Bob'],
'Age': [20, 25]
})
grid_settings = {
DG_COLUMNS: {
"name": {
"index": 0,
"title": "Name",
},
"age": {
"index": 1,
"title": "Age",
"agg_func": agg_func,
}}, }
dg = DataGrid(df, id=TEST_GRID_ID, grid_settings=grid_settings)
dg_grid_id = dg.get_grid_id()
footer = dg.mk_table_footer()
expected = Div(
Div(
Div(
Div(None),
data_col="name",
# data_row="-1"
),
Div(
Div(expected_value),
data_col="age",
# data_row="-1"
),
cls="dt-row dt-footer"
),
id=f"tf_{dg_grid_id}",
)
assert matches(footer, expected)
@pytest.mark.parametrize("agg_func,expected_value", [
(DG_AGGREGATE_FILTERED_MAX, 30),
(DG_AGGREGATE_FILTERED_MIN, 20),
(DG_AGGREGATE_FILTERED_COUNT, 3),
(DG_AGGREGATE_FILTERED_SUM, 75),
(DG_AGGREGATE_FILTERED_MEAN, 25.0),
])
def test_i_can_test_filtered_aggregate(agg_func, expected_value):
df = pd.DataFrame({
'Name': ['Alice', 'Bob', 'Charlie', 'David'],
'Age': [20, 25, 30, 35],
})
grid_settings = {
DG_COLUMNS: {
"name": {
"index": 0,
"title": "Name",
},
"age": {
"index": 1,
"title": "Age",
"agg_func": agg_func,
}}, }
dg = DataGrid(df, id=TEST_GRID_ID, grid_settings=grid_settings)
dg.filter("name", ["Alice", "Bob", "Charlie"])
dg_grid_id = dg.get_grid_id()
footer = dg.mk_table_footer()
expected = Div(
Div(
Div(
Div(None),
data_col="name",
# data_row="-1"
),
Div(
Div(expected_value),
data_col="age",
# data_row="-1"
),
cls="dt-row dt-footer"
),
id=f"tf_{dg_grid_id}",
)
assert matches(footer, expected)
def test_i_can_move_columns(dg, grid_id):
dg.manage_grid_state(MOVE_KEY, ["name"], "0", "1")
res = dg.mk_table()
expected = Div(
Div(
Div(Div(Div("Age")), data_col="age"),
Div(Div(Div("Name")), data_col="name"),
cls="dt-row dt-header",
id=f"th_{grid_id}"),
Div(
Div(
Div(Div("20"), data_col="age"),
Div(Div("Alice"), data_col="name"),
),
Div(
Div(Div("25"), data_col="age"),
Div(Div("Bob"), data_col="name"),
),
id=f"tb_{grid_id}",
),
Div(
Div(
Div(Div(), data_col="age"),
Div(Div(), data_col="name"),
),
id=f"tf_{grid_id}")
)
table = search_elements_by_name(res, None, {"class": "dt-inner-table"})[0]
assert matches(table, expected)
def test_i_can_move_columns_multiple_times(dg2, grid_id):
dg2.manage_grid_state(MOVE_KEY, ["name"], "0", "1")
columns_ids = [c.col_id for c in dg2.get_state().columns]
assert columns_ids == ["age", "name", "city", "student", "date"]
dg2.manage_grid_state(MOVE_KEY, ["age"], "0", "1")
columns_ids = [c.col_id for c in dg2.get_state().columns]
assert columns_ids == ["name", "age", "city", "student", "date"]
dg2.manage_grid_state(MOVE_KEY, ["city"], "2", "4")
columns_ids = [c.col_id for c in dg2.get_state().columns]
assert columns_ids == ["name", "age", "student", "date", "city"]
def test_order_is_kept_when_column_is_hidden_or_shown(dg2, grid_id):
dg2.manage_grid_state(MOVE_KEY, ["name"], "0", "2")
dg2.manage_grid_state(VISIBLE_KEY, ["age"], "true", "false")
columns_ids = [c.col_id for c in dg2.get_state().columns]
assert columns_ids == ["age", "city", "name", "student", "date"]
dg2.manage_grid_state(VISIBLE_KEY, ["age"], "false", "true")
columns_ids = [c.col_id for c in dg2.get_state().columns]
assert columns_ids == ["age", "city", "name", "student", "date"]
def test_the_correct_column_is_select_when_column_order_is_changed(dg2, grid_id):
dg2.get_state().selection_mode = DG_SELECTION_MODE_COLUMN
dg2.manage_grid_state(MOVE_KEY, ["name"], "0", "1")
res = dg2.manage_cell_click(1, 0)
selected = get_selected(res)
assert selected == [('column', 'age'), ('cell', 1, 0)]
def test_filter_only_apply_to_visible_columns(dg, grid_id):
dg.manage_grid_state(DG_STATE_COLUMN_VISIBILITY, ["age"], "true", "false")
dg.filter("age", ["20"])
res = dg.mk_table_body()
values = extract_table_values(res, header=False)
expected = [
["Alice"],
] # since "age" is not visible, but the filter must be kept (as we do with the sort)
assert values == expected
# same if I use the filter all
dg.filter(FILTER_INPUT_CID, "20")
res = dg.mk_table_body()
values = extract_table_values(res, header=False)
expected = [] # since "age" is not visible there no match
assert values == expected
def test_i_can_render_rows_indexes(dg, grid_id):
dg.get_grid_settings()[DG_ROWS_INDEXES] = True
res = dg.mk_table()
table = search_elements_by_name(res, None, {"class": "dt-inner-table"})[0]
expected = Div(
Div(
Div(cls="dt-row-index-cell", data_col="-1"),
Div(Div(Div("Name")), data_col="name"),
Div(Div(Div("Age")), data_col="age"),
id=f"th_{grid_id}"),
Div(
Div(
Div(Div("0"), cls="dt-row-index-cell", data_col="-1"),
Div(Div("Alice"), data_col="name"),
Div(Div("20"), data_col="age"),
),
Div(
Div(Div("1"), data_col="-1"),
Div(Div("Bob"), data_col="name"),
Div(Div("25"), data_col="age"),
),
id=f"tb_{grid_id}",
),
Div(
Div(
Div(cls="dt-row-index-cell", data_col="-1"),
Div(Div(), data_col="name"),
Div(Div(), data_col="age"),
),
id=f"tf_{grid_id}")
)
assert matches(table, expected)
def test_i_can_hide_row(dg, grid_id):
dg.manage_grid_state(DG_STATE_ROW_VISIBILITY, ["0"], "true", "false")
res = dg.mk_table_body()
values = extract_table_values(res, header=False)
expected = [['Bob', '25']]
assert values == expected
def test_i_can_select_a_row(dg, grid_id):
res = dg.manage_row_click(0, '')
selected = get_selected(res)
assert selected == [('row', 0)]
context_menus = get_context_menu(res)
assert context_menus == [{
"hx-post": "/on_state_changed",
"data_tooltip": "Hide row 0",
"attribute": DG_STATE_ROW_VISIBILITY,
"col_ids": "0",
"old_value": True,
"new_value": False,
}]
def test_i_can_select_and_unselected_rows_using_control_key(dg, grid_id):
res = dg.manage_row_click(0, 'ctrl')
selected = get_selected(res)
assert selected == [('row', 0)]
context_menus = get_context_menu(res)
assert context_menus == [{
"hx-post": "/on_state_changed",
"data_tooltip": "Hide row 0",
"attribute": DG_STATE_ROW_VISIBILITY,
"col_ids": "0",
"old_value": True,
"new_value": False,
}]
# If I click again the row is no longer selected
res = dg.manage_row_click(0, 'ctrl-')
selected = get_selected(res)
assert selected == []
context_menus = get_context_menu(res)
assert context_menus == []
def test_i_can_select_multiple_rows_using_control_key(dg, grid_id):
dg.manage_row_click(1, '')
res = dg.manage_row_click(0, 'ctrl-')
selected = get_selected(res)
assert selected == [('row', 1), ('row', 0)]
context_menus = get_context_menu(res)
assert context_menus == [{
"hx-post": "/on_state_changed",
"data_tooltip": "Hide rows 0, 1", # the order is always ascending
"attribute": DG_STATE_ROW_VISIBILITY,
"col_ids": "0, 1",
"old_value": True,
"new_value": False,
}]
def test_i_can_select_multiple_rows_using_shift_key(dg2, grid_id):
res = dg2.manage_row_click(3, 'shift-')
selected = get_selected(res)
assert selected == [('row', 3)]
res = dg2.manage_row_click(0, 'shift-')
selected = get_selected(res)
assert selected == [('row', 0), ('row', 1), ('row', 2), ('row', 3)]
context_menus = get_context_menu(res)
assert context_menus == [{
"hx-post": "/on_state_changed",
"data_tooltip": "Hide rows 0, 1, 2, 3", # the order is always ascending
"attribute": DG_STATE_ROW_VISIBILITY,
"col_ids": "0, 1, 2, 3",
"old_value": True,
"new_value": False,
}]
def test_hidden_rows_can_be_made_visible_using_shift_key(dg2, grid_id):
dg2.manage_grid_state(DG_STATE_ROW_VISIBILITY, ["2"], "true", "false")
dg2.manage_row_click(0, '')
res = dg2.manage_row_click(3, 'shift-')
context_menus = get_context_menu(res)
assert context_menus == [
{
"hx-post": "/on_state_changed",
"data_tooltip": "Hide rows 0, 1, 3", # the order is always ascending
"attribute": DG_STATE_ROW_VISIBILITY,
"col_ids": "0, 1, 3",
"old_value": True,
"new_value": False
},
{
"hx-post": "/on_state_changed",
"data_tooltip": "Show row 2",
"attribute": DG_STATE_ROW_VISIBILITY,
"col_ids": "2",
"old_value": False,
"new_value": True
}]