Compare commits
3 Commits
2c5fe004f5
...
ResolvingP
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dc7687b25 | |||
| f08ae4a90b | |||
| b48aaf4621 |
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import html
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Literal, Any
|
||||
@@ -122,14 +122,21 @@ class DataGrid(BaseComponent):
|
||||
else:
|
||||
return ColumnType.Text # Default to Text if no match
|
||||
|
||||
def _init_columns(_df):
|
||||
columns = [DataGridColumnState(make_safe_id(col_id),
|
||||
col_index,
|
||||
col_id,
|
||||
_get_column_type(self._df[make_safe_id(col_id)].dtype))
|
||||
for col_index, col_id in enumerate(_df.columns)]
|
||||
if self._state.row_index:
|
||||
columns.insert(0, DataGridColumnState(make_safe_id(ROW_INDEX_ID), -1, " ", ColumnType.RowIndex))
|
||||
|
||||
return columns
|
||||
|
||||
self._df = df.copy()
|
||||
self._df.columns = self._df.columns.map(make_safe_id) # make sure column names are trimmed
|
||||
self._state.rows = [DataGridRowState(row_id) for row_id in self._df.index]
|
||||
self._state.columns = [DataGridColumnState(make_safe_id(col_id),
|
||||
col_index,
|
||||
col_id,
|
||||
_get_column_type(self._df[make_safe_id(col_id)].dtype))
|
||||
for col_index, col_id in enumerate(df.columns)]
|
||||
self._state.columns = _init_columns(df) # use df not self._df to keep the original title
|
||||
self._fast_access = self._init_fast_access(self._df)
|
||||
self._total_rows = len(self._df) if self._df is not None else 0
|
||||
|
||||
@@ -211,6 +218,7 @@ class DataGrid(BaseComponent):
|
||||
|
||||
self._state.columns = new_columns_states
|
||||
|
||||
self._fast_access = self._init_fast_access(self._df)
|
||||
self._views.recompute_need_save()
|
||||
|
||||
self._db.save_all(self._settings, self._state, self._df if new_column else None)
|
||||
@@ -446,8 +454,6 @@ class DataGrid(BaseComponent):
|
||||
_mk_keyboard_management(),
|
||||
Div(
|
||||
self.mk_table_header(),
|
||||
#self.mk_table_body(),
|
||||
#self.mk_table_body_sse(),
|
||||
self.mk_table_body_page(),
|
||||
self.mk_table_footer(),
|
||||
cls="dt2-inner-table"),
|
||||
@@ -483,32 +489,11 @@ class DataGrid(BaseComponent):
|
||||
|
||||
header_class = "dt2-row dt2-header" + "" if self._settings.header_visible else " hidden"
|
||||
return Div(
|
||||
Div(sse_swap="message"),
|
||||
*[_mk_header(col_def) for col_def in self._state.columns],
|
||||
cls=header_class,
|
||||
id=f"th_{self._id}"
|
||||
)
|
||||
|
||||
def mk_table_body_sse(self):
|
||||
"""
|
||||
This function is used to create a sse update
|
||||
Unfortunately, the sse does not update the correct element
|
||||
tb_{self._id} is not updated
|
||||
Plus UI refreshment issues
|
||||
"""
|
||||
max_height = self._compute_body_max_height()
|
||||
|
||||
return Div(
|
||||
hx_ext="sse",
|
||||
sse_connect=f"{ROUTE_ROOT}{Routes.YieldRow}?_id={self._id}",
|
||||
sse_close='close',
|
||||
sse_swap="message",
|
||||
hx_swap="beforeend",
|
||||
cls="dt2-body",
|
||||
style=f"max-height:{max_height}px;",
|
||||
id=f"tb_{self._id}",
|
||||
)
|
||||
|
||||
def mk_table_body_page(self):
|
||||
"""
|
||||
This function is used to update the table body when the vertical scrollbar reaches the end
|
||||
@@ -523,22 +508,6 @@ class DataGrid(BaseComponent):
|
||||
id=f"tb_{self._id}",
|
||||
)
|
||||
|
||||
def mk_table_body(self):
|
||||
df = self._get_filtered_df()
|
||||
max_height = self._compute_body_max_height()
|
||||
|
||||
return Div(
|
||||
*[Div(
|
||||
*[self.mk_body_cell(col_pos, row_index, col_def) for col_pos, col_def in enumerate(self._state.columns)],
|
||||
cls="dt2-row",
|
||||
data_row=f"{row_index}",
|
||||
id=f"tr_{self._id}-{row_index}",
|
||||
) for row_index in df.index],
|
||||
cls="dt2-body",
|
||||
style=f"max-height:{max_height}px;",
|
||||
id=f"tb_{self._id}"
|
||||
)
|
||||
|
||||
def mk_table_footer(self):
|
||||
return Div(
|
||||
*[Div(
|
||||
@@ -560,7 +529,7 @@ class DataGrid(BaseComponent):
|
||||
else:
|
||||
last_row = None
|
||||
|
||||
rows = [Div(
|
||||
rows = [Div(
|
||||
*[self.mk_body_cell(col_pos, row_index, col_def) for col_pos, col_def in enumerate(self._state.columns)],
|
||||
cls="dt2-row",
|
||||
data_row=f"{row_index}",
|
||||
@@ -568,26 +537,10 @@ class DataGrid(BaseComponent):
|
||||
**self.commands.get_page(page_index + 1) if row_index == last_row else {}
|
||||
) for row_index in df.index[start:end]]
|
||||
|
||||
rows.append(Script(f"manageScrollbars('{self._id}', false);"),)
|
||||
rows.append(Script(f"manageScrollbars('{self._id}', false);"), )
|
||||
|
||||
return rows
|
||||
|
||||
async def mk_body_content_sse(self):
|
||||
df = self._get_filtered_df()
|
||||
for i, row_index in enumerate(df.index):
|
||||
yield sse_message(Div(
|
||||
*[self.mk_body_cell(col_pos, row_index, col_def) for col_pos, col_def in enumerate(self._state.columns)],
|
||||
cls="dt2-row",
|
||||
data_row=f"{row_index}",
|
||||
id=f"tr_{self._id}-{row_index}",
|
||||
))
|
||||
if i % 50 == 0:
|
||||
await asyncio.sleep(0.01)
|
||||
logger.debug(f"yielding row {i}")
|
||||
|
||||
logger.debug(f"yielding close event")
|
||||
yield f"event: close\ndata: \n\n"
|
||||
|
||||
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState):
|
||||
if not col_def.usable:
|
||||
return None
|
||||
@@ -615,7 +568,7 @@ class DataGrid(BaseComponent):
|
||||
return mk_my_ellipsis(_value, cls="dt2-cell-content-number")
|
||||
|
||||
def process_cell_content(_value):
|
||||
value_str = str(_value)
|
||||
value_str = html.escape(str(_value))
|
||||
|
||||
if FILTER_INPUT_CID not in self._state.filtered or (
|
||||
keyword := self._state.filtered[FILTER_INPUT_CID]) is None:
|
||||
@@ -632,7 +585,6 @@ class DataGrid(BaseComponent):
|
||||
return tuple(res)
|
||||
|
||||
column_type = col_def.type
|
||||
# value = self._df.iloc[row_index, col_def.col_index]
|
||||
value = self._fast_access[col_def.col_id][row_index]
|
||||
|
||||
if column_type == ColumnType.Bool:
|
||||
@@ -922,7 +874,12 @@ class DataGrid(BaseComponent):
|
||||
dict: A dictionary where the keys are the column names of the input DataFrame
|
||||
and the values are the corresponding column values as NumPy arrays.
|
||||
"""
|
||||
return {col: df[col].to_numpy() for col in df.columns}
|
||||
if df is None:
|
||||
return {}
|
||||
|
||||
res = {col: df[col].to_numpy() for col in df.columns}
|
||||
res[ROW_INDEX_ID] = df.index.to_numpy()
|
||||
return res
|
||||
|
||||
@timed
|
||||
def __ft__(self):
|
||||
|
||||
@@ -19,6 +19,8 @@ DATAGRID_STATE_FOOTER = "footer"
|
||||
|
||||
DATAGRID_PAGE_SIZE = 50
|
||||
|
||||
ROW_INDEX_ID = "__row_index__"
|
||||
|
||||
class Routes:
|
||||
Filter = "/filter" # request the filtering in the grid
|
||||
ResetFilter = "/reset_filter" #
|
||||
|
||||
@@ -69,6 +69,7 @@ class DataGridSettings:
|
||||
class DataGridState:
|
||||
sidebar_visible: bool = False
|
||||
selected_view: str = None
|
||||
row_index: bool = False
|
||||
columns: list[DataGridColumnState] = dataclasses.field(default_factory=list)
|
||||
rows: list[DataGridRowState] = dataclasses.field(default_factory=list) # only the rows that have a specific state
|
||||
footers: list[DataGridFooterConf] = dataclasses.field(default_factory=list)
|
||||
|
||||
@@ -7,10 +7,13 @@ attr_map = {
|
||||
"_id": "id",
|
||||
}
|
||||
|
||||
|
||||
def safe_attr(attr_name):
|
||||
attr_name = attr_name.replace("hx_", "hx-")
|
||||
attr_name = attr_name.replace("data_", "data-")
|
||||
return attr_map.get(attr_name, attr_name)
|
||||
|
||||
|
||||
def to_html(item):
|
||||
if item is None:
|
||||
return ""
|
||||
@@ -27,14 +30,14 @@ def to_html(item):
|
||||
|
||||
|
||||
class MyFt:
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
self.name = name
|
||||
def __init__(self, tag, *args, **kwargs):
|
||||
self.tag = tag
|
||||
self.children = args
|
||||
self.attrs = kwargs
|
||||
self.attrs = {safe_attr(k): v for k, v in kwargs.items()}
|
||||
|
||||
def to_html(self):
|
||||
body_items = [to_html(item) for item in self.children]
|
||||
return f"<{self.name} {' '.join(f'{safe_attr(k)}="{v}"' for k, v in self.attrs.items())}>{' '.join(body_items)}</div>"
|
||||
return f"<{self.tag} {' '.join(f'{k}="{v}"' for k, v in self.attrs.items())}>{' '.join(body_items)}</div>"
|
||||
|
||||
def __ft__(self):
|
||||
return NotStr(self.to_html())
|
||||
|
||||
@@ -642,10 +642,10 @@ def extract_table_values_new(ft, header=True):
|
||||
# first, get the header
|
||||
|
||||
if header:
|
||||
header = search_elements_by_name(ft, attrs={"class": "dt2-header"}, comparison_method='contains')[0]
|
||||
header_element = search_elements_by_name(ft, attrs={"class": "dt2-header"}, comparison_method='contains')[0]
|
||||
header_map = {}
|
||||
res = OrderedDict()
|
||||
for row in header.children:
|
||||
for row in header_element.children:
|
||||
col_id = row.attrs["data-col"]
|
||||
title = row.attrs["data-tooltip"]
|
||||
header_map[col_id] = title
|
||||
@@ -654,9 +654,10 @@ def extract_table_values_new(ft, header=True):
|
||||
body = search_elements_by_name(ft, attrs={"class": "dt2-body"}, comparison_method='contains')[0]
|
||||
for row in body.children:
|
||||
for col in row.children:
|
||||
col_id = col.attrs["data-col"]
|
||||
cell_value = _get_cell_content_value(col)
|
||||
res[header_map[col_id]].append(cell_value)
|
||||
if hasattr(col, "attrs"):
|
||||
col_id = col.attrs["data-col"]
|
||||
cell_value = _get_cell_content_value(col)
|
||||
res[header_map[col_id]].append(cell_value)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -509,3 +509,18 @@ def test_i_can_compute_footer_menu_position_when_not_enough_space(dg):
|
||||
)
|
||||
|
||||
assert matches(menu, expected)
|
||||
|
||||
|
||||
def test_the_content_of_the_cell_is_escaped(empty_dg):
|
||||
df = pd.DataFrame({
|
||||
'value': ['<div> My Content </div>'],
|
||||
'value2': ['{My Content}'],
|
||||
})
|
||||
my_dg = empty_dg.init_from_dataframe(df)
|
||||
|
||||
actual = my_dg.__ft__()
|
||||
table_content = extract_table_values_new(actual, header=True)
|
||||
|
||||
assert table_content == OrderedDict({
|
||||
'value': ['<div> My Content </div>'],
|
||||
'value2': ['{My Content}']})
|
||||
|
||||
Reference in New Issue
Block a user