I can open an excel file and see its content
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import pandas as pd
|
||||
from fastcore.basics import NotStr
|
||||
from fasthtml.components import Div
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
@@ -23,13 +24,12 @@ class Commands(BaseCommands):
|
||||
"New grid",
|
||||
self._owner.new_grid)
|
||||
|
||||
def open_from_excel(self, tab_id, get_content_callback):
|
||||
excel_content = get_content_callback()
|
||||
def open_from_excel(self, tab_id, file_upload):
|
||||
return Command("OpenFromExcel",
|
||||
"Open from Excel",
|
||||
self._owner.open_from_excel,
|
||||
tab_id,
|
||||
excel_content).htmx(target=None)
|
||||
file_upload).htmx(target=None)
|
||||
|
||||
|
||||
class DataGridsManager(MultipleInstance):
|
||||
@@ -42,13 +42,14 @@ class DataGridsManager(MultipleInstance):
|
||||
def upload_from_source(self):
|
||||
file_upload = FileUpload(self)
|
||||
tab_id = self._tabs_manager.create_tab("Upload Datagrid", file_upload)
|
||||
file_upload.on_ok = self.commands.open_from_excel(tab_id, file_upload.get_content)
|
||||
file_upload.set_on_ok(self.commands.open_from_excel(tab_id, file_upload))
|
||||
return self._tabs_manager.show_tab(tab_id)
|
||||
|
||||
def open_from_excel(self, tab_id, excel_content):
|
||||
def open_from_excel(self, tab_id, file_upload):
|
||||
excel_content = file_upload.get_content()
|
||||
df = pd.read_excel(excel_content)
|
||||
content = df.to_html(index=False)
|
||||
self._tabs_manager.switch(tab_id, content)
|
||||
return self._tabs_manager.change_tab_content(tab_id, file_upload.get_file_name(), NotStr(content))
|
||||
|
||||
def mk_main_icons(self):
|
||||
return Div(
|
||||
|
||||
@@ -25,6 +25,8 @@ class FileUploadState(DbObject):
|
||||
self.ns_sheets_names: list | None = None
|
||||
self.ns_selected_sheet_name: str | None = None
|
||||
self.ns_file_content: bytes | None = None
|
||||
self.ns_on_ok = None
|
||||
self.ns_on_cancel = None
|
||||
|
||||
|
||||
class Commands(BaseCommands):
|
||||
@@ -49,11 +51,16 @@ class FileUpload(MultipleInstance):
|
||||
super().__init__(parent, _id=_id, **kwargs)
|
||||
self.commands = Commands(self)
|
||||
self._state = FileUploadState(self)
|
||||
self._state.ns_on_ok = None
|
||||
|
||||
def set_on_ok(self, callback):
|
||||
self._state.ns_on_ok = callback
|
||||
|
||||
def upload_file(self, file: UploadFile):
|
||||
logger.debug(f"upload_file: {file=}")
|
||||
if file:
|
||||
self._state.ns_file_content = file.file.read()
|
||||
self._state.ns_file_name = file.filename
|
||||
self._state.ns_sheets_names = self.get_sheets_names(self._state.ns_file_content)
|
||||
self._state.ns_selected_sheet_name = self._state.ns_sheets_names[0] if len(self._state.ns_sheets_names) > 0 else 0
|
||||
|
||||
@@ -76,6 +83,8 @@ class FileUpload(MultipleInstance):
|
||||
def get_content(self):
|
||||
return self._state.ns_file_content
|
||||
|
||||
def get_file_name(self):
|
||||
return self._state.ns_file_name
|
||||
|
||||
@staticmethod
|
||||
def get_sheets_names(file_content):
|
||||
@@ -104,7 +113,7 @@ class FileUpload(MultipleInstance):
|
||||
self.mk_sheet_selector(),
|
||||
cls="flex"
|
||||
),
|
||||
mk.dialog_buttons(),
|
||||
mk.dialog_buttons(on_ok=self._state.ns_on_ok, on_cancel=self._state.ns_on_cancel),
|
||||
)
|
||||
|
||||
def __ft__(self):
|
||||
|
||||
@@ -164,13 +164,14 @@ class TabsManager(MultipleInstance):
|
||||
self._add_or_update_tab(tab_id, label, component, activate)
|
||||
return tab_id
|
||||
|
||||
def show_tab(self, tab_id, activate: bool = True, oob=True):
|
||||
def show_tab(self, tab_id, activate: bool = True, oob=True, is_new=True):
|
||||
"""
|
||||
Send the tab to the client if needed.
|
||||
If the tab was already sent, just update the active tab.
|
||||
:param tab_id:
|
||||
:param activate:
|
||||
:param oob: default=True so other control will not care of the target
|
||||
:param is_new: is it a new tab or an existing one?
|
||||
:return:
|
||||
"""
|
||||
logger.debug(f"show_tab {tab_id=}")
|
||||
@@ -190,7 +191,9 @@ class TabsManager(MultipleInstance):
|
||||
logger.debug(f" Content not in client memory. Sending it.")
|
||||
self._state.ns_tabs_sent_to_client.add(tab_id)
|
||||
tab_content = self._mk_tab_content(tab_id, content)
|
||||
return self._mk_tabs_controller(oob), self._mk_tabs_header_wrapper(), self._wrap_tab_content(tab_content)
|
||||
return (self._mk_tabs_controller(oob),
|
||||
self._mk_tabs_header_wrapper(),
|
||||
self._wrap_tab_content(tab_content, is_new))
|
||||
else:
|
||||
logger.debug(f" Content already in client memory. Just switch.")
|
||||
return self._mk_tabs_controller(oob) # no new tab_id => header is already up to date
|
||||
@@ -204,7 +207,7 @@ class TabsManager(MultipleInstance):
|
||||
|
||||
self._add_or_update_tab(tab_id, label, component, activate)
|
||||
self._state.ns_tabs_sent_to_client.discard(tab_id) # to make sure that the new content will be sent to the client
|
||||
return self.show_tab(tab_id, activate=activate, oob=True)
|
||||
return self.show_tab(tab_id, activate=activate, oob=True, is_new=False)
|
||||
|
||||
def close_tab(self, tab_id: str):
|
||||
"""
|
||||
@@ -359,11 +362,15 @@ class TabsManager(MultipleInstance):
|
||||
cls="dropdown dropdown-end"
|
||||
)
|
||||
|
||||
def _wrap_tab_content(self, tab_content):
|
||||
return Div(
|
||||
tab_content,
|
||||
hx_swap_oob=f"beforeend:#{self._id}-content-wrapper",
|
||||
)
|
||||
def _wrap_tab_content(self, tab_content, is_new=True):
|
||||
if is_new:
|
||||
return Div(
|
||||
tab_content,
|
||||
hx_swap_oob=f"beforeend:#{self._id}-content-wrapper"
|
||||
)
|
||||
else:
|
||||
tab_content.attrs["hx-swap-oob"] = "outerHTML"
|
||||
return tab_content
|
||||
|
||||
def _tab_already_exists(self, label, component):
|
||||
if not isinstance(component, BaseInstance):
|
||||
|
||||
@@ -25,7 +25,7 @@ class BaseCommand:
|
||||
:type description: str
|
||||
"""
|
||||
|
||||
def __init__(self, name, description):
|
||||
def __init__(self, name, description, auto_register=True):
|
||||
self.id = uuid.uuid4()
|
||||
self.name = name
|
||||
self.description = description
|
||||
@@ -34,7 +34,8 @@ class BaseCommand:
|
||||
self._ft = None
|
||||
|
||||
# register the command
|
||||
CommandsManager.register(self)
|
||||
if auto_register:
|
||||
CommandsManager.register(self)
|
||||
|
||||
def get_htmx_params(self):
|
||||
return {
|
||||
@@ -139,7 +140,7 @@ class Command(BaseCommand):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def _convert(self, key, value):
|
||||
def _cast_parameter(self, key, value):
|
||||
if key in self.callback_parameters:
|
||||
param = self.callback_parameters[key]
|
||||
if param.annotation == bool:
|
||||
@@ -174,7 +175,7 @@ class Command(BaseCommand):
|
||||
if client_response:
|
||||
for k, v in client_response.items():
|
||||
if k in self.callback_parameters:
|
||||
new_kwargs[k] = self._convert(k, v)
|
||||
new_kwargs[k] = self._cast_parameter(k, v)
|
||||
if 'client_response' in self.callback_parameters:
|
||||
new_kwargs['client_response'] = client_response
|
||||
|
||||
@@ -186,7 +187,9 @@ class Command(BaseCommand):
|
||||
# Set the hx-swap-oob attribute on all elements returned by the callback
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret[1:]:
|
||||
if hasattr(r, 'attrs') and r.get("id", None) is not None:
|
||||
if (hasattr(r, 'attrs')
|
||||
and "hx-swap-oob" not in r.attrs
|
||||
and r.get("id", None) is not None):
|
||||
r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true")
|
||||
|
||||
if not ret_from_bindings:
|
||||
|
||||
@@ -700,6 +700,65 @@ class TestTabsManagerRender:
|
||||
expected = Div(cls=Contains("hidden"))
|
||||
assert matches(tab_content, expected)
|
||||
|
||||
def test_i_can_show_a_new_content(self, tabs_manager):
|
||||
"""Test that TabsManager.show_tab() send the correct div to the client"""
|
||||
tab_id = tabs_manager.create_tab("Tab1", Div("My Content"))
|
||||
actual = tabs_manager.show_tab(tab_id)
|
||||
|
||||
expected = (
|
||||
Div(data_active_tab=tab_id,
|
||||
hx_on__after_settle=f'updateTabs("{tabs_manager.get_id()}-controller");',
|
||||
hx_swap_oob="true"), # the controller is correctly updated
|
||||
Div(
|
||||
id=f'{tabs_manager.get_id()}-header-wrapper'
|
||||
), # content of the header
|
||||
Div(
|
||||
Div(Div("My Content")),
|
||||
hx_swap_oob=f"beforeend:#{tabs_manager.get_id()}-content-wrapper", # hx_swap_oob="beforeend:" important !
|
||||
), # content + where to put it
|
||||
)
|
||||
assert matches(actual, expected)
|
||||
|
||||
def test_i_can_show_content_after_switch(self, tabs_manager):
|
||||
tab_id = tabs_manager.create_tab("Tab1", Div("My Content"))
|
||||
tabs_manager.show_tab(tab_id) # first time, send everything
|
||||
actual = tabs_manager.show_tab(tab_id) # second time, send only the controller
|
||||
|
||||
expected = Div(data_active_tab=tab_id,
|
||||
hx_on__after_settle=f'updateTabs("{tabs_manager.get_id()}-controller");',
|
||||
hx_swap_oob="true")
|
||||
|
||||
assert matches(actual, expected)
|
||||
|
||||
def test_i_can_close_a_tab(self, tabs_manager):
|
||||
tab_id = tabs_manager.create_tab("Tab1", Div("My Content"))
|
||||
tabs_manager.show_tab(tab_id) # was sent
|
||||
actual = tabs_manager.close_tab(tab_id)
|
||||
|
||||
expected = (
|
||||
Div(id=f'{tabs_manager.get_id()}-controller'),
|
||||
Div(id=f'{tabs_manager.get_id()}-header-wrapper'),
|
||||
Div(id=f'{tabs_manager.get_id()}-{tab_id}-content', hx_swap_oob="delete") # hx_swap_oob="delete" important !
|
||||
)
|
||||
|
||||
assert matches(actual, expected)
|
||||
|
||||
def test_i_can_change_content(self, tabs_manager):
|
||||
tab_id = tabs_manager.create_tab("Tab1", Div("My Content"))
|
||||
tabs_manager.show_tab(tab_id)
|
||||
actual = tabs_manager.change_tab_content(tab_id, "New Label", Div("New Content"))
|
||||
|
||||
expected = (
|
||||
Div(data_active_tab=tab_id, hx_swap_oob="true"),
|
||||
Div(id=f'{tabs_manager.get_id()}-header-wrapper'),
|
||||
Div(
|
||||
Div("New Content"),
|
||||
id=f'{tabs_manager.get_id()}-{tab_id}-content',
|
||||
hx_swap_oob="outerHTML", # hx_swap_oob="true" important !
|
||||
),
|
||||
)
|
||||
assert matches(actual, expected)
|
||||
|
||||
# =========================================================================
|
||||
# Complete Render
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user