Fixed unit tests
This commit is contained in:
@@ -78,7 +78,7 @@ def index(session):
|
||||
layout.left_drawer.add(btn_popup, "Test")
|
||||
|
||||
# data grids
|
||||
dgs_manager = DataGridsManager(session_instance)
|
||||
dgs_manager = DataGridsManager(session_instance, save_state=True)
|
||||
layout.left_drawer.add_group("Documents", Div("Documents",
|
||||
dgs_manager.mk_main_icons(),
|
||||
cls="mf-layout-group flex gap-3"))
|
||||
|
||||
@@ -224,6 +224,10 @@ class DataGrid(MultipleInstance):
|
||||
self._columns = None
|
||||
self.commands = Commands(self)
|
||||
|
||||
# reset
|
||||
self._state.selection.selected = None
|
||||
self._state.selection.last_selected = None
|
||||
|
||||
# Obtain DataService from DataServicesManager (no parent hierarchy)
|
||||
data_services_manager = InstancesManager.get_by_type(self._session, DataServicesManager)
|
||||
data_service_id = self.get_data_service_id_from_data_grid_id(self._id)
|
||||
@@ -264,7 +268,7 @@ class DataGrid(MultipleInstance):
|
||||
# self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed())
|
||||
|
||||
if self._settings.enable_formatting:
|
||||
provider = InstancesManager.get_by_type(self._session, DatagridMetadataProvider, None)
|
||||
provider = DatagridMetadataProvider(self._parent)
|
||||
completion_engine = FormattingCompletionEngine(provider, self.get_table_name())
|
||||
editor_conf = DslEditorConf(engine_id=completion_engine.get_id())
|
||||
dsl = FormattingDSL()
|
||||
@@ -305,6 +309,10 @@ class DataGrid(MultipleInstance):
|
||||
def _fast_access(self):
|
||||
return self._data_service.get_store().ns_fast_access
|
||||
|
||||
@property
|
||||
def _row_data(self):
|
||||
return self._data_service.get_store().ns_row_data
|
||||
|
||||
def _apply_sort(self, df):
|
||||
if df is None:
|
||||
return None
|
||||
@@ -413,9 +421,9 @@ class DataGrid(MultipleInstance):
|
||||
if self._state.table_format:
|
||||
return self._state.table_format
|
||||
|
||||
# Get global tables formatting from DatagridMetadataProvider
|
||||
provider = InstancesManager.get_by_type(self._session, DatagridMetadataProvider, None)
|
||||
return provider.all_tables_formats if provider is not None else []
|
||||
# Get global tables formatting from DataGridsManager
|
||||
dgm = self.get_parent()
|
||||
return dgm.get_state().all_tables_formats if dgm is not None else []
|
||||
|
||||
def _init_columns(self):
|
||||
# Populate UI state from DataService columns when creating a new grid
|
||||
@@ -494,22 +502,6 @@ class DataGrid(MultipleInstance):
|
||||
|
||||
self._state.save()
|
||||
|
||||
def handle_reorder_columns(self, order: list[str]):
|
||||
"""Reorder columns based on a full ordered list of column IDs.
|
||||
|
||||
Args:
|
||||
order: List of col_id strings in the desired display order.
|
||||
Columns not present in order are appended at the end.
|
||||
"""
|
||||
logger.debug(f"handle_reorder_columns: {order=}")
|
||||
columns_by_id = {c.col_id: c for c in self._state.columns}
|
||||
new_order = [columns_by_id[col_id] for col_id in order if col_id in columns_by_id]
|
||||
remaining = [c for c in self._state.columns if c.col_id not in order]
|
||||
self._state.columns = new_order + remaining
|
||||
self._init_columns()
|
||||
self._state.save()
|
||||
return self.render_partial("table")
|
||||
|
||||
def calculate_optimal_column_width(self, col_id: str) -> int:
|
||||
"""
|
||||
Calculate optimal width for a column based on content.
|
||||
@@ -625,6 +617,42 @@ class DataGrid(MultipleInstance):
|
||||
self._state.save()
|
||||
return self.render_partial()
|
||||
|
||||
def handle_columns_reorder(self, order: list[str]):
|
||||
"""Reorder columns based on a full ordered list of column IDs.
|
||||
|
||||
Args:
|
||||
order: List of col_id strings in the desired display order.
|
||||
Columns not present in order are appended at the end.
|
||||
"""
|
||||
logger.debug(f"handle_reorder_columns: {order=}")
|
||||
columns_by_id = {c.col_id: c for c in self._state.columns}
|
||||
new_order = [columns_by_id[col_id] for col_id in order if col_id in columns_by_id]
|
||||
remaining = [c for c in self._state.columns if c.col_id not in order]
|
||||
self._state.columns = new_order + remaining
|
||||
self._init_columns()
|
||||
self._state.save()
|
||||
return self.render_partial("table")
|
||||
|
||||
def handle_columns_updates(self, updates: dict[str, dict]):
|
||||
logger.debug(f"handle_columns_update: {updates=}")
|
||||
|
||||
def _update(col_def):
|
||||
need_saving = False
|
||||
for key, value in update.items():
|
||||
if hasattr(col_def, key):
|
||||
setattr(col_def, key, value)
|
||||
need_saving = True
|
||||
return need_saving
|
||||
|
||||
for col_id, update in updates.items():
|
||||
column_state = [col_def for col_def in self._columns if col_def.col_id == col_id][0]
|
||||
if _update(column_state.get_col_ui_state()):
|
||||
self._state.save()
|
||||
if _update(column_state.get_col_def()):
|
||||
self._data_service.save_state()
|
||||
|
||||
return self.render_partial("table")
|
||||
|
||||
def handle_toggle_columns_manager(self):
|
||||
logger.debug(f"toggle_columns_manager")
|
||||
self._panel.set_title(side="right", title="Columns")
|
||||
@@ -803,7 +831,7 @@ class DataGrid(MultipleInstance):
|
||||
formatted_value = None
|
||||
rules = self._get_format_rules(col_pos, row_index, col_def)
|
||||
if rules:
|
||||
row_data = self._df_store.ns_row_data[row_index] if row_index < len(self._df_store.ns_row_data) else None
|
||||
row_data = self._row_data[row_index] if row_index < len(self._row_data) else None
|
||||
style, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
|
||||
|
||||
# Use formatted value or convert to string
|
||||
|
||||
@@ -23,6 +23,14 @@ class Commands(BaseCommands):
|
||||
self._owner,
|
||||
self._owner.handle_on_reorder
|
||||
).htmx(target=f"#{self._id}")
|
||||
|
||||
def toggle_column_visibility(self, col_id: str):
|
||||
return Command("ToggleColumnVisibility",
|
||||
"Toggle column visibility",
|
||||
self._owner,
|
||||
self._owner.handle_toggle_column_visibility,
|
||||
kwargs={"col_id": col_id}
|
||||
).htmx(target=f"#{self._id}")
|
||||
|
||||
|
||||
class DataGridColumnsList(MultipleInstance):
|
||||
@@ -43,7 +51,14 @@ class DataGridColumnsList(MultipleInstance):
|
||||
|
||||
def handle_on_reorder(self, order: list):
|
||||
logger.debug(f"on_reorder {order=}")
|
||||
ret = self._parent.handle_reorder_columns(order)
|
||||
ret = self._parent.handle_columns_reorder(order)
|
||||
return self.render(), ret
|
||||
|
||||
def handle_toggle_column_visibility(self, col_id):
|
||||
logger.debug(f"handle_toggle_column_visibility {col_id=}")
|
||||
col_def = [c for c in self.columns if c.col_id == col_id][0]
|
||||
updates = {col_id: {"visible": not col_def.visible}}
|
||||
ret = self._parent.handle_columns_updates(updates)
|
||||
return self.render(), ret
|
||||
|
||||
def mk_column_label(self, col_def: DataGridColumnState):
|
||||
@@ -51,7 +66,7 @@ class DataGridColumnsList(MultipleInstance):
|
||||
mk.icon(grip_horizontal, cls="mf-drag-handle cursor-grab mr-1 opacity-40"),
|
||||
mk.mk(
|
||||
Input(type="checkbox", cls="checkbox checkbox-sm", checked=col_def.visible),
|
||||
# command=self.commands.toggle_column(col_def.col_id)
|
||||
command=self.commands.toggle_column_visibility(col_def.col_id)
|
||||
),
|
||||
mk.mk(
|
||||
Div(
|
||||
|
||||
@@ -127,7 +127,7 @@ class DataGridFormattingEditor(DslEditor):
|
||||
from myfasthtml.controls.DataGridsManager import DataGridsManager
|
||||
manager = InstancesManager.get_by_type(self._session, DataGridsManager)
|
||||
if manager:
|
||||
manager.all_tables_formats = tables_rules
|
||||
manager.get_state().all_tables_formats = tables_rules
|
||||
|
||||
# Step 6: Update state atomically
|
||||
self._parent.get_state().update(state)
|
||||
|
||||
@@ -15,6 +15,7 @@ from myfasthtml.core.DataGridsRegistry import DataGridsRegistry
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.data.DataServicesManager import DataServicesManager
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.formatting.dataclasses import FormatRule
|
||||
from myfasthtml.core.instances import InstancesManager, SingleInstance
|
||||
from myfasthtml.icons.fluent_p1 import table_add20_regular
|
||||
from myfasthtml.icons.fluent_p3 import folder_open20_regular
|
||||
@@ -31,10 +32,11 @@ class DocumentDefinition:
|
||||
|
||||
|
||||
class DataGridsState(DbObject):
|
||||
def __init__(self, owner, name=None):
|
||||
super().__init__(owner, name=name)
|
||||
def __init__(self, owner, save_state, name=None):
|
||||
super().__init__(owner, save_state=save_state, name=name)
|
||||
with self.initializing():
|
||||
self.elements: list[DocumentDefinition] = []
|
||||
self.all_tables_formats: list[FormatRule] = []
|
||||
|
||||
|
||||
class Commands(BaseCommands):
|
||||
@@ -87,12 +89,12 @@ class DataGridsManager(SingleInstance):
|
||||
by DataServicesManager and DataService.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, _id=None):
|
||||
def __init__(self, parent, _id=None, save_state=None):
|
||||
if not getattr(self, "_is_new_instance", False):
|
||||
return
|
||||
super().__init__(parent, _id=_id)
|
||||
self.commands = Commands(self)
|
||||
self._state = DataGridsState(self)
|
||||
self._state = DataGridsState(self, save_state)
|
||||
self._tree = self._mk_tree()
|
||||
self._tree.bind_command("SelectNode", self.commands.show_document())
|
||||
self._tree.bind_command("DeleteNode", self.commands.delete_grid(), when="before")
|
||||
@@ -102,6 +104,9 @@ class DataGridsManager(SingleInstance):
|
||||
# Data layer — session-scoped singletons
|
||||
self._data_services_manager = DataServicesManager(self._parent)
|
||||
|
||||
def get_state(self):
|
||||
return self._state
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Grid lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -78,6 +78,12 @@ class DataGridColumnState:
|
||||
self._col_def = col_def
|
||||
self._col_ui_state = col_ui_state
|
||||
|
||||
def get_col_def(self):
|
||||
return self._col_def
|
||||
|
||||
def get_col_ui_state(self):
|
||||
return self._col_ui_state
|
||||
|
||||
@property
|
||||
def col_id(self):
|
||||
return self._col_def.col_id
|
||||
|
||||
@@ -84,6 +84,9 @@ class DataService(MultipleInstance):
|
||||
self._store = DataStore(self, save_state=save_state)
|
||||
self._init_store()
|
||||
|
||||
def save_state(self):
|
||||
self._state.save()
|
||||
|
||||
@property
|
||||
def columns(self) -> list[ColumnDefinition]:
|
||||
"""Return the list of column definitions."""
|
||||
|
||||
@@ -120,3 +120,10 @@ class DataServicesManager(SingleInstance):
|
||||
return service.get_store()
|
||||
logger.warning(f"DataServicesManager: table '{table_name}' not found")
|
||||
return None
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
For test purposes only.
|
||||
:return:
|
||||
"""
|
||||
self._services.clear()
|
||||
@@ -3,6 +3,7 @@ Completion engine for the formatting DSL.
|
||||
|
||||
Implements the BaseCompletionEngine for DataGrid formatting rules.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
||||
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
|
||||
@@ -11,6 +12,7 @@ from . import presets
|
||||
from .contexts import Context, DetectedScope, detect_scope, detect_context
|
||||
from .provider import DatagridMetadataProvider
|
||||
|
||||
logger = logging.getLogger("FormattingCompletionEngine")
|
||||
|
||||
class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
"""
|
||||
@@ -222,8 +224,8 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
if tables:
|
||||
columns = self.provider.list_columns(self.table_name)
|
||||
return [Suggestion(col, "Column", "column") for col in columns]
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as ex:
|
||||
logger.error(f"Error getting column suggestions: {ex}")
|
||||
return []
|
||||
|
||||
def _get_column_suggestions_with_closing_quote(self) -> list[Suggestion]:
|
||||
|
||||
@@ -18,186 +18,185 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider):
|
||||
"""Concrete session-scoped metadata provider for DataGrid DSL engines.
|
||||
"""Concrete session-scoped metadata provider for DataGrid DSL engines.
|
||||
|
||||
Implements BaseMetadataProvider by delegating live data queries to
|
||||
DataServicesManager. Also holds the global formatting presets and the
|
||||
all_tables_formats rule applied to every table.
|
||||
Implements BaseMetadataProvider by delegating live data queries to
|
||||
DataServicesManager. Also holds the global formatting presets and the
|
||||
all_tables_formats rule applied to every table.
|
||||
|
||||
Access pattern (from any component):
|
||||
provider = InstancesManager.get_by_type(session, DatagridMetadataProvider)
|
||||
Access pattern (from any component):
|
||||
provider = InstancesManager.get_by_type(session, DatagridMetadataProvider)
|
||||
|
||||
Attributes:
|
||||
style_presets: Dict of named style presets available in the DSL.
|
||||
formatter_presets: Dict of named formatter presets available in the DSL.
|
||||
all_tables_formats: Global format rules applied to all tables.
|
||||
Attributes:
|
||||
style_presets: Dict of named style presets available in the DSL.
|
||||
formatter_presets: Dict of named formatter presets available in the DSL.
|
||||
all_tables_formats: Global format rules applied to all tables.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, session: Optional[dict] = None,
|
||||
_id: Optional[str] = None):
|
||||
super().__init__(parent, session, _id)
|
||||
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
||||
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
||||
self.all_tables_formats: list = []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Table and column metadata — delegated to DataServicesManager
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_tables(self) -> list[str]:
|
||||
"""Return the list of all registered table names.
|
||||
|
||||
Returns:
|
||||
List of table names in "namespace.name" format.
|
||||
"""
|
||||
manager = self._get_data_services_manager()
|
||||
if manager is None:
|
||||
return []
|
||||
return [s.table_name for s in manager._services.values() if s.table_name]
|
||||
|
||||
def list_columns(self, table_name: str) -> list[str]:
|
||||
"""Return the column identifiers for a table.
|
||||
|
||||
def __init__(self, parent=None, session: Optional[dict] = None,
|
||||
_id: Optional[str] = None):
|
||||
super().__init__(parent, session, _id)
|
||||
with self.initializing():
|
||||
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
||||
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
||||
self.all_tables_formats: list = []
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Table and column metadata — delegated to DataServicesManager
|
||||
# ------------------------------------------------------------------
|
||||
Returns:
|
||||
List of col_id strings.
|
||||
"""
|
||||
service = self._get_service(table_name)
|
||||
if service is None:
|
||||
return []
|
||||
return [c.col_id for c in service.columns]
|
||||
|
||||
def list_column_values(self, table_name: str, column_name: str) -> list[Any]:
|
||||
"""Return the distinct values present in a column.
|
||||
|
||||
def list_tables(self) -> list[str]:
|
||||
"""Return the list of all registered table names.
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
column_name: Column identifier.
|
||||
|
||||
Returns:
|
||||
List of table names in "namespace.name" format.
|
||||
"""
|
||||
manager = self._get_data_services_manager()
|
||||
if manager is None:
|
||||
return []
|
||||
return [s.table_name for s in manager._services.values() if s.table_name]
|
||||
Returns:
|
||||
List of distinct values, empty list if not found.
|
||||
"""
|
||||
service = self._get_service(table_name)
|
||||
if service is None:
|
||||
return []
|
||||
store = service.get_store()
|
||||
if store.ne_df is None or column_name not in store.ne_df.columns:
|
||||
return []
|
||||
return store.ne_df[column_name].dropna().unique().tolist()
|
||||
|
||||
def get_row_count(self, table_name: str) -> int:
|
||||
"""Return the number of rows in a table.
|
||||
|
||||
def list_columns(self, table_name: str) -> list[str]:
|
||||
"""Return the column identifiers for a table.
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
Returns:
|
||||
Row count, or 0 if not found.
|
||||
"""
|
||||
service = self._get_service(table_name)
|
||||
if service is None:
|
||||
return 0
|
||||
store = service.get_store()
|
||||
return store.ns_total_rows or 0
|
||||
|
||||
def get_column_type(self, table_name: str, column_name: str):
|
||||
"""Return the ColumnType for a column.
|
||||
|
||||
Returns:
|
||||
List of col_id strings.
|
||||
"""
|
||||
service = self._get_service(table_name)
|
||||
if service is None:
|
||||
return []
|
||||
return [c.col_id for c in service.columns]
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
column_name: Column identifier.
|
||||
|
||||
def list_column_values(self, table_name: str, column_name: str) -> list[Any]:
|
||||
"""Return the distinct values present in a column.
|
||||
Returns:
|
||||
ColumnType enum value, or None if not found.
|
||||
"""
|
||||
service = self._get_service(table_name)
|
||||
if service is None:
|
||||
return None
|
||||
for col in service.columns:
|
||||
if col.col_id == column_name:
|
||||
return col.type
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Preset metadata — held locally
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_style_presets(self) -> list[str]:
|
||||
"""Return the names of all registered style presets."""
|
||||
return list(self.style_presets.keys())
|
||||
|
||||
def list_format_presets(self) -> list[str]:
|
||||
"""Return the names of all registered formatter presets."""
|
||||
return list(self.formatter_presets.keys())
|
||||
|
||||
def get_style_presets(self) -> dict:
|
||||
"""Return the full style presets dict."""
|
||||
return self.style_presets
|
||||
|
||||
def get_formatter_presets(self) -> dict:
|
||||
"""Return the full formatter presets dict."""
|
||||
return self.formatter_presets
|
||||
|
||||
def add_style_preset(self, name: str, preset: dict) -> None:
|
||||
"""Add or update a named style preset.
|
||||
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
column_name: Column identifier.
|
||||
Args:
|
||||
name: Preset name.
|
||||
preset: Style definition dict.
|
||||
"""
|
||||
self.style_presets[name] = preset
|
||||
|
||||
def add_formatter_preset(self, name: str, preset: dict) -> None:
|
||||
"""Add or update a named formatter preset.
|
||||
|
||||
Returns:
|
||||
List of distinct values, empty list if not found.
|
||||
"""
|
||||
service = self._get_service(table_name)
|
||||
if service is None:
|
||||
return []
|
||||
store = service.get_store()
|
||||
if store.ne_df is None or column_name not in store.ne_df.columns:
|
||||
return []
|
||||
return store.ne_df[column_name].dropna().unique().tolist()
|
||||
Args:
|
||||
name: Preset name.
|
||||
preset: Formatter definition dict.
|
||||
"""
|
||||
self.formatter_presets[name] = preset
|
||||
|
||||
def remove_style_preset(self, name: str) -> None:
|
||||
"""Remove a style preset by name.
|
||||
|
||||
def get_row_count(self, table_name: str) -> int:
|
||||
"""Return the number of rows in a table.
|
||||
Args:
|
||||
name: Preset name to remove.
|
||||
"""
|
||||
self.style_presets.pop(name, None)
|
||||
|
||||
def remove_formatter_preset(self, name: str) -> None:
|
||||
"""Remove a formatter preset by name.
|
||||
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
Args:
|
||||
name: Preset name to remove.
|
||||
"""
|
||||
self.formatter_presets.pop(name, None)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Private helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_data_services_manager(self) -> Optional[DataServicesManager]:
|
||||
"""Return the DataServicesManager for this session."""
|
||||
return InstancesManager.get_by_type(
|
||||
self._session, DataServicesManager, default=None
|
||||
)
|
||||
|
||||
def _get_service(self, table_name: str):
|
||||
"""Return the DataService for a given table name.
|
||||
|
||||
Returns:
|
||||
Row count, or 0 if not found.
|
||||
"""
|
||||
service = self._get_service(table_name)
|
||||
if service is None:
|
||||
return 0
|
||||
store = service.get_store()
|
||||
return store.ns_total_rows or 0
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
|
||||
def get_column_type(self, table_name: str, column_name: str):
|
||||
"""Return the ColumnType for a column.
|
||||
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
column_name: Column identifier.
|
||||
|
||||
Returns:
|
||||
ColumnType enum value, or None if not found.
|
||||
"""
|
||||
service = self._get_service(table_name)
|
||||
if service is None:
|
||||
return None
|
||||
for col in service.columns:
|
||||
if col.col_id == column_name:
|
||||
return col.type
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Preset metadata — held locally
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_style_presets(self) -> list[str]:
|
||||
"""Return the names of all registered style presets."""
|
||||
return list(self.style_presets.keys())
|
||||
|
||||
def list_format_presets(self) -> list[str]:
|
||||
"""Return the names of all registered formatter presets."""
|
||||
return list(self.formatter_presets.keys())
|
||||
|
||||
def get_style_presets(self) -> dict:
|
||||
"""Return the full style presets dict."""
|
||||
return self.style_presets
|
||||
|
||||
def get_formatter_presets(self) -> dict:
|
||||
"""Return the full formatter presets dict."""
|
||||
return self.formatter_presets
|
||||
|
||||
def add_style_preset(self, name: str, preset: dict) -> None:
|
||||
"""Add or update a named style preset.
|
||||
|
||||
Args:
|
||||
name: Preset name.
|
||||
preset: Style definition dict.
|
||||
"""
|
||||
self.style_presets[name] = preset
|
||||
|
||||
def add_formatter_preset(self, name: str, preset: dict) -> None:
|
||||
"""Add or update a named formatter preset.
|
||||
|
||||
Args:
|
||||
name: Preset name.
|
||||
preset: Formatter definition dict.
|
||||
"""
|
||||
self.formatter_presets[name] = preset
|
||||
|
||||
def remove_style_preset(self, name: str) -> None:
|
||||
"""Remove a style preset by name.
|
||||
|
||||
Args:
|
||||
name: Preset name to remove.
|
||||
"""
|
||||
self.style_presets.pop(name, None)
|
||||
|
||||
def remove_formatter_preset(self, name: str) -> None:
|
||||
"""Remove a formatter preset by name.
|
||||
|
||||
Args:
|
||||
name: Preset name to remove.
|
||||
"""
|
||||
self.formatter_presets.pop(name, None)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Private helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_data_services_manager(self) -> Optional[DataServicesManager]:
|
||||
"""Return the DataServicesManager for this session."""
|
||||
return InstancesManager.get_by_type(
|
||||
self._session, DataServicesManager, default=None
|
||||
)
|
||||
|
||||
def _get_service(self, table_name: str):
|
||||
"""Return the DataService for a given table name.
|
||||
|
||||
Args:
|
||||
table_name: Fully qualified table name.
|
||||
|
||||
Returns:
|
||||
DataService instance, or None if not found.
|
||||
"""
|
||||
manager = self._get_data_services_manager()
|
||||
if manager is None:
|
||||
return None
|
||||
for service in manager._services.values():
|
||||
if service.table_name == table_name:
|
||||
return service
|
||||
return None
|
||||
Returns:
|
||||
DataService instance, or None if not found.
|
||||
"""
|
||||
manager = self._get_data_services_manager()
|
||||
if manager is None:
|
||||
return None
|
||||
for service in manager._services.values():
|
||||
if service.table_name == table_name:
|
||||
return service
|
||||
return None
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from myfasthtml.controls.DataGrid import DataGrid, DatagridConf
|
||||
from myfasthtml.controls.DataGridsManager import DataGridsManager
|
||||
from myfasthtml.controls.TabsManager import TabsManager
|
||||
from myfasthtml.core.data.DataServicesManager import DataServicesManager
|
||||
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
||||
|
||||
|
||||
@@ -27,3 +32,27 @@ def session():
|
||||
def root_instance(session):
|
||||
InstancesManager.reset()
|
||||
return RootInstanceForTests(session=session)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def datagrids_manager(root_instance):
|
||||
return DataGridsManager(root_instance)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dataservices_manager(root_instance):
|
||||
return InstancesManager.get_by_type(root_instance._session, DataServicesManager)
|
||||
|
||||
|
||||
def get_data_grid(root_instance, df: DataFrame, table_name: str = "test.grid1", save_state: bool = False):
|
||||
TabsManager(root_instance) # just define it
|
||||
dgm = DataGridsManager(root_instance)
|
||||
dsm = InstancesManager.get_by_type(root_instance._session, DataServicesManager)
|
||||
|
||||
data_service = dsm.create_service(table_name, save_state=save_state)
|
||||
data_service.load_dataframe(df)
|
||||
|
||||
grid_id = DataGrid.get_grid_id_from_data_service_id(data_service.get_id())
|
||||
namespace, table_name = table_name.split(".")
|
||||
conf = DatagridConf(namespace=namespace, name=table_name)
|
||||
return DataGrid(dgm, conf=conf, save_state=save_state, _id=grid_id)
|
||||
|
||||
@@ -3,6 +3,7 @@ import pytest
|
||||
from fastcore.basics import NotStr
|
||||
from fasthtml.components import Div, Script
|
||||
|
||||
from controls.conftest import get_data_grid
|
||||
from myfasthtml.controls.DataGrid import DataGrid, DatagridConf
|
||||
from myfasthtml.controls.DataGridsManager import DataGridsManager
|
||||
from myfasthtml.controls.TabsManager import TabsManager
|
||||
@@ -357,6 +358,31 @@ class TestDataGridBehaviour:
|
||||
dg._selection_mode_selector.cycle_state()
|
||||
dg.change_selection_mode()
|
||||
assert dg._state.selection.selection_mode == "cell"
|
||||
|
||||
def test_i_can_handle_column_update(self, root_instance):
|
||||
df = pd.DataFrame({
|
||||
"name": ["Alice", "Bob", "Charlie"],
|
||||
"age": [25, 30, 35],
|
||||
"active": [True, False, True],
|
||||
})
|
||||
dg = get_data_grid(root_instance, df, save_state=True)
|
||||
|
||||
dg.handle_columns_updates({"name": {"title": "Name",
|
||||
"width": 150,
|
||||
"formula": "{age} + 1",
|
||||
"type": ColumnType.Number,
|
||||
"visible": False}})
|
||||
|
||||
# check the ui_state
|
||||
col_ui_state = dg._state.columns[0]
|
||||
assert col_ui_state.width == 150
|
||||
assert col_ui_state.visible is False
|
||||
|
||||
# check the colum def
|
||||
col_def = dg._data_service._state.columns[0]
|
||||
assert col_def.title == "Name"
|
||||
assert col_def.type == ColumnType.Number
|
||||
assert col_def.formula == "{age} + 1"
|
||||
|
||||
|
||||
class TestDataGridRender:
|
||||
|
||||
@@ -1,423 +0,0 @@
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import pytest
|
||||
from fasthtml.common import Div, FT, Input, Form, Fieldset, Select
|
||||
|
||||
from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
|
||||
from myfasthtml.controls.Search import Search
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridColumnUiState
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
|
||||
from myfasthtml.core.instances import InstancesManager, MultipleInstance
|
||||
from myfasthtml.test.matcher import (
|
||||
matches, find_one, find, Contains, TestIcon, TestObject
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockDatagridState:
|
||||
"""Mock state object that mimics DatagridState."""
|
||||
columns: list = field(default_factory=list)
|
||||
|
||||
|
||||
class MockDataGrid(MultipleInstance):
|
||||
"""Mock DataGrid parent for testing DataGridColumnsManager."""
|
||||
|
||||
def __init__(self, parent, columns=None, _id=None):
|
||||
super().__init__(parent, _id=_id)
|
||||
self._state = MockDatagridState(columns=columns or [])
|
||||
self._save_state_called = False
|
||||
|
||||
def get_state(self):
|
||||
return self._state
|
||||
|
||||
def save_state(self):
|
||||
self._save_state_called = True
|
||||
|
||||
def get_table_name(self):
|
||||
return "mock_table"
|
||||
|
||||
def get_formula_engine(self):
|
||||
return None
|
||||
|
||||
|
||||
# col_def: ColumnDefinition, col_ui_state: DataGridColumnUiState
|
||||
|
||||
@pytest.fixture
|
||||
def mock_datagrid(root_instance):
|
||||
"""Create a mock DataGrid with sample columns."""
|
||||
columns = [
|
||||
DataGridColumnState(ColumnDefinition(col_id="name", col_index=0, title="Name", type=ColumnType.Text),
|
||||
DataGridColumnUiState(col_id="name", visible=True)),
|
||||
DataGridColumnState(ColumnDefinition(col_id="age", col_index=1, title="Age", type=ColumnType.Number),
|
||||
DataGridColumnUiState(col_id="age", visible=True)),
|
||||
DataGridColumnState(ColumnDefinition(col_id="email", col_index=2, title="Email", type=ColumnType.Text),
|
||||
DataGridColumnUiState(col_id="email", visible=False)),
|
||||
]
|
||||
yield MockDataGrid(root_instance, columns=columns, _id="test-datagrid")
|
||||
InstancesManager.reset()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def columns_manager(mock_datagrid):
|
||||
"""Create a DataGridColumnsManager instance for testing."""
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
yield DataGridColumnsManager(mock_datagrid)
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
|
||||
|
||||
class TestDataGridColumnsManagerBehaviour:
|
||||
"""Tests for DataGridColumnsManager behavior and logic."""
|
||||
|
||||
# =========================================================================
|
||||
# Get Column Definition
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_get_existing_column_by_id(self, columns_manager):
|
||||
"""Test finding an existing column by its ID."""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
|
||||
assert col_def is not None
|
||||
assert col_def.col_id == "name"
|
||||
assert col_def.title == "Name"
|
||||
|
||||
def test_i_cannot_get_nonexistent_column(self, columns_manager):
|
||||
"""Test that getting a nonexistent column returns None."""
|
||||
col_def = columns_manager._get_col_def_from_col_id("nonexistent")
|
||||
|
||||
assert col_def is None
|
||||
|
||||
def test_i_can_get_and_update_column_state(self, columns_manager):
|
||||
"""Test that get_col_def_from_col_id updates the column state."""
|
||||
updates = {"title": "New Name", "visible": "on", "type": "Number", "width": 200}
|
||||
col_def = columns_manager._get_updated_col_def_from_col_id("name", updates)
|
||||
assert col_def.title == "New Name"
|
||||
assert col_def.visible is True
|
||||
assert col_def.type == ColumnType.Number
|
||||
assert col_def.width == 200
|
||||
|
||||
def test_i_can_get_and_update_column_state_visible_false(self, columns_manager):
|
||||
"""Test that get_col_def_from_col_id updates the column state."""
|
||||
updates = {} # visible is missing in the update => It must be set to False
|
||||
col_def = columns_manager._get_updated_col_def_from_col_id("name", updates)
|
||||
|
||||
assert col_def.visible is False
|
||||
|
||||
# =========================================================================
|
||||
# Toggle Column Visibility
|
||||
# =========================================================================
|
||||
|
||||
@pytest.mark.parametrize("col_id, initial_visible, expected_visible", [
|
||||
("name", True, False), # visible -> hidden
|
||||
("email", False, True), # hidden -> visible
|
||||
])
|
||||
def test_i_can_toggle_column_visibility(self, columns_manager, col_id, initial_visible, expected_visible):
|
||||
"""Test toggling column visibility from visible to hidden and vice versa."""
|
||||
col_def = columns_manager._get_col_def_from_col_id(col_id)
|
||||
assert col_def.visible == initial_visible
|
||||
|
||||
columns_manager.toggle_column(col_id)
|
||||
|
||||
col_def = columns_manager._get_col_def_from_col_id(col_id)
|
||||
assert col_def.visible == expected_visible
|
||||
|
||||
def test_toggle_column_saves_state(self, columns_manager, mock_datagrid):
|
||||
"""Test that toggle_column calls save_state on parent."""
|
||||
mock_datagrid._save_state_called = False
|
||||
|
||||
columns_manager.toggle_column("name")
|
||||
|
||||
assert mock_datagrid._save_state_called is True
|
||||
|
||||
def test_toggle_column_returns_column_label(self, columns_manager):
|
||||
"""Test that toggle_column returns an HTML element."""
|
||||
result = columns_manager.toggle_column("name")
|
||||
|
||||
assert isinstance(result, FT)
|
||||
|
||||
def test_i_cannot_toggle_nonexistent_column(self, columns_manager):
|
||||
"""Test that toggling a nonexistent column returns an error message."""
|
||||
result = columns_manager.toggle_column("nonexistent")
|
||||
|
||||
expected = Div("Column 'nonexistent' not found")
|
||||
assert matches(result, expected)
|
||||
|
||||
# =========================================================================
|
||||
# Show All Columns
|
||||
# =========================================================================
|
||||
|
||||
def test_show_all_columns_returns_search_component(self, columns_manager):
|
||||
"""Test that mk_all_columns returns a Search component."""
|
||||
result = columns_manager.mk_all_columns()
|
||||
|
||||
assert isinstance(result, Search)
|
||||
|
||||
def test_show_all_columns_returns_configured_search(self, columns_manager):
|
||||
"""Test that mk_all_columns returns a correctly configured Search component."""
|
||||
result = columns_manager.mk_all_columns()
|
||||
|
||||
assert result.items_names == "Columns"
|
||||
assert len(result.items) == 3
|
||||
col_def = result.items[0]
|
||||
assert result.get_attr(col_def) == col_def.col_id
|
||||
|
||||
# =========================================================================
|
||||
# Update Column
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_update_column_title(self, columns_manager):
|
||||
"""Test updating a column's title via client_response."""
|
||||
columns_manager.save_column_details("name", {"title": "New Name"})
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.title == "New Name"
|
||||
|
||||
def test_i_can_update_column_visibility_via_form(self, columns_manager):
|
||||
"""Test updating column visibility via checkbox form value."""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.visible is True
|
||||
|
||||
# Unchecked checkbox sends nothing, checked sends "on"
|
||||
columns_manager.save_column_details("name", {"visible": "off"}) # Not "on" means unchecked
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.visible is False
|
||||
|
||||
# Check it back on
|
||||
columns_manager.save_column_details("name", {"visible": "on"})
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.visible is True
|
||||
|
||||
def test_i_can_update_column_type(self, columns_manager):
|
||||
"""Test updating a column's type."""
|
||||
columns_manager.save_column_details("name", {"type": "Number"})
|
||||
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.type == ColumnType.Number
|
||||
|
||||
def test_i_can_update_column_width(self, columns_manager):
|
||||
"""Test updating a column's width."""
|
||||
columns_manager.save_column_details("name", {"width": "200"})
|
||||
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.width == 200
|
||||
|
||||
def test_update_column_saves_state(self, columns_manager, mock_datagrid):
|
||||
"""Test that save_column_details calls save_state on parent."""
|
||||
mock_datagrid._save_state_called = False
|
||||
|
||||
columns_manager.save_column_details("name", {"title": "Updated"})
|
||||
|
||||
assert mock_datagrid._save_state_called is True
|
||||
|
||||
def test_update_column_ignores_unknown_attributes(self, columns_manager):
|
||||
"""Test that save_column_details ignores attributes not in DataGridColumnState."""
|
||||
columns_manager.save_column_details("name", {"unknown_attr": "value", "title": "New Title"})
|
||||
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.title == "New Title"
|
||||
assert not hasattr(col_def, "unknown_attr")
|
||||
|
||||
|
||||
class TestDataGridColumnsManagerRender:
|
||||
"""Tests for DataGridColumnsManager HTML rendering."""
|
||||
|
||||
@pytest.fixture
|
||||
def columns_manager(self, mock_datagrid):
|
||||
"""Create a fresh DataGridColumnsManager for render tests."""
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
cm = DataGridColumnsManager(mock_datagrid)
|
||||
yield cm
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
|
||||
# =========================================================================
|
||||
# Global Structure
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_render_columns_manager_with_columns(self, columns_manager):
|
||||
"""Test that DataGridColumnsManager renders with correct global structure.
|
||||
|
||||
Why these elements matter:
|
||||
- id: Required for HTMX targeting in commands
|
||||
- Contains Search component: Main content for column list
|
||||
"""
|
||||
html = columns_manager.render()
|
||||
|
||||
expected = Div(
|
||||
TestObject(Search), # Search component (column list)
|
||||
Div(), # New column button
|
||||
id=columns_manager._id,
|
||||
)
|
||||
|
||||
assert matches(html, expected)
|
||||
|
||||
# =========================================================================
|
||||
# mk_column_label
|
||||
# =========================================================================
|
||||
|
||||
def test_column_label_has_checkbox_and_details_navigation(self, columns_manager):
|
||||
"""Test that column label contains checkbox and navigation to details.
|
||||
|
||||
Why these elements matter:
|
||||
- Checkbox (Input type=checkbox): Controls column visibility
|
||||
- Label with column ID: Identifies the column
|
||||
- Chevron icon: Indicates navigation to details
|
||||
- id with tcolman_ prefix: Required for HTMX swap targeting
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
label = columns_manager.mk_column_label(col_def)
|
||||
|
||||
# Should have the correct ID pattern
|
||||
expected = Div(
|
||||
id=f"tcolman_{columns_manager._id}-name",
|
||||
cls=Contains("flex"),
|
||||
)
|
||||
assert matches(label, expected)
|
||||
|
||||
# Should contain a checkbox
|
||||
checkbox = find_one(label, Input(type="checkbox"))
|
||||
assert checkbox is not None
|
||||
|
||||
# Should contain chevron icon for navigation
|
||||
chevron = find_one(label, TestIcon("chevron_right20_regular"))
|
||||
assert chevron is not None
|
||||
|
||||
def test_column_label_checkbox_is_checked_when_visible(self, columns_manager):
|
||||
"""Test that checkbox is checked when column is visible.
|
||||
|
||||
Why this matters:
|
||||
- checked attribute: Reflects current visibility state
|
||||
- User can see which columns are visible
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.visible is True
|
||||
|
||||
label = columns_manager.mk_column_label(col_def)
|
||||
checkbox = find_one(label, Input(type="checkbox"))
|
||||
|
||||
# Checkbox should have checked attribute
|
||||
assert checkbox.attrs.get("checked") is True
|
||||
|
||||
def test_column_label_checkbox_is_unchecked_when_hidden(self, columns_manager):
|
||||
"""Test that checkbox is unchecked when column is hidden.
|
||||
|
||||
Why this matters:
|
||||
- No checked attribute: Indicates column is hidden
|
||||
- Visual feedback for user
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("email")
|
||||
assert col_def.visible is False
|
||||
|
||||
label = columns_manager.mk_column_label(col_def)
|
||||
checkbox = find_one(label, Input(type="checkbox"))
|
||||
|
||||
# Checkbox should not have checked attribute (or it should be False/None)
|
||||
checked = checkbox.attrs.get("checked")
|
||||
assert checked is None or checked is False
|
||||
|
||||
# =========================================================================
|
||||
# mk_column_details
|
||||
# =========================================================================
|
||||
|
||||
def test_column_details_contains_all_form_fields(self, columns_manager):
|
||||
"""Test that column details form contains all required fields.
|
||||
|
||||
Why these elements matter:
|
||||
- col_id field (readonly): Shows column identifier
|
||||
- title field: Editable column display name
|
||||
- visible checkbox: Toggle visibility
|
||||
- type select: Change column type
|
||||
- width input: Set column width
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
details = columns_manager.mk_column_details(col_def)
|
||||
|
||||
# Should contain Form
|
||||
form = Form()
|
||||
del form.attrs["enctype"]
|
||||
form = find_one(details, form)
|
||||
assert form is not None
|
||||
|
||||
# Should contain all required input fields
|
||||
col_id_input = find_one(form, Input(name="col_id"))
|
||||
assert col_id_input is not None
|
||||
assert col_id_input.attrs.get("readonly") is True
|
||||
|
||||
title_input = find_one(form, Input(name="title"))
|
||||
assert title_input is not None
|
||||
|
||||
visible_checkbox = find_one(form, Input(name="visible", type="checkbox"))
|
||||
assert visible_checkbox is not None
|
||||
|
||||
type_select = find_one(form, Select(name="type"))
|
||||
assert type_select is not None
|
||||
|
||||
width_input = find_one(form, Input(name="width", type="number"))
|
||||
assert width_input is not None
|
||||
|
||||
def test_column_details_has_back_button(self, columns_manager):
|
||||
"""Test that column details has a back button to return to all columns.
|
||||
|
||||
Why this matters:
|
||||
- Back navigation: User can return to column list
|
||||
- Chevron left icon: Visual indicator of back action
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
details = columns_manager.mk_column_details(col_def)
|
||||
|
||||
# Should contain back chevron icon
|
||||
back_icon = find_one(details, TestIcon("chevron_left20_regular", wrapper="span"))
|
||||
assert back_icon is not None
|
||||
|
||||
def test_column_details_form_has_fieldset_with_legend(self, columns_manager):
|
||||
"""Test that column details form has a fieldset with legend.
|
||||
|
||||
Why this matters:
|
||||
- Fieldset groups related fields
|
||||
- Legend provides context ("Column details")
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
details = columns_manager.mk_column_details(col_def)
|
||||
|
||||
fieldset = find_one(details, Fieldset(legend="Column details"))
|
||||
assert fieldset is not None
|
||||
|
||||
# =========================================================================
|
||||
# show_column_details
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_show_column_details_for_existing_column(self, columns_manager):
|
||||
"""Test that show_column_details returns a form-based view for an existing column.
|
||||
|
||||
Why these elements matter:
|
||||
- Form element: show_column_details must return an editable form view
|
||||
- Exactly one form: Ensures the response is unambiguous (not multiple forms)
|
||||
"""
|
||||
result = columns_manager.show_column_details("name")
|
||||
|
||||
expected = Form()
|
||||
del expected.attrs["enctype"]
|
||||
forms = find(result, expected)
|
||||
assert len(forms) == 1, "Should contain exactly one form"
|
||||
|
||||
# =========================================================================
|
||||
# mk_all_columns
|
||||
# =========================================================================
|
||||
|
||||
def test_all_columns_renders_all_column_labels(self, columns_manager):
|
||||
"""Test that all columns render produces labels for all columns.
|
||||
|
||||
Why this matters:
|
||||
- All columns visible in list
|
||||
- Each column has its label rendered
|
||||
"""
|
||||
search = columns_manager.mk_all_columns()
|
||||
rendered = search.render()
|
||||
|
||||
# Should find 3 column labels in the results
|
||||
results_div = find_one(rendered, Div(id=f"{search._id}-results"))
|
||||
assert results_div is not None
|
||||
|
||||
# Each column should have a label with tcolman_ prefix
|
||||
for col_id in ["name", "age", "email"]:
|
||||
label = find_one(results_div, Div(id=f"tcolman_{columns_manager._id}-{col_id}"))
|
||||
assert label is not None, f"Column label for '{col_id}' should be present"
|
||||
@@ -5,44 +5,39 @@ Tests the complete formatting flow: DSL → Storage → Application.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from myfasthtml.controls.DataGrid import DataGrid
|
||||
from controls.conftest import get_data_grid
|
||||
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
|
||||
from myfasthtml.controls.DataGridsManager import DataGridsManager
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridColumnUiState
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
|
||||
from myfasthtml.controls.datagrid_objects import DataGridRowUiState
|
||||
from myfasthtml.core.data.DataServicesManager import DataServicesManager
|
||||
from myfasthtml.core.formatting.dataclasses import FormatRule, Style
|
||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||
from myfasthtml.core.instances import InstancesManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manager(root_instance):
|
||||
"""Create a DataGridsManager instance."""
|
||||
mgr = DataGridsManager(root_instance, _id="test-manager")
|
||||
yield mgr
|
||||
InstancesManager.reset()
|
||||
def datagrid(root_instance):
|
||||
"""Create a DataGrid instance."""
|
||||
df = DataFrame(
|
||||
{"amount": [100, 200, 300],
|
||||
"status": ["active", "inactive", "active"]}
|
||||
)
|
||||
dg = get_data_grid(root_instance, df, table_name="app.products")
|
||||
dg.handle_columns_updates({"amount": {"title": "Amount"},
|
||||
"status": {"title": "Status"}})
|
||||
yield dg
|
||||
|
||||
dsm = InstancesManager.get_by_type(root_instance._session, DataServicesManager)
|
||||
dsm.clear()
|
||||
dgm = dg.get_parent()
|
||||
dgm.get_state().all_tables_formats.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def datagrid(manager):
|
||||
"""Create a DataGrid instance."""
|
||||
from myfasthtml.controls.DataGrid import DatagridConf
|
||||
conf = DatagridConf(namespace="app", name="products")
|
||||
grid = DataGrid(manager, conf=conf, save_state=False, _id="mf-data_grid-test-datagrid")
|
||||
|
||||
# ColumnDefinition, col_ui_state: DataGridColumnUiState
|
||||
# Add some columns
|
||||
grid.columns = [
|
||||
DataGridColumnState(ColumnDefinition(col_id="amount", col_index=0, title="Amount", type=ColumnType.Number),
|
||||
DataGridColumnUiState(col_id="amount", visible=True)),
|
||||
DataGridColumnState(ColumnDefinition(col_id="status", col_index=1, title="Status", type=ColumnType.Text),
|
||||
DataGridColumnUiState(col_id="status", visible=True))
|
||||
]
|
||||
|
||||
yield grid
|
||||
InstancesManager.reset()
|
||||
def datagrids_manager(datagrid):
|
||||
"""Create a DataGridsManager instance."""
|
||||
return datagrid.get_parent()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -65,7 +60,7 @@ class TestFormatRulesHierarchy:
|
||||
column_rules = [FormatRule(style=Style(preset="success"))]
|
||||
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||
|
||||
datagrid._state.cell_formats["tcell_test-datagrid-0-0"] = cell_rules
|
||||
datagrid._state.cell_formats[f"tcell_{datagrid.get_id()}-0-0"] = cell_rules
|
||||
datagrid._state.columns[0].format = column_rules
|
||||
datagrid._state.table_format = table_rules
|
||||
|
||||
@@ -82,7 +77,8 @@ class TestFormatRulesHierarchy:
|
||||
column_rules = [FormatRule(style=Style(preset="success"))]
|
||||
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||
|
||||
datagrid._state.rows[0].format = row_rules
|
||||
datagrid._state.rows.append(DataGridRowUiState(1, format=table_rules)) # this one should be skipped
|
||||
datagrid._state.rows.append(DataGridRowUiState(0, format=row_rules))
|
||||
datagrid._state.columns[0].format = column_rules
|
||||
datagrid._state.table_format = table_rules
|
||||
|
||||
@@ -107,14 +103,14 @@ class TestFormatRulesHierarchy:
|
||||
# Should return column rules
|
||||
assert rules == column_rules
|
||||
|
||||
def test_i_can_get_table_level_rules(self, datagrid, manager):
|
||||
def test_i_can_get_table_level_rules(self, datagrid, datagrids_manager):
|
||||
"""Test that table-level rules have fourth priority."""
|
||||
# Setup rules at different levels
|
||||
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||
tables_rules = [FormatRule(style=Style(preset="neutral"))]
|
||||
|
||||
datagrid._state.table_format = table_rules
|
||||
manager.all_tables_formats = tables_rules
|
||||
datagrids_manager.get_state().all_tables_formats = tables_rules
|
||||
|
||||
# Get rules for cell (no higher level rules)
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
@@ -122,11 +118,11 @@ class TestFormatRulesHierarchy:
|
||||
# Should return table rules
|
||||
assert rules == table_rules
|
||||
|
||||
def test_i_can_get_tables_level_rules(self, datagrid, manager):
|
||||
def test_i_can_get_tables_level_rules(self, datagrid, datagrids_manager):
|
||||
"""Test that tables-level rules have lowest priority."""
|
||||
# Setup global rules
|
||||
tables_rules = [FormatRule(style=Style(preset="neutral"))]
|
||||
manager.all_tables_formats = tables_rules
|
||||
datagrids_manager.get_state().all_tables_formats = tables_rules
|
||||
|
||||
# Get rules for cell (no other rules)
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
@@ -138,25 +134,6 @@ class TestFormatRulesHierarchy:
|
||||
"""Test that None is returned when no rules are defined."""
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
assert rules == []
|
||||
|
||||
@pytest.mark.parametrize("level,setup_func,expected_preset", [
|
||||
("cell", lambda dg: dg._state.cell_formats.__setitem__("tcell_test-datagrid-0-0",
|
||||
[FormatRule(style=Style(preset="error"))]), "error"),
|
||||
("row", lambda dg: setattr(dg._state.rows[0], "format",
|
||||
[FormatRule(style=Style(preset="warning"))]), "warning"),
|
||||
("column", lambda dg: setattr(dg._state.columns[0], "format",
|
||||
[FormatRule(style=Style(preset="success"))]), "success"),
|
||||
("table", lambda dg: setattr(dg._state, "table_format",
|
||||
[FormatRule(style=Style(preset="info"))]), "info"),
|
||||
])
|
||||
def test_hierarchy_priority(self, datagrid, level, setup_func, expected_preset):
|
||||
"""Test that each level has correct priority in the hierarchy."""
|
||||
setup_func(datagrid)
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
|
||||
assert rules is not None
|
||||
assert len(rules) == 1
|
||||
assert rules[0].style.preset == expected_preset
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -194,7 +171,7 @@ table "wrong_name":
|
||||
# Rules should not be applied (wrong table name)
|
||||
assert len(datagrid._state.table_format) == 0
|
||||
|
||||
def test_i_can_dispatch_tables_rules(self, manager, datagrid, editor):
|
||||
def test_i_can_dispatch_tables_rules(self, datagrids_manager, datagrid, editor):
|
||||
"""Test that tables rules are dispatched to DataGridsManager."""
|
||||
|
||||
dsl = '''
|
||||
@@ -206,11 +183,11 @@ tables:
|
||||
editor.on_content_changed()
|
||||
|
||||
# Check that manager.all_tables_formats is populated
|
||||
assert len(manager.all_tables_formats) == 2
|
||||
assert manager.all_tables_formats[0].style.preset == "neutral"
|
||||
assert manager.all_tables_formats[1].formatter.precision == 2
|
||||
assert len(datagrids_manager.get_state().all_tables_formats) == 2
|
||||
assert datagrids_manager.get_state().all_tables_formats[0].style.preset == "neutral"
|
||||
assert datagrids_manager.get_state().all_tables_formats[1].formatter.precision == 2
|
||||
|
||||
def test_i_can_combine_all_scope_types(self, manager, datagrid, editor):
|
||||
def test_i_can_combine_all_scope_types(self, datagrids_manager, datagrid, editor):
|
||||
"""Test that all 5 scope types can be used together."""
|
||||
|
||||
dsl = '''
|
||||
@@ -233,7 +210,7 @@ cell (amount, 1):
|
||||
editor.on_content_changed()
|
||||
|
||||
# Check all levels are populated
|
||||
assert len(manager.all_tables_formats) == 1
|
||||
assert len(datagrids_manager.get_state().all_tables_formats) == 1
|
||||
assert len(datagrid._state.table_format) == 1
|
||||
assert len(datagrid._state.columns[0].format) == 1
|
||||
assert len(datagrid._state.rows[0].format) == 1
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||
from myfasthtml.test.matcher import matches, TestIconNotStr
|
||||
|
||||
|
||||
def test_existing_icon():
|
||||
@@ -13,8 +14,7 @@ def test_dynamic_icon():
|
||||
|
||||
def test_unknown_icon():
|
||||
IconsHelper.reset()
|
||||
assert IconsHelper.get("does_not_exist") is None
|
||||
|
||||
assert matches(IconsHelper.get("does_not_exist"), TestIconNotStr("Question20Regular"))
|
||||
|
||||
|
||||
def test_dynamic_icon_by_package():
|
||||
|
||||
Reference in New Issue
Block a user