Introducing columns formulas
This commit is contained in:
@@ -418,9 +418,41 @@ class DataGrid(MultipleInstance):
|
||||
self._df_store.ns_fast_access = _init_fast_access(self._df)
|
||||
self._df_store.ns_row_data = _init_row_data(self._df)
|
||||
self._df_store.ns_total_rows = len(self._df) if self._df is not None else 0
|
||||
if init_state:
|
||||
self._register_existing_formulas()
|
||||
|
||||
return self
|
||||
|
||||
def _register_existing_formulas(self) -> None:
|
||||
"""
|
||||
Re-register all formula columns with the FormulaEngine.
|
||||
|
||||
Called after data reload to ensure the engine knows about all
|
||||
formula columns and their expressions.
|
||||
"""
|
||||
engine = self._get_formula_engine()
|
||||
if engine is None:
|
||||
return
|
||||
table = self.get_table_name()
|
||||
for col_def in self._state.columns:
|
||||
if col_def.formula:
|
||||
try:
|
||||
engine.set_formula(table, col_def.col_id, col_def.formula)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to register formula for %s.%s: %s", table, col_def.col_id, e)
|
||||
|
||||
def _recalculate_formulas(self) -> None:
|
||||
"""
|
||||
Recalculate dirty formula columns before rendering.
|
||||
|
||||
Called at the start of mk_body_content_page() to ensure formula
|
||||
columns are up-to-date before cells are rendered.
|
||||
"""
|
||||
engine = self._get_formula_engine()
|
||||
if engine is None:
|
||||
return
|
||||
engine.recalculate_if_needed(self.get_table_name(), self._df_store)
|
||||
|
||||
def _get_format_rules(self, col_pos, row_index, col_def):
|
||||
"""
|
||||
Get format rules for a cell, returning only the most specific level defined.
|
||||
@@ -575,6 +607,11 @@ class DataGrid(MultipleInstance):
|
||||
def get_table_name(self):
|
||||
return f"{self._settings.namespace}.{self._settings.name}" if self._settings.namespace else self._settings.name
|
||||
|
||||
def get_formula_engine(self):
|
||||
"""Return the FormulaEngine from the DataGridsManager, if available."""
|
||||
return self._parent.get_formula_engine()
|
||||
|
||||
|
||||
def mk_headers(self):
|
||||
resize_cmd = self.commands.set_column_width()
|
||||
move_cmd = self.commands.move_column()
|
||||
@@ -701,6 +738,7 @@ class DataGrid(MultipleInstance):
|
||||
OPTIMIZED: Extract filter keyword once instead of 10,000 times.
|
||||
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
|
||||
"""
|
||||
self._recalculate_formulas()
|
||||
df = self._get_filtered_df()
|
||||
if df is None:
|
||||
return []
|
||||
|
||||
@@ -99,6 +99,9 @@ class DataGridColumnsManager(MultipleInstance):
|
||||
col_def.type = ColumnType(v)
|
||||
elif k == "width":
|
||||
col_def.width = int(v)
|
||||
elif k == "formula":
|
||||
col_def.formula = v or ""
|
||||
self._register_formula(col_def)
|
||||
else:
|
||||
setattr(col_def, k, v)
|
||||
|
||||
@@ -107,6 +110,21 @@ class DataGridColumnsManager(MultipleInstance):
|
||||
|
||||
return self.mk_all_columns()
|
||||
|
||||
def _register_formula(self, col_def) -> None:
|
||||
"""Register or remove a formula column with the FormulaEngine."""
|
||||
engine = self._parent.get_formula_engine()
|
||||
if engine is None:
|
||||
return
|
||||
table = self._parent.get_table_name()
|
||||
if col_def.formula:
|
||||
try:
|
||||
engine.set_formula(table, col_def.col_id, col_def.formula)
|
||||
logger.debug("Registered formula for %s.%s", table, col_def.col_id)
|
||||
except Exception as e:
|
||||
logger.warning("Formula error for %s.%s: %s", table, col_def.col_id, e)
|
||||
else:
|
||||
engine.remove_formula(table, col_def.col_id)
|
||||
|
||||
def mk_column_label(self, col_def: DataGridColumnState):
|
||||
return Div(
|
||||
mk.mk(
|
||||
@@ -168,6 +186,17 @@ class DataGridColumnsManager(MultipleInstance):
|
||||
value=col_def.title,
|
||||
),
|
||||
|
||||
*([
|
||||
Label("Formula"),
|
||||
Textarea(
|
||||
col_def.formula or "",
|
||||
name="formula",
|
||||
cls=f"textarea textarea-{size} w-full font-mono",
|
||||
placeholder="{Column} * {OtherColumn}",
|
||||
rows=3,
|
||||
),
|
||||
] if col_def.type == ColumnType.Formula else []),
|
||||
|
||||
legend="Column details",
|
||||
cls="fieldset border-base-300 rounded-box"
|
||||
),
|
||||
|
||||
67
src/myfasthtml/controls/DataGridFormulaEditor.py
Normal file
67
src/myfasthtml/controls/DataGridFormulaEditor.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
DataGridFormulaEditor — DslEditor for formula column expressions.
|
||||
|
||||
Extends DslEditor with formula-specific behavior:
|
||||
- Parses the formula on content change
|
||||
- Registers the formula with FormulaEngine
|
||||
- Triggers a body re-render on the parent DataGrid
|
||||
"""
|
||||
import logging
|
||||
|
||||
from myfasthtml.controls.DslEditor import DslEditor
|
||||
from myfasthtml.core.formula.dsl.exceptions import FormulaSyntaxError, FormulaCycleError
|
||||
|
||||
logger = logging.getLogger("DataGridFormulaEditor")
|
||||
|
||||
|
||||
class DataGridFormulaEditor(DslEditor):
|
||||
"""
|
||||
Formula editor for a specific DataGrid column.
|
||||
|
||||
Args:
|
||||
parent: The parent DataGrid instance.
|
||||
col_def: The DataGridColumnState for the formula column.
|
||||
conf: DslEditorConf for CodeMirror configuration.
|
||||
_id: Optional instance ID.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, col_def, conf=None, _id=None):
|
||||
super().__init__(parent, conf=conf, _id=_id)
|
||||
self._col_def = col_def
|
||||
|
||||
def on_content_changed(self):
|
||||
"""
|
||||
Called when the formula text is changed in the editor.
|
||||
|
||||
1. Updates col_def.formula with the new text.
|
||||
2. Registers the formula with the FormulaEngine.
|
||||
3. Triggers a body re-render of the parent DataGrid.
|
||||
"""
|
||||
formula_text = self.get_content()
|
||||
|
||||
# Update the column definition
|
||||
self._col_def.formula = formula_text or ""
|
||||
|
||||
# Register with the FormulaEngine
|
||||
engine = self._parent._get_formula_engine()
|
||||
if engine is not None:
|
||||
table = self._parent.get_table_name()
|
||||
try:
|
||||
engine.set_formula(table, self._col_def.col_id, formula_text)
|
||||
logger.debug(
|
||||
"Formula updated for %s.%s: %s",
|
||||
table, self._col_def.col_id, formula_text,
|
||||
)
|
||||
except FormulaSyntaxError as e:
|
||||
logger.debug("Formula syntax error, keeping old formula: %s", e)
|
||||
return
|
||||
except FormulaCycleError as e:
|
||||
logger.warning("Formula cycle detected for %s.%s: %s", table, self._col_def.col_id, e)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning("Formula engine error for %s.%s: %s", table, self._col_def.col_id, e)
|
||||
return
|
||||
|
||||
# Save state and re-render the grid body
|
||||
self._parent.save_state()
|
||||
return self._parent.render_partial("body")
|
||||
@@ -12,7 +12,7 @@ from myfasthtml.icons.fluent import brain_circuit20_regular
|
||||
from myfasthtml.icons.fluent_p1 import filter20_regular, search20_regular
|
||||
from myfasthtml.icons.fluent_p2 import dismiss_circle20_regular
|
||||
|
||||
logger = logging.getLogger("DataGridFilter")
|
||||
logger = logging.getLogger("DataGridQuery")
|
||||
|
||||
DG_QUERY_FILTER = "filter"
|
||||
DG_QUERY_SEARCH = "search"
|
||||
|
||||
@@ -16,6 +16,7 @@ from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider
|
||||
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS, DEFAULT_FORMATTER_PRESETS
|
||||
from myfasthtml.core.formula.engine import FormulaEngine
|
||||
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
|
||||
@@ -91,6 +92,11 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
||||
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
||||
self.all_tables_formats: list = []
|
||||
|
||||
# Formula engine shared across all DataGrids in this session
|
||||
self._formula_engine = FormulaEngine(
|
||||
registry_resolver=self._resolve_store_for_table
|
||||
)
|
||||
|
||||
def upload_from_source(self):
|
||||
file_upload = FileUpload(self)
|
||||
@@ -167,10 +173,10 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
|
||||
def list_column_values(self, table_name, column_name):
|
||||
return self._registry.get_column_values(table_name, column_name)
|
||||
|
||||
|
||||
def get_row_count(self, table_name):
|
||||
return self._registry.get_row_count(table_name)
|
||||
|
||||
|
||||
def get_column_type(self, table_name, column_name):
|
||||
return self._registry.get_column_type(table_name, column_name)
|
||||
|
||||
@@ -180,7 +186,29 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
def list_format_presets(self) -> list[str]:
|
||||
return list(self.formatter_presets.keys())
|
||||
|
||||
# === Presets Management ===
|
||||
def _resolve_store_for_table(self, table_name: str):
|
||||
"""
|
||||
Resolve the DatagridStore for a given table name.
|
||||
|
||||
Used by FormulaEngine as the registry_resolver callback.
|
||||
|
||||
Args:
|
||||
table_name: Full table name in ``"namespace.name"`` format.
|
||||
|
||||
Returns:
|
||||
DatagridStore instance or None if not found.
|
||||
"""
|
||||
try:
|
||||
as_fullname_dict = self._registry._get_entries_as_full_name_dict()
|
||||
grid_id = as_fullname_dict.get(table_name)
|
||||
if grid_id is None:
|
||||
return None
|
||||
datagrid = InstancesManager.get(self._session, grid_id, None)
|
||||
if datagrid is None:
|
||||
return None
|
||||
return datagrid._df_store
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_style_presets(self) -> dict:
|
||||
"""Get the global style presets."""
|
||||
@@ -190,6 +218,10 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
"""Get the global formatter presets."""
|
||||
return self.formatter_presets
|
||||
|
||||
def get_formula_engine(self) -> FormulaEngine:
|
||||
"""The FormulaEngine shared across all DataGrids in this session."""
|
||||
return self._formula_engine
|
||||
|
||||
def add_style_preset(self, name: str, preset: dict):
|
||||
"""
|
||||
Add or update a style preset.
|
||||
|
||||
@@ -20,6 +20,7 @@ class DataGridColumnState:
|
||||
visible: bool = True
|
||||
width: int = DATAGRID_DEFAULT_COLUMN_WIDTH
|
||||
format: list = field(default_factory=list) #
|
||||
formula: str = "" # formula expression for ColumnType.Formula columns
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
Reference in New Issue
Block a user