Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure Tabulator.selection is consisting across pagination and filtering #7058

Merged
merged 3 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions panel/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
import asyncio
import atexit
import datetime as dt
import os
import pathlib
import re
Expand Down Expand Up @@ -493,3 +494,37 @@ def eh(exception):
yield exceptions
finally:
config.exception_handler = old_eh


@pytest.fixture
def df_mixed():
df = pd.DataFrame({
'int': [1, 2, 3, 4],
'float': [3.14, 6.28, 9.42, -2.45],
'str': ['A', 'B', 'C', 'D'],
'bool': [True, True, True, False],
'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10), dt.date(2019, 1, 10)],
'datetime': [dt.datetime(2019, 1, 1, 10), dt.datetime(2020, 1, 1, 12), dt.datetime(2020, 1, 10, 13), dt.datetime(2020, 1, 15, 13)]
}, index=['idx0', 'idx1', 'idx2', 'idx3'])
return df

@pytest.fixture
def df_strings():
descr = [
'Under the Weather',
'Top Drawer',
'Happy as a Clam',
'Cut To The Chase',
'Knock Your Socks Off',
'A Cold Day in Hell',
'All Greek To Me',
'A Cut Above',
'Cut The Mustard',
'Up In Arms',
'Playing For Keeps',
'Fit as a Fiddle',
]

code = [f'{i:02d}' for i in range(len(descr))]

return pd.DataFrame(dict(code=code, descr=descr))
116 changes: 101 additions & 15 deletions panel/tests/ui/widgets/test_tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,11 @@
from panel.layout.base import Column
from panel.models.tabulator import _TABULATOR_THEMES_MAPPING
from panel.tests.util import get_ctrl_modifier, serve_component, wait_until
from panel.widgets import Select, Tabulator
from panel.widgets import Select, Tabulator, TextInput

pytestmark = pytest.mark.ui


@pytest.fixture
def df_mixed():
df = pd.DataFrame({
'int': [1, 2, 3, 4],
'float': [3.14, 6.28, 9.42, -2.45],
'str': ['A', 'B', 'C', 'D'],
'bool': [True, True, True, False],
'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10), dt.date(2019, 1, 10)],
'datetime': [dt.datetime(2019, 1, 1, 10), dt.datetime(2020, 1, 1, 12), dt.datetime(2020, 1, 10, 13), dt.datetime(2020, 1, 15, 13)]
}, index=['idx0', 'idx1', 'idx2', 'idx3'])
return df


@pytest.fixture(scope='session')
def df_mixed_as_string():
return """index
Expand Down Expand Up @@ -2289,7 +2276,7 @@
max_int = df_mixed['int'].max()
wait_until(lambda: page.locator('.tabulator-cell', has=page.locator(f'text="{max_int}"')) is not None, page)
max_cell = page.locator('.tabulator-cell', has=page.locator(f'text="{max_int}"'))
expect(max_cell).to_have_count(1)

Check failure on line 2279 in panel/tests/ui/widgets/test_tabulator.py

View workflow job for this annotation

GitHub Actions / ui:test-ui:ubuntu-latest

test_tabulator_patching_and_styling AssertionError: Locator expected to have count '1' Actual value: 0 Call log: LocatorAssertions.to_have_count with timeout 5000ms - waiting for locator(".tabulator-cell").filter(has=locator("text=\"100\"")) - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0"
expect(max_cell).to_have_css('background-color', _color_mapping['yellow'])


Expand Down Expand Up @@ -2967,7 +2954,6 @@
assert widget.selected_dataframe.equals(expected_selected)


@pytest.mark.xfail(reason='https://github.com/holoviz/panel/issues/3664')
def test_tabulator_selection_header_filter_unchanged(page):
df = pd.DataFrame({
'col1': list('XYYYYY'),
Expand Down Expand Up @@ -3354,7 +3340,7 @@
assert table_values == list(df2['x'].sort_values(ascending=False))
else:
return False
wait_until(x_values, page)

Check failure on line 3343 in panel/tests/ui/widgets/test_tabulator.py

View workflow job for this annotation

GitHub Actions / ui:test-ui:ubuntu-latest

test_tabulator_sorter_default_number TimeoutError: wait_until timed out in 5000 milliseconds

Check failure on line 3343 in panel/tests/ui/widgets/test_tabulator.py

View workflow job for this annotation

GitHub Actions / ui:test-ui:ubuntu-latest

test_tabulator_sorter_default_number TimeoutError: wait_until timed out in 5000 milliseconds


def test_tabulator_update_hidden_columns(page):
Expand Down Expand Up @@ -3410,6 +3396,106 @@
wait_until(lambda: widget.page_size == 3, page)


@pytest.mark.parametrize('pagination', ['local', 'remote', None])
def test_selection_indices_on_paginated_and_filtered_data(page, df_strings, pagination):
tbl = Tabulator(
df_strings,
disabled=True,
pagination=pagination,
page_size=6,
)

descr_filter = TextInput(name='descr', value='cut')

def contains_filter(df, pattern=None):
if not pattern:
return df
return df[df.descr.str.contains(pattern, case=False)]

filter_fn = param.bind(contains_filter, pattern=descr_filter)
tbl.add_filter(filter_fn)

serve_component(page, tbl)

expect(page.locator('.tabulator-table')).to_have_count(1)

row = page.locator('.tabulator-row').nth(1)
row.click()

wait_until(lambda: tbl.selection == [7], page)

tbl.page_size = 2

page.locator('.tabulator-row').nth(0).click()

wait_until(lambda: tbl.selection == [3], page)

if pagination:
page.locator('.tabulator-pages > .tabulator-page').nth(1).click()
expect(page.locator('.tabulator-row')).to_have_count(1)
page.locator('.tabulator-row').nth(0).click()
else:
expect(page.locator('.tabulator-row')).to_have_count(3)
page.locator('.tabulator-row').nth(2).click()

wait_until(lambda: tbl.selection == [8], page)

descr_filter.value = ''

wait_until(lambda: tbl.selection == [8], page)


@pytest.mark.parametrize('pagination', ['local', 'remote', None])
def test_selection_indices_on_paginated_sorted_and_filtered_data(page, df_strings, pagination):
tbl = Tabulator(
df_strings,
disabled=True,
pagination=pagination,
page_size=6,
)

descr_filter = TextInput(name='descr', value='cut')

def contains_filter(df, pattern=None):
if not pattern:
return df
return df[df.descr.str.contains(pattern, case=False)]

filter_fn = param.bind(contains_filter, pattern=descr_filter)
tbl.add_filter(filter_fn)

serve_component(page, tbl)

expect(page.locator('.tabulator-table')).to_have_count(1)

page.locator('.tabulator-col-title-holder').nth(3).click()

row = page.locator('.tabulator-row').nth(1)
row.click()

wait_until(lambda: tbl.selection == [8], page)

tbl.page_size = 2

page.locator('.tabulator-col-title-holder').nth(3).click()
page.locator('.tabulator-row').nth(0).click()

wait_until(lambda: tbl.selection == [3], page)

if pagination:
page.locator('.tabulator-pages > .tabulator-page').nth(1).click()
expect(page.locator('.tabulator-row')).to_have_count(1)
page.locator('.tabulator-row').nth(0).click()
else:
expect(page.locator('.tabulator-row')).to_have_count(3)
page.locator('.tabulator-row').nth(2).click()

wait_until(lambda: tbl.selection == [7], page)

descr_filter.value = ''

wait_until(lambda: tbl.selection == [7], page)


class Test_RemotePagination:

Expand Down
45 changes: 44 additions & 1 deletion panel/tests/widgets/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import numpy as np
import pandas as pd
import param
import pytest

from bokeh.models.widgets.tables import (
Expand Down Expand Up @@ -36,6 +37,7 @@ def makeMixedDataFrame():
return pd.DataFrame(data)



def test_dataframe_widget(dataframe, document, comm):

table = DataFrame(dataframe)
Expand Down Expand Up @@ -395,6 +397,8 @@ def test_tabulator_selected_and_filtered_dataframe(document, comm):

table.add_filter('foo3', 'C')

assert table.selection == list(range(5))

pd.testing.assert_frame_equal(table.selected_dataframe, df[df["C"] == "foo3"])

table.remove_filter('foo3')
Expand All @@ -403,7 +407,46 @@ def test_tabulator_selected_and_filtered_dataframe(document, comm):

table.add_filter('foo3', 'C')

assert table.selection == [0]
assert table.selection == [0, 1, 2]


@pytest.mark.parametrize('pagination', ['local', 'remote', None])
def test_selection_indices_on_remote_paginated_and_filtered_data(document, comm, df_strings, pagination):
tbl = Tabulator(
df_strings,
pagination=pagination,
page_size=6,
show_index=False,
height=300,
width=400
)

descr_filter = TextInput(name='descr')

def contains_filter(df, pattern=None):
if not pattern:
return df
return df[df.descr.str.contains(pattern, case=False)]

filter_fn = param.bind(contains_filter, pattern=descr_filter)
tbl.add_filter(filter_fn)

model = tbl.get_root(document, comm)

descr_filter.value = 'cut'

pd.testing.assert_frame_equal(
tbl.current_view, df_strings[df_strings.descr.str.contains('cut', case=False)]
)

model.source.selected.indices = [0, 2]

assert tbl.selection == [3, 8]

model.page_size = 2
model.source.selected.indices = [1]

assert tbl.selection == [7]


def test_tabulator_config_defaults(document, comm):
Expand Down
27 changes: 8 additions & 19 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,21 +295,8 @@ def _update_index_mapping(self):

@updating
def _update_cds(self, *events: param.parameterized.Event):
old_processed = self._processed
self._processed, data = self._get_data()
self._update_index_mapping()
# If there is a selection we have to compute new index
if self.selection and old_processed is not None:
indexes = list(self._processed.index)
selection = []
for sel in self.selection:
try:
iv = old_processed.index[sel]
idx = indexes.index(iv)
selection.append(idx)
except Exception:
continue
self.selection = selection
self._data = {k: _convert_datetime_array_ignore_list(v) for k, v in data.items()}
msg = {'data': self._data}
for ref, (m, _) in self._models.items():
Expand Down Expand Up @@ -879,7 +866,8 @@ def selected_dataframe(self):
"""
if not self.selection:
return self.current_view.iloc[:0]
return self._processed.iloc[self.selection]
df = self.value.iloc[self.selection]
return self._filter_dataframe(df)


class DataFrame(BaseTable):
Expand Down Expand Up @@ -1599,6 +1587,7 @@ def _update_selectable(self):
for ref, (model, _) in self._models.items():
self._apply_update([], {'selectable_rows': selectable}, model, ref)

@param.depends('page_size', watch=True)
def _update_max_page(self):
length = self._length
nrows = self.page_size or self.initial_page_size
Expand Down Expand Up @@ -1652,9 +1641,6 @@ def _update_column(self, column: str, array: np.ndarray):
self._processed.loc[index, column] = array

def _update_selection(self, indices: list[int] | SelectionEvent):
if self.pagination != 'remote':
self.selection = indices
return
if isinstance(indices, list):
selected = True
ilocs = []
Expand All @@ -1663,8 +1649,11 @@ def _update_selection(self, indices: list[int] | SelectionEvent):
ilocs = [] if indices.flush else self.selection.copy()
indices = indices.indices

nrows = self.page_size or self.initial_page_size
start = (self.page-1)*nrows
if self.pagination == 'remote':
nrows = self.page_size or self.initial_page_size
start = (self.page-1)*nrows
else:
start = 0
index = self._processed.iloc[[start+ind for ind in indices]].index
for v in index.values:
try:
Expand Down
Loading