diff --git a/app/callbacks.py b/app/callbacks.py index 52338cd..f8bff18 100644 --- a/app/callbacks.py +++ b/app/callbacks.py @@ -142,19 +142,19 @@ def process_bgc_class(bgc_class: tuple[str, ...] | None) -> list[str]: @app.callback( [ Output("gm-tab", "disabled"), - Output("gm-accordion-control", "disabled"), + Output("gm-filter-accordion-control", "disabled"), + Output("gm-filter-blocks-id", "data", allow_duplicate=True), + Output("gm-filter-blocks-container", "children", allow_duplicate=True), Output("gm-table-card-header", "style"), Output("gm-table-card-body", "style", allow_duplicate=True), Output("mg-tab", "disabled"), - Output("blocks-id", "data", allow_duplicate=True), - Output("blocks-container", "children", allow_duplicate=True), ], [Input("file-store", "data")], prevent_initial_call=True, ) def disable_tabs_and_reset_blocks( file_path: Path | str | None, -) -> tuple[bool, bool, dict, dict[str, str], bool, list[str], list[dmc.Grid]]: +) -> tuple[bool, bool, list[str], list[dmc.Grid], dict, dict[str, str], bool]: """Manage tab states and reset blocks based on file upload status. Args: @@ -165,16 +165,24 @@ def disable_tabs_and_reset_blocks( """ if file_path is None: # Disable the tabs, don't change blocks - return True, True, {}, {"display": "block"}, True, [], [] + return True, True, [], [], {}, {"display": "block"}, True # Enable the tabs and reset blocks - initial_block_id = [str(uuid.uuid4())] - new_blocks = [create_initial_block(initial_block_id[0])] - - return False, False, {}, {"display": "block"}, False, initial_block_id, new_blocks + gm_filter_initial_block_id = [str(uuid.uuid4())] + gm_filter_new_blocks = [gm_filter_create_initial_block(gm_filter_initial_block_id[0])] + + return ( + False, + False, + gm_filter_initial_block_id, + gm_filter_new_blocks, + {}, + {"display": "block"}, + False, + ) -def create_initial_block(block_id: str) -> dmc.Grid: +def gm_filter_create_initial_block(block_id: str) -> dmc.Grid: """Create the initial block component with the given ID. Args: @@ -184,12 +192,12 @@ def create_initial_block(block_id: str) -> dmc.Grid: A Grid component with nested elements. """ return dmc.Grid( - id={"type": "gm-block", "index": block_id}, + id={"type": "gm-filter-block", "index": block_id}, children=[ dmc.GridCol( dbc.Button( [html.I(className="fas fa-plus")], - id={"type": "gm-add-button", "index": block_id}, + id={"type": "gm-filter-add-button", "index": block_id}, className="btn-primary", ), span=2, @@ -198,7 +206,7 @@ def create_initial_block(block_id: str) -> dmc.Grid: dcc.Dropdown( options=GM_DROPDOWN_MENU_OPTIONS, value="GCF_ID", - id={"type": "gm-dropdown-menu", "index": block_id}, + id={"type": "gm-filter-dropdown-menu", "index": block_id}, clearable=False, ), span=4, @@ -206,12 +214,12 @@ def create_initial_block(block_id: str) -> dmc.Grid: dmc.GridCol( [ dmc.TextInput( - id={"type": "gm-dropdown-ids-text-input", "index": block_id}, + id={"type": "gm-filter-dropdown-ids-text-input", "index": block_id}, placeholder="1, 2, 3, ...", className="custom-textinput", ), dcc.Dropdown( - id={"type": "gm-dropdown-bgc-class-dropdown", "index": block_id}, + id={"type": "gm-filter-dropdown-bgc-class-dropdown", "index": block_id}, options=GM_DROPDOWN_BGC_CLASS_OPTIONS, multi=True, style={"display": "none"}, @@ -227,7 +235,7 @@ def create_initial_block(block_id: str) -> dmc.Grid: @app.callback( Output("gm-graph", "figure"), Output("gm-graph", "style"), - Output("file-content-mg", "children"), + Output("mg-file-content", "children"), [Input("processed-data-store", "data")], ) def gm_plot(stored_data: str | None) -> tuple[dict | go.Figure, dict, str]: @@ -273,11 +281,11 @@ def gm_plot(stored_data: str | None) -> tuple[dict | go.Figure, dict, str]: @app.callback( - Output("blocks-id", "data"), - Input({"type": "gm-add-button", "index": ALL}, "n_clicks"), - State("blocks-id", "data"), + Output("gm-filter-blocks-id", "data"), + Input({"type": "gm-filter-add-button", "index": ALL}, "n_clicks"), + State("gm-filter-blocks-id", "data"), ) -def add_block(n_clicks: list[int], blocks_id: list[str]) -> list[str]: +def gm_filter_add_block(n_clicks: list[int], blocks_id: list[str]) -> list[str]: """Add a new block to the layout when the add button is clicked. Args: @@ -296,11 +304,13 @@ def add_block(n_clicks: list[int], blocks_id: list[str]) -> list[str]: @app.callback( - Output("blocks-container", "children"), - Input("blocks-id", "data"), - State("blocks-container", "children"), + Output("gm-filter-blocks-container", "children"), + Input("gm-filter-blocks-id", "data"), + State("gm-filter-blocks-container", "children"), ) -def display_blocks(blocks_id: list[str], existing_blocks: list[dmc.Grid]) -> list[dmc.Grid]: +def gm_filter_display_blocks( + blocks_id: list[str], existing_blocks: list[dmc.Grid] +) -> list[dmc.Grid]: """Display the blocks for the input block IDs. Args: @@ -314,19 +324,19 @@ def display_blocks(blocks_id: list[str], existing_blocks: list[dmc.Grid]) -> lis new_block_id = blocks_id[-1] new_block = dmc.Grid( - id={"type": "gm-block", "index": new_block_id}, + id={"type": "gm-filter-block", "index": new_block_id}, children=[ dmc.GridCol( html.Div( [ dbc.Button( [html.I(className="fas fa-plus")], - id={"type": "gm-add-button", "index": new_block_id}, + id={"type": "gm-filter-add-button", "index": new_block_id}, className="btn-primary", ), html.Label( "OR", - id={"type": "gm-or-label", "index": new_block_id}, + id={"type": "gm-filter-or-label", "index": new_block_id}, className="ms-2 px-2 py-1 rounded", style={ "color": "green", @@ -347,7 +357,7 @@ def display_blocks(blocks_id: list[str], existing_blocks: list[dmc.Grid]) -> lis dcc.Dropdown( options=GM_DROPDOWN_MENU_OPTIONS, value="GCF_ID", - id={"type": "gm-dropdown-menu", "index": new_block_id}, + id={"type": "gm-filter-dropdown-menu", "index": new_block_id}, clearable=False, ), span=4, @@ -355,12 +365,15 @@ def display_blocks(blocks_id: list[str], existing_blocks: list[dmc.Grid]) -> lis dmc.GridCol( [ dmc.TextInput( - id={"type": "gm-dropdown-ids-text-input", "index": new_block_id}, + id={"type": "gm-filter-dropdown-ids-text-input", "index": new_block_id}, placeholder="1, 2, 3, ...", className="custom-textinput", ), dcc.Dropdown( - id={"type": "gm-dropdown-bgc-class-dropdown", "index": new_block_id}, + id={ + "type": "gm-filter-dropdown-bgc-class-dropdown", + "index": new_block_id, + }, options=GM_DROPDOWN_BGC_CLASS_OPTIONS, multi=True, style={"display": "none"}, @@ -387,15 +400,15 @@ def display_blocks(blocks_id: list[str], existing_blocks: list[dmc.Grid]) -> lis @app.callback( - Output({"type": "gm-dropdown-ids-text-input", "index": MATCH}, "style"), - Output({"type": "gm-dropdown-bgc-class-dropdown", "index": MATCH}, "style"), - Output({"type": "gm-dropdown-ids-text-input", "index": MATCH}, "placeholder"), - Output({"type": "gm-dropdown-bgc-class-dropdown", "index": MATCH}, "placeholder"), - Output({"type": "gm-dropdown-ids-text-input", "index": MATCH}, "value"), - Output({"type": "gm-dropdown-bgc-class-dropdown", "index": MATCH}, "value"), - Input({"type": "gm-dropdown-menu", "index": MATCH}, "value"), + Output({"type": "gm-filter-dropdown-ids-text-input", "index": MATCH}, "style"), + Output({"type": "gm-filter-dropdown-bgc-class-dropdown", "index": MATCH}, "style"), + Output({"type": "gm-filter-dropdown-ids-text-input", "index": MATCH}, "placeholder"), + Output({"type": "gm-filter-dropdown-bgc-class-dropdown", "index": MATCH}, "placeholder"), + Output({"type": "gm-filter-dropdown-ids-text-input", "index": MATCH}, "value"), + Output({"type": "gm-filter-dropdown-bgc-class-dropdown", "index": MATCH}, "value"), + Input({"type": "gm-filter-dropdown-menu", "index": MATCH}, "value"), ) -def update_placeholder( +def gm_filter_update_placeholder( selected_value: str, ) -> tuple[dict[str, str], dict[str, str], str, str, str, list[Any]]: """Update the placeholder text and style of input fields based on the dropdown selection. @@ -425,7 +438,7 @@ def update_placeholder( return {"display": "none"}, {"display": "none"}, "", "", "", [] -def apply_filters( +def gm_filter_apply( df: pd.DataFrame, dropdown_menus: list[str], text_inputs: list[str], @@ -470,16 +483,16 @@ def apply_filters( Output("gm-table", "tooltip_data"), Output("gm-table-card-body", "style"), Output("gm-table", "selected_rows", allow_duplicate=True), - Output("select-all-checkbox", "value"), + Output("gm-table-select-all-checkbox", "value"), Input("processed-data-store", "data"), - Input("apply-filters-button", "n_clicks"), - State({"type": "gm-dropdown-menu", "index": ALL}, "value"), - State({"type": "gm-dropdown-ids-text-input", "index": ALL}, "value"), - State({"type": "gm-dropdown-bgc-class-dropdown", "index": ALL}, "value"), - State("select-all-checkbox", "value"), + Input("gm-filter-apply-button", "n_clicks"), + State({"type": "gm-filter-dropdown-menu", "index": ALL}, "value"), + State({"type": "gm-filter-dropdown-ids-text-input", "index": ALL}, "value"), + State({"type": "gm-filter-dropdown-bgc-class-dropdown", "index": ALL}, "value"), + State("gm-table-select-all-checkbox", "value"), prevent_initial_call=True, ) -def update_datatable( +def gm_table_update_datatable( processed_data: str | None, n_clicks: int | None, dropdown_menus: list[str], @@ -509,9 +522,9 @@ def update_datatable( except (json.JSONDecodeError, KeyError, pd.errors.EmptyDataError): return [], [], [], {"display": "none"}, [], [] - if ctx.triggered_id == "apply-filters-button": + if ctx.triggered_id == "gm-filter-apply-button": # Apply filters only when the button is clicked - filtered_df = apply_filters(df, dropdown_menus, text_inputs, bgc_class_dropdowns) + filtered_df = gm_filter_apply(df, dropdown_menus, text_inputs, bgc_class_dropdowns) # Reset the checkbox when filters are applied new_checkbox_value = [] else: @@ -550,12 +563,12 @@ def update_datatable( @app.callback( Output("gm-table", "selected_rows", allow_duplicate=True), - Input("select-all-checkbox", "value"), + Input("gm-table-select-all-checkbox", "value"), State("gm-table", "data"), State("gm-table", "derived_virtual_data"), prevent_initial_call=True, ) -def toggle_selection( +def gm_table_toggle_selection( value: list | None, original_rows: list, filtered_rows: list | None, @@ -588,7 +601,9 @@ def toggle_selection( Input("gm-table", "derived_virtual_data"), Input("gm-table", "derived_virtual_selected_rows"), ) -def select_rows(rows: list[dict[str, Any]], selected_rows: list[int] | None) -> tuple[str, str]: +def gm_table_select_rows( + rows: list[dict[str, Any]], selected_rows: list[int] | None +) -> tuple[str, str]: """Display the total number of rows and the number of selected rows in the table. Args: diff --git a/app/layouts.py b/app/layouts.py index 394cee9..5d31ad0 100644 --- a/app/layouts.py +++ b/app/layouts.py @@ -73,35 +73,34 @@ # ------------------ Tabs ------------------ # -# dropdown menu items -initial_block_id = str(uuid.uuid4()) -gm_input_group = html.Div( +# gm filter dropdown menu items +gm_filter_input_group = html.Div( [ - dcc.Store(id="blocks-id", data=[]), # Start with one block + dcc.Store(id="gm-filter-blocks-id", data=[]), # Start with one block html.Div( - id="blocks-container", + id="gm-filter-blocks-container", children=[], ), ] ) -# gm accordion (filter) card -gm_accordion = dmc.Accordion( +# gm filter (accordion) card +gm_filter_accordion = dmc.Accordion( [ dmc.AccordionItem( [ dmc.AccordionControl( "Genomics filter", disabled=True, - id="gm-accordion-control", + id="gm-filter-accordion-control", className="mt-5 mb-3", ), dmc.AccordionPanel( [ - gm_input_group, + gm_filter_input_group, html.Div( dbc.Button( "Apply Filters", - id="apply-filters-button", + id="gm-filter-apply-button", color="primary", className="mt-3", ), @@ -110,7 +109,7 @@ ] ), ], - value="gm-accordion", + value="gm-filter-accordion", ), ], className="mt-5 mb-3", @@ -132,7 +131,7 @@ html.Div( dcc.Checklist( options=[{"label": "", "value": "disabled"}], - id="select-all-checkbox", + id="gm-table-select-all-checkbox", style={ "position": "absolute", "top": "4px", @@ -204,7 +203,7 @@ # gm tab content gm_content = dbc.Row( [ - dbc.Col(gm_accordion, width=10, className="mx-auto dbc"), + dbc.Col(gm_filter_accordion, width=10, className="mx-auto dbc"), dbc.Col(gm_graph, width=10, className="mx-auto"), dbc.Col(gm_table, width=10, className="mx-auto"), ] @@ -213,7 +212,7 @@ mg_content = dbc.Row( dbc.Col( dbc.Card( - dbc.CardBody([html.Div(id="file-content-mg")]), + dbc.CardBody([html.Div(id="mg-file-content")]), ) ), ) diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 45a2a57..4fd763c 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -7,13 +7,13 @@ import pandas as pd import pytest from dash_uploader import UploadStatus -from app.callbacks import add_block -from app.callbacks import apply_filters from app.callbacks import disable_tabs_and_reset_blocks +from app.callbacks import gm_filter_add_block +from app.callbacks import gm_filter_apply +from app.callbacks import gm_table_select_rows +from app.callbacks import gm_table_toggle_selection +from app.callbacks import gm_table_update_datatable from app.callbacks import process_uploaded_data -from app.callbacks import select_rows -from app.callbacks import toggle_selection -from app.callbacks import update_datatable from app.callbacks import upload_data from . import DATA_DIR @@ -134,7 +134,7 @@ def test_process_uploaded_data_structure(): def test_disable_tabs(mock_uuid): # Test with None as input result = disable_tabs_and_reset_blocks(None) - assert result == (True, True, {}, {"display": "block"}, True, [], []) + assert result == (True, True, [], [], {}, {"display": "block"}, True) # Test with a string as input result = disable_tabs_and_reset_blocks(MOCK_FILE_PATH) @@ -143,11 +143,11 @@ def test_disable_tabs(mock_uuid): ( gm_tab_disabled, gm_accordion_disabled, + block_ids, + blocks, table_header_style, table_body_style, mg_tab_disabled, - block_ids, - blocks, ) = result assert gm_tab_disabled is False @@ -172,41 +172,41 @@ def test_disable_tabs(mock_uuid): ), # three buttons, each clicked once ], ) -def test_add_block(mock_uuid, n_clicks, initial_blocks, expected_result): +def test_gm_filter_add_block(mock_uuid, n_clicks, initial_blocks, expected_result): if isinstance(expected_result, list): - result = add_block(n_clicks, initial_blocks) + result = gm_filter_add_block(n_clicks, initial_blocks) assert result == expected_result else: with expected_result: - add_block(n_clicks, initial_blocks) + gm_filter_add_block(n_clicks, initial_blocks) -def test_apply_filters(sample_processed_data): +def test_gm_filter_apply(sample_processed_data): data = json.loads(sample_processed_data) df = pd.DataFrame(data["gcf_data"]) # Test GCF_ID filter gcf_ids = df["GCF ID"].iloc[:2].tolist() - filtered_df = apply_filters(df, ["GCF_ID"], [",".join(gcf_ids)], [[]]) + filtered_df = gm_filter_apply(df, ["GCF_ID"], [",".join(gcf_ids)], [[]]) assert len(filtered_df) == 2 assert set(filtered_df["GCF ID"]) == set(gcf_ids) # Test BGC_CLASS filter bgc_class = df["BGC Classes"].iloc[0][0] # Get the first BGC class from the first row - filtered_df = apply_filters(df, ["BGC_CLASS"], [""], [[bgc_class]]) + filtered_df = gm_filter_apply(df, ["BGC_CLASS"], [""], [[bgc_class]]) assert len(filtered_df) > 0 assert all(bgc_class in classes for classes in filtered_df["BGC Classes"]) # Test no filter - filtered_df = apply_filters(df, [], [], []) + filtered_df = gm_filter_apply(df, [], [], []) assert len(filtered_df) == len(df) -def test_update_datatable(sample_processed_data): +def test_gm_table_update_datatable(sample_processed_data): with patch("app.callbacks.ctx") as mock_ctx: # Test with processed data and no filters applied mock_ctx.triggered_id = None - result = update_datatable( + result = gm_table_update_datatable( sample_processed_data, None, # n_clicks [], # dropdown_menus @@ -238,12 +238,12 @@ def test_update_datatable(sample_processed_data): assert checkbox_value == [] # Test with None input - result = update_datatable(None, None, [], [], [], None) + result = gm_table_update_datatable(None, None, [], [], [], None) assert result == ([], [], [], {"display": "none"}, [], []) # Test with apply-filters-button triggered - mock_ctx.triggered_id = "apply-filters-button" - result = update_datatable( + mock_ctx.triggered_id = "gm-filter-apply-button" + result = gm_table_update_datatable( sample_processed_data, 1, # n_clicks ["GCF_ID"], # dropdown_menus @@ -258,38 +258,38 @@ def test_update_datatable(sample_processed_data): assert checkbox_value == [] -def test_toggle_selection(sample_processed_data): +def test_gm_table_toggle_selection(sample_processed_data): data = json.loads(sample_processed_data) original_rows = data["gcf_data"] filtered_rows = original_rows[:2] # Test selecting all rows - result = toggle_selection(["disabled"], original_rows, filtered_rows) + result = gm_table_toggle_selection(["disabled"], original_rows, filtered_rows) assert result == [0, 1] # Assuming it now returns a list of indices directly # Test deselecting all rows - result = toggle_selection([], original_rows, filtered_rows) + result = gm_table_toggle_selection([], original_rows, filtered_rows) assert result == [] # Test with None filtered_rows - result = toggle_selection(["disabled"], original_rows, None) + result = gm_table_toggle_selection(["disabled"], original_rows, None) assert result == list(range(len(original_rows))) # Should select all rows in original_rows # Test with empty value (deselecting when no filter is applied) - result = toggle_selection([], original_rows, None) + result = gm_table_toggle_selection([], original_rows, None) assert result == [] -def test_select_rows(sample_processed_data): +def test_gm_table_select_rows(sample_processed_data): data = json.loads(sample_processed_data) rows = data["gcf_data"] selected_rows = [0, 1] - output1, output2 = select_rows(rows, selected_rows) + output1, output2 = gm_table_select_rows(rows, selected_rows) assert output1 == f"Total rows: {len(rows)}" assert output2.startswith(f"Selected rows: {len(selected_rows)}\nSelected GCF IDs: ") # Test with no rows - output1, output2 = select_rows([], None) + output1, output2 = gm_table_select_rows([], None) assert output1 == "No data available." assert output2 == "No rows selected."