From 2f08caef425e61c529df8c31c146395fe2b2cac2 Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Fri, 5 Sep 2025 18:46:06 +0200 Subject: [PATCH 01/13] first implentatoin of the qpcr --- utils/plates_callback_utils.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index 10aee96..18ed384 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -71,6 +71,9 @@ def handle_plate_upload(contents, filenames, app_state): uploaded_file = AppSpecificFile.create_from_raw_data(name=name, raw_data=decoded_str) app_state, plate = Plate.from_file(input_file=uploaded_file, app_state=app_state) + # here we make the new functon to automatically populate sample_type + app_state, plate = populate_sample_types(app_state, plate) + if plate.plate_type in ("D1000", "D5000", "HSD1000"): plate = initialize_sample_attributes(plate, app_state) @@ -147,6 +150,34 @@ def update_app_state_from_ui(app_state, all_ui_tables): # print(f"Updated well {well_pos} with sample_id: {sample_id}, sample_type: {sample_type}") +def populate_sample_types(app_state, plate): + print("plate.plate_type", plate.plate_type) + print("plate", plate) + if plate.plate_type.lower() == "qpcr": + for well in plate.plate_wells: + position = well.well_position_id + row = position[0] + col = int(position[1:]) + + # Assign sample types according to your rules + if "A" <= row <= "F" and 1 <= col <= 3: + sample_type = "Standard" + elif row in ("G", "H") and 1 <= col <= 3: + sample_type = "Negative control" + else: + sample_type = "Library" + + # well.get_sample(app_state).sample_type use this by Griffin + sample = well.get_sample(app_state) + if sample: + sample.sample_type = sample_type + print(f"Well {position}: Set sample_type to {sample_type}") + else: + print(f"Well {position}: No sample found to set sample_type.") + + return app_state, plate + return app_state, plate + From 5accbdb27543215f9c5ec344ee65ef9281231698 Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Sun, 7 Sep 2025 01:28:10 +0200 Subject: [PATCH 02/13] changed plate updated --- index.py | 7 +++++- utils/api_call_utils.py | 4 ++++ utils/plates_callback_utils.py | 44 ++++++++++++++++++++++++---------- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/index.py b/index.py index 855c175..541895b 100644 --- a/index.py +++ b/index.py @@ -528,7 +528,12 @@ def plate_mapper(plate_ids, objects): selected_well_ids = {p['customdata'][0] for p in selected_data['points']} for well in plate.plate_wells: if str(well.well_position_id) in selected_well_ids: - well.get_sample(app_state).sample_type = sample_type + sample = well.get_sample(app_state) + sample.sample_type = sample_type + if sample.sample_type not in ("Library", "Positiv Controll"): + well.sample_identifier = "" + well.well_note = "" + warnings = collect_missing_sample_warnings(app_state) show_warning = bool(warnings) diff --git a/utils/api_call_utils.py b/utils/api_call_utils.py index e9a3770..3530c20 100644 --- a/utils/api_call_utils.py +++ b/utils/api_call_utils.py @@ -44,6 +44,10 @@ def initialize_sample_attributes(plate: Plate, app_state): for well in plate.plate_wells: + sample_type = well.get_sample(app_state).sample_type + if sample_type != "Library": + continue + formatted_tube_id = format_tube_id(well.file_sample_identifier) if not formatted_tube_id: continue diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index 18ed384..08e7c01 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -20,7 +20,8 @@ def find_and_populate_samples_by_sample_id( """ # 4. Annotate wells in place (update nested Sample directly) - if sample_id: + sample_type = well.get_sample(app_state).sample_type + if sample_id and sample_type in ("Library", "Positive Control"): sample_data = bfabric_interface.get_wrapper().read("sample", {"id": [sample_id]}) if not sample_data: well.well_note = "Unable to Update" @@ -144,22 +145,24 @@ def update_app_state_from_ui(app_state, all_ui_tables): # print(f"Updated well {well_pos} with sample_type: {sample_type}") break if sample_id: - if sample.sample_id != sample_id: + if sample.sample_id != sample_id and sample.sample_type in ("Library", "Positive Control"): find_and_populate_samples_by_sample_id(app_state, sample_id, well=pw) break # print(f"Updated well {well_pos} with sample_id: {sample_id}, sample_type: {sample_type}") def populate_sample_types(app_state, plate): - print("plate.plate_type", plate.plate_type) - print("plate", plate) if plate.plate_type.lower() == "qpcr": for well in plate.plate_wells: position = well.well_position_id row = position[0] col = int(position[1:]) - # Assign sample types according to your rules + # Only consider columns 1, 4, 7, 10 + if col not in (1, 4, 7, 10): + continue + + # Assign sample types based on postion if "A" <= row <= "F" and 1 <= col <= 3: sample_type = "Standard" elif row in ("G", "H") and 1 <= col <= 3: @@ -167,16 +170,33 @@ def populate_sample_types(app_state, plate): else: sample_type = "Library" - # well.get_sample(app_state).sample_type use this by Griffin sample = well.get_sample(app_state) - if sample: - sample.sample_type = sample_type - print(f"Well {position}: Set sample_type to {sample_type}") - else: - print(f"Well {position}: No sample found to set sample_type.") + sample.sample_type = sample_type return app_state, plate - return app_state, plate + + + else: + for well in plate.plate_wells: + desc = well.sample_identifier + desc_lower = desc.lower() + + if ( + "negative control" in desc_lower + or "neg" in desc_lower + ): + sample_type = "Negative Control" + elif "2220" in desc_lower: + sample_type = "Positive Control" + elif any(x in desc_lower for x in ["phix", "phi x", "phi-x", "phi_x"]): + sample_type = "PhiX" + else: + sample_type = "Library" + + sample = well.get_sample(app_state) + sample.sample_type = sample_type + + return app_state, plate From 53f9f2d072849dbd803709811863a9ae0d5bb627 Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Tue, 9 Sep 2025 18:02:50 +0200 Subject: [PATCH 03/13] added coloring, non ediatble sample_id, updating rows when changing sample_type --- index.py | 35 ++++++++++++++----- utils/api_call_utils.py | 8 +++++ utils/plate_render_utils.py | 64 +++++++++++++++++++++++----------- utils/plates_callback_utils.py | 34 ++++++++++++++---- 4 files changed, 104 insertions(+), 37 deletions(-) diff --git a/index.py b/index.py index 541895b..6fa373c 100644 --- a/index.py +++ b/index.py @@ -9,7 +9,7 @@ import base64 import io import pandas as pd -from models import Plate, PlateWell, Measurement +from models import Plate, PlateWell, Measurement, Sample from app_state import AppState import json from dash.exceptions import PreventUpdate @@ -32,7 +32,6 @@ EMPTY_BORDER_PX, make_tab, get_tab_id, - SAMPLE_TYPE_OPTIONS ) pd.set_option('future.no_silent_downcasting', True) @@ -529,10 +528,30 @@ def plate_mapper(plate_ids, objects): for well in plate.plate_wells: if str(well.well_position_id) in selected_well_ids: sample = well.get_sample(app_state) + if plate.plate_type != "qPCR": + if sample.sample_type not in ("Library", "Positive Control") and sample_type in ("Library", "Positive Control"): + well.well_note = "No sample found" + if sample_type not in ("Library", "Positive Control"): + well.well_note = "" + sample_identifier = f"{sample_type}_{well.well_position_id}_{plate_id}" + # Check if sample with this identifier already exists if not create + if sample_identifier in app_state.sample_by_id: + # Use existing sample + existing_sample = app_state.sample_by_id[sample_identifier] + well.file_sample_identifier = existing_sample.file_sample_identifier + else: + # Create new sample + new_sample = Sample( + sample_id="", + sample_name="", + container="", + file_sample_identifier=sample_identifier, + sample_type=sample_type, + ) + app_state.sample_registry.append(new_sample) + well.file_sample_identifier = sample_identifier sample.sample_type = sample_type - if sample.sample_type not in ("Library", "Positiv Controll"): - well.sample_identifier = "" - well.well_note = "" + warnings = collect_missing_sample_warnings(app_state) @@ -801,8 +820,6 @@ def update_rows_callback(_, app_data_state_json, table_id): df = plate.dataset(app_state, tidy=False).replace(['None', 'none', 'NaN', 'nan', ''], np.nan).infer_objects(copy=False) - print("Plate dataset: ", df.head(3)) - # Keep columns tidy, but do not drop keys AG Grid needs (e.g., well_position_id if you use getRowId) if is_qpcr: df = df.dropna(axis=1, how="all") @@ -1054,8 +1071,8 @@ def toggle_delete_plate_modal(n_open, n_cancel, n_confirm): return False return no_update - +# port=bfabric_web_apps.PORT # Here we run the app on the specified host and port. if __name__ == "__main__": - app.run(debug=bfabric_web_apps.DEBUG, port=bfabric_web_apps.PORT, host=bfabric_web_apps.HOST) + app.run(debug=bfabric_web_apps.DEBUG, port=8051 , host=bfabric_web_apps.HOST) diff --git a/utils/api_call_utils.py b/utils/api_call_utils.py index 3530c20..22aa841 100644 --- a/utils/api_call_utils.py +++ b/utils/api_call_utils.py @@ -45,6 +45,14 @@ def initialize_sample_attributes(plate: Plate, app_state): for well in plate.plate_wells: sample_type = well.get_sample(app_state).sample_type + + if sample_type == "Positive Control": + well.well_note = "No sample found" + well.get_sample(app_state).sample_id = "" + well.get_sample(app_state).sample_name = "" + well.get_sample(app_state).container = "" + continue + if sample_type != "Library": continue diff --git a/utils/plate_render_utils.py b/utils/plate_render_utils.py index 1108325..4f42454 100644 --- a/utils/plate_render_utils.py +++ b/utils/plate_render_utils.py @@ -10,15 +10,6 @@ pd.set_option('future.no_silent_downcasting', True) -# Mapping of sample types to their display options -SAMPLE_TYPE_OPTIONS = [ - {"label": "Negative Control", "value": "Negative Control"}, - {"label": "Positive Control", "value": "Positive Control"}, - {"label": "Standard", "value": "Standard"}, - {"label": "PhiX", "value": "PhiX"}, - {"label": "Library", "value": "Library"}, -] - SAMPLE_TYPE_COLORS = { "Negative Control": "#ef5350", # red "Positive Control": "#2979ff", # blue @@ -174,6 +165,23 @@ def build_tab_body(plate, app_state): import dash_bootstrap_components as dbc import dash_ag_grid as dag + # Dynamic options for the sample_type dropdown: + if plate.plate_type == "qPCR": + SAMPLE_TYPE_OPTIONS = [ + {"label": "Negative Control", "value": "Negative Control"}, + {"label": "Positive Control", "value": "Positive Control"}, + {"label": "Standard", "value": "Standard"}, + {"label": "PhiX", "value": "PhiX"}, + {"label": "Library", "value": "Library"}, + ] + else: + SAMPLE_TYPE_OPTIONS = [ + {"label": "Negative Control", "value": "Negative Control"}, + {"label": "Positive Control", "value": "Positive Control"}, + {"label": "PhiX", "value": "PhiX"}, + {"label": "Library", "value": "Library"}, + ] + pid = str(plate.plate_id) # normalize once; reuse everywhere is_qpcr = (plate.plate_type == "qPCR") @@ -235,17 +243,29 @@ def _is_num(s): "cellEditorParams": {"values": [opt["value"] for opt in SAMPLE_TYPE_OPTIONS]}, "singleClickEdit": True, "cellStyle": { - "function": """(params) => { - const colors = { - 'Negative Control': '#ef5350', - 'Positive Control': '#2979ff', - 'Standard': '#153677', - 'PhiX': '#ffd600', - 'Library': '#43a047' - }; - const c = colors[params.value] || ''; - return { backgroundColor: c, color: (params.value==='PhiX'?'black':'white') }; - }""" + "styleConditions": [ + { + "condition": "params.value === 'Negative Control'", + "style": {"backgroundColor": "rgb(247,169,168)", "color": "black"}, + }, + { + "condition": "params.value === 'Positive Control'", + "style": {"backgroundColor": "rgb(148,188,255)", "color": "black"}, + }, + { + "condition": "params.value === 'Standard'", + "style": {"backgroundColor": "rgb(138,154,187)", "color": "black"}, + }, + { + "condition": "params.value === 'PhiX'", + "style": {"backgroundColor": "rgb(255,235,127)", "color": "black"}, + }, + { + "condition": "params.value === 'Library'", + "style": {"backgroundColor": "rgb(161,207,163)", "color": "black"}, + }, + ], + "defaultStyle": {"backgroundColor": "white", "color": "black"}, }, "cellRenderer": { "function": {"code": """ @@ -266,7 +286,9 @@ def _is_num(s): # ensure sample_id is editable and right after sample_type col_sample_id = { "field": "sample_id", - "editable": True, + "editable": { + "function": "params.data.sample_type === 'Library' || params.data.sample_type === 'Positive Control'" + }, "filter": "agTextColumnFilter", "valueFormatter": {"function": "params.value == null ? '' : params.value"}, } diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index 08e7c01..5a5e5d7 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -20,8 +20,7 @@ def find_and_populate_samples_by_sample_id( """ # 4. Annotate wells in place (update nested Sample directly) - sample_type = well.get_sample(app_state).sample_type - if sample_id and sample_type in ("Library", "Positive Control"): + if sample_id: sample_data = bfabric_interface.get_wrapper().read("sample", {"id": [sample_id]}) if not sample_data: well.well_note = "Unable to Update" @@ -46,12 +45,14 @@ def find_and_populate_samples_by_sample_id( sample_name = safe_str(bfabric_sample.get("name")) container_id = safe_str(bfabric_sample.get("container", {}).get("id")) well.well_note = "Sample found" + sample_type = well.get_sample(app_state).sample_type new_sample = Sample( sample_id=sample_id, sample_name=sample_name, container=container_id, - file_sample_identifier=tube_id + file_sample_identifier=tube_id, + sample_type=sample_type ) app_state.sample_registry.append(new_sample) well.file_sample_identifier = tube_id @@ -141,16 +142,35 @@ def update_app_state_from_ui(app_state, all_ui_tables): if sample: if sample.sample_type != sample_type: + if plate.plate_type != "qPCR": + if sample.sample_type not in ("Library", "Positive Control") and sample_type in ("Library", "Positive Control"): + pw.well_note = "No sample found" + + if sample_type not in ("Library", "Positive Control"): + pw.well_note = "" + sample_identifier = f"{sample_type}_{pw.well_position_id}_{pw.plate_id}" + # Check if sample with this identifier already exists, if not create + if sample_identifier in app_state.sample_by_id: + existing_sample = app_state.sample_by_id[sample_identifier] + pw.file_sample_identifier = existing_sample.file_sample_identifier + else: + new_sample = Sample( + sample_id="", + sample_name="", + container="", + file_sample_identifier=sample_identifier, + sample_type=sample_type, + ) + app_state.sample_registry.append(new_sample) + pw.file_sample_identifier = sample_identifier + sample.sample_type = sample_type - # print(f"Updated well {well_pos} with sample_type: {sample_type}") break if sample_id: if sample.sample_id != sample_id and sample.sample_type in ("Library", "Positive Control"): find_and_populate_samples_by_sample_id(app_state, sample_id, well=pw) break - # print(f"Updated well {well_pos} with sample_id: {sample_id}, sample_type: {sample_type}") - def populate_sample_types(app_state, plate): if plate.plate_type.lower() == "qpcr": for well in plate.plate_wells: @@ -166,7 +186,7 @@ def populate_sample_types(app_state, plate): if "A" <= row <= "F" and 1 <= col <= 3: sample_type = "Standard" elif row in ("G", "H") and 1 <= col <= 3: - sample_type = "Negative control" + sample_type = "Negative Control" else: sample_type = "Library" From 9f89eca8928898dc447c80b64b6c63c518bcdd78 Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Wed, 10 Sep 2025 16:18:01 +0200 Subject: [PATCH 04/13] changed logic to not updated sample identifier --- index.py | 29 +++-------------------------- utils/plate_render_utils.py | 30 ++++++++++++++++++++++-------- utils/plates_callback_utils.py | 22 ---------------------- utils/warning_utils.py | 12 +++++++++++- 4 files changed, 36 insertions(+), 57 deletions(-) diff --git a/index.py b/index.py index 6fa373c..47a07cc 100644 --- a/index.py +++ b/index.py @@ -528,28 +528,6 @@ def plate_mapper(plate_ids, objects): for well in plate.plate_wells: if str(well.well_position_id) in selected_well_ids: sample = well.get_sample(app_state) - if plate.plate_type != "qPCR": - if sample.sample_type not in ("Library", "Positive Control") and sample_type in ("Library", "Positive Control"): - well.well_note = "No sample found" - if sample_type not in ("Library", "Positive Control"): - well.well_note = "" - sample_identifier = f"{sample_type}_{well.well_position_id}_{plate_id}" - # Check if sample with this identifier already exists if not create - if sample_identifier in app_state.sample_by_id: - # Use existing sample - existing_sample = app_state.sample_by_id[sample_identifier] - well.file_sample_identifier = existing_sample.file_sample_identifier - else: - # Create new sample - new_sample = Sample( - sample_id="", - sample_name="", - container="", - file_sample_identifier=sample_identifier, - sample_type=sample_type, - ) - app_state.sample_registry.append(new_sample) - well.file_sample_identifier = sample_identifier sample.sample_type = sample_type @@ -772,9 +750,9 @@ def sync_plate_style(cell_evt, row_data, virtual_row_data, fig): fills.append(fill_color) customdata.append([w, st]) - if w in error_wells: + if w in error_wells and st in ("Library", "Positive Control"): line_colors.append(ERROR_BORDER); line_widths.append(3) - elif w in warn_wells: + elif w in warn_wells and st in ("Library", "Positive Control"): line_colors.append(WARN_BORDER); line_widths.append(3) else: line_colors.append(DEFAULT_BORDER); line_widths.append(BORDER_PX) @@ -1071,8 +1049,7 @@ def toggle_delete_plate_modal(n_open, n_cancel, n_confirm): return False return no_update -# port=bfabric_web_apps.PORT # Here we run the app on the specified host and port. if __name__ == "__main__": - app.run(debug=bfabric_web_apps.DEBUG, port=8051 , host=bfabric_web_apps.HOST) + app.run(debug=bfabric_web_apps.DEBUG, port=bfabric_web_apps.PORT , host=bfabric_web_apps.HOST) diff --git a/utils/plate_render_utils.py b/utils/plate_render_utils.py index 4f42454..d9be7a8 100644 --- a/utils/plate_render_utils.py +++ b/utils/plate_render_utils.py @@ -110,7 +110,7 @@ def fill_color(g): line_colors = [] line_widths = [] for w in df["well"]: - if w in error_wells: + if w not in error_wells: line_colors.append(ERROR_BORDER); line_widths.append(3) elif w in warn_wells: line_colors.append(WARN_BORDER); line_widths.append(3) @@ -207,8 +207,22 @@ def build_tab_body(plate, app_state): getRowStyle = { "styleConditions": [ - {"condition": "params.data.__error__ === true", "style": {"backgroundColor": "#ffcccc"}}, - {"condition": "params.data.__warning__ === true", "style": {"backgroundColor": "#f9f490"}}, + { + "condition": ( + "params.data.__error__ === true && " + + "params.data.sample_type !== 'Negative Control' && " + + "params.data.sample_type !== 'PhiX'" + ), + "style": {"backgroundColor": "#ffcccc"}, + }, + { + "condition": ( + "params.data.__warning__ === true && " + + "params.data.sample_type !== 'Negative Control' && " + + "params.data.sample_type !== 'PhiX'" + ), + "style": {"backgroundColor": "#f9f490"}, + }, ] } @@ -246,23 +260,23 @@ def _is_num(s): "styleConditions": [ { "condition": "params.value === 'Negative Control'", - "style": {"backgroundColor": "rgb(247,169,168)", "color": "black"}, + "style": {"backgroundColor": "rgb(247,169,168)", "color": "black", "borderRadius": "10px"}, }, { "condition": "params.value === 'Positive Control'", - "style": {"backgroundColor": "rgb(148,188,255)", "color": "black"}, + "style": {"backgroundColor": "rgb(148,188,255)", "color": "black", "borderRadius": "10px"}, }, { "condition": "params.value === 'Standard'", - "style": {"backgroundColor": "rgb(138,154,187)", "color": "black"}, + "style": {"backgroundColor": "rgb(138,154,187)", "color": "black", "borderRadius": "10px"}, }, { "condition": "params.value === 'PhiX'", - "style": {"backgroundColor": "rgb(255,235,127)", "color": "black"}, + "style": {"backgroundColor": "rgb(255,235,127)", "color": "black", "borderRadius": "10px"}, }, { "condition": "params.value === 'Library'", - "style": {"backgroundColor": "rgb(161,207,163)", "color": "black"}, + "style": {"backgroundColor": "rgb(161,207,163)", "color": "black", "borderRadius": "10px"}, }, ], "defaultStyle": {"backgroundColor": "white", "color": "black"}, diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index 5a5e5d7..676968f 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -142,28 +142,6 @@ def update_app_state_from_ui(app_state, all_ui_tables): if sample: if sample.sample_type != sample_type: - if plate.plate_type != "qPCR": - if sample.sample_type not in ("Library", "Positive Control") and sample_type in ("Library", "Positive Control"): - pw.well_note = "No sample found" - - if sample_type not in ("Library", "Positive Control"): - pw.well_note = "" - sample_identifier = f"{sample_type}_{pw.well_position_id}_{pw.plate_id}" - # Check if sample with this identifier already exists, if not create - if sample_identifier in app_state.sample_by_id: - existing_sample = app_state.sample_by_id[sample_identifier] - pw.file_sample_identifier = existing_sample.file_sample_identifier - else: - new_sample = Sample( - sample_id="", - sample_name="", - container="", - file_sample_identifier=sample_identifier, - sample_type=sample_type, - ) - app_state.sample_registry.append(new_sample) - pw.file_sample_identifier = sample_identifier - sample.sample_type = sample_type break if sample_id: diff --git a/utils/warning_utils.py b/utils/warning_utils.py index 30740a8..3746a0f 100644 --- a/utils/warning_utils.py +++ b/utils/warning_utils.py @@ -12,7 +12,17 @@ def collect_missing_sample_warnings(app_state): # Tab label, just like in UI tab_name = f"Tapestation-{tapestation_index} ({plate.plate_id})" tapestation_index += 1 - missing = [w for w in plate.plate_wells if getattr(w, "well_note", None) in ("No sample found", "Multiple samples found")] + + missing = [] + for w in plate.plate_wells: + sample = w.get_sample(app_state) + sample_type = getattr(sample, "sample_type", None) + if ( + getattr(w, "well_note", None) in ("No sample found", "Multiple samples found") + and sample_type in ("Library", "Positive Control") + ): + missing.append(w) + if missing: for note in set(w.well_note for w in missing): wells = [w.well_position_id for w in missing if w.well_note == note] From 670ec29f9412062d8c06f616cfe33dce5ccb17c0 Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Wed, 10 Sep 2025 17:15:27 +0200 Subject: [PATCH 05/13] updated to add warning if switched to library or positiv control --- index.py | 6 +++++- utils/api_call_utils.py | 2 +- utils/plate_render_utils.py | 2 +- utils/plates_callback_utils.py | 5 +++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/index.py b/index.py index 10023ad..062ec04 100644 --- a/index.py +++ b/index.py @@ -528,10 +528,14 @@ def plate_mapper(plate_ids, objects): for well in plate.plate_wells: if str(well.well_position_id) in selected_well_ids: sample = well.get_sample(app_state) + if sample_type in ("Library", "Positive Control") and sample.sample_type in ("Standard", "Negative Control", "PhiX") and well.well_note == "": + well.well_note = "No sample found" sample.sample_type = sample_type + + warnings = collect_missing_sample_warnings(app_state) show_warning = bool(warnings) warning_msg = build_warning_msg(warnings) @@ -1049,5 +1053,5 @@ def toggle_delete_plate_modal(n_open, n_cancel, n_confirm): # Here we run the app on the specified host and port. if __name__ == "__main__": - app.run(debug=bfabric_web_apps.DEBUG, port=bfabric_web_apps.PORT , host=bfabric_web_apps.HOST) + app.run(debug=bfabric_web_apps.DEBUG, port=bfabric_web_apps.PORT, host=bfabric_web_apps.HOST) diff --git a/utils/api_call_utils.py b/utils/api_call_utils.py index 22aa841..cc87178 100644 --- a/utils/api_call_utils.py +++ b/utils/api_call_utils.py @@ -28,7 +28,7 @@ def initialize_sample_attributes(plate: Plate, app_state): for well in plate.plate_wells: tube_id = str(getattr(well, "file_sample_identifier", "")).strip() - print("tube_id", tube_id) + if tube_id: formatted = format_tube_id(tube_id) formatted_tube_ids.append(formatted) diff --git a/utils/plate_render_utils.py b/utils/plate_render_utils.py index 53090d8..9912e05 100644 --- a/utils/plate_render_utils.py +++ b/utils/plate_render_utils.py @@ -110,7 +110,7 @@ def fill_color(g): line_colors = [] line_widths = [] for w in df["well"]: - if w not in error_wells: + if w in error_wells: line_colors.append(ERROR_BORDER); line_widths.append(3) elif w in warn_wells: line_colors.append(WARN_BORDER); line_widths.append(3) diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index 309aa54..515c88f 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -146,7 +146,12 @@ def update_app_state_from_ui(app_state, all_ui_tables): if sample: if sample.sample_type != sample_type: + if sample_type in ("Library", "Positive Control") and sample.sample_type in ("Standard", "Negative Control", "PhiX") and pw.well_note == "": + pw.well_note = "No sample found" + # Only update sample_type if both old and new are in ("Library", "Positive Control") + # This avoids overwriting "Negative Control" or "PhiX" with "Library" sample.sample_type = sample_type + break if sample_id: if sample.sample_id != sample_id and sample.sample_type in ("Library", "Positive Control"): From 5bd2339cead897ac8c9923deae142b4b6cda338b Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Wed, 10 Sep 2025 18:01:33 +0200 Subject: [PATCH 06/13] added print statment --- utils/plates_callback_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index 515c88f..a5dae5c 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -79,8 +79,8 @@ def handle_plate_upload(contents, filenames, app_state): if plate.plate_type in ("D1000", "D5000", "HSD1000"): plate = initialize_sample_attributes(plate, app_state) + #print("intial app state sample_registry:", app_state.sample_registry) # print("intial app state plate_registry:", app_state.plate_registry) - # print("Added plate:", plate.plate_id) From 5d14032194293610bf4b388c0db645deca8c9cbb Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Wed, 10 Sep 2025 18:08:05 +0200 Subject: [PATCH 07/13] cleaning --- index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.py b/index.py index 062ec04..f9aa144 100644 --- a/index.py +++ b/index.py @@ -9,7 +9,7 @@ import base64 import io import pandas as pd -from models import Plate, PlateWell, Measurement, Sample +from models import Plate, PlateWell, Measurement from app_state import AppState import json from dash.exceptions import PreventUpdate From 5822e9af4d373cde171656a98c24771d9f9d325f Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Thu, 11 Sep 2025 15:38:54 +0200 Subject: [PATCH 08/13] changed how positive control smaple gets loaded --- utils/api_call_utils.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/utils/api_call_utils.py b/utils/api_call_utils.py index cc87178..2f5312d 100644 --- a/utils/api_call_utils.py +++ b/utils/api_call_utils.py @@ -46,14 +46,7 @@ def initialize_sample_attributes(plate: Plate, app_state): sample_type = well.get_sample(app_state).sample_type - if sample_type == "Positive Control": - well.well_note = "No sample found" - well.get_sample(app_state).sample_id = "" - well.get_sample(app_state).sample_name = "" - well.get_sample(app_state).container = "" - continue - - if sample_type != "Library": + if sample_type not in ("Library", "Positive Control"): continue formatted_tube_id = format_tube_id(well.file_sample_identifier) From f9ae886096531f8100fce06f43f8d2ed5fbb903c Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Thu, 11 Sep 2025 16:20:34 +0200 Subject: [PATCH 09/13] added delete plate modal, changed so the identifier lookup happens for all sample_types --- index.py | 2 -- utils/api_call_utils.py | 5 +---- utils/plate_render_utils.py | 18 ++++++++++++++++++ utils/plates_callback_utils.py | 8 ++------ 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/index.py b/index.py index f9aa144..b837a4a 100644 --- a/index.py +++ b/index.py @@ -528,8 +528,6 @@ def plate_mapper(plate_ids, objects): for well in plate.plate_wells: if str(well.well_position_id) in selected_well_ids: sample = well.get_sample(app_state) - if sample_type in ("Library", "Positive Control") and sample.sample_type in ("Standard", "Negative Control", "PhiX") and well.well_note == "": - well.well_note = "No sample found" sample.sample_type = sample_type diff --git a/utils/api_call_utils.py b/utils/api_call_utils.py index 2f5312d..8c45ef3 100644 --- a/utils/api_call_utils.py +++ b/utils/api_call_utils.py @@ -21,7 +21,7 @@ def format_tube_id(val): return "/".join(parts) return val -def initialize_sample_attributes(plate: Plate, app_state): +def populate_samples_by_identifier_lookup(plate: Plate, app_state): B = bfabric_interface.get_wrapper() formatted_tube_ids = [] @@ -46,9 +46,6 @@ def initialize_sample_attributes(plate: Plate, app_state): sample_type = well.get_sample(app_state).sample_type - if sample_type not in ("Library", "Positive Control"): - continue - formatted_tube_id = format_tube_id(well.file_sample_identifier) if not formatted_tube_id: continue diff --git a/utils/plate_render_utils.py b/utils/plate_render_utils.py index 9912e05..040e1b7 100644 --- a/utils/plate_render_utils.py +++ b/utils/plate_render_utils.py @@ -458,6 +458,24 @@ def _is_num(s): ) ]) + + delete_plate_modal = html.Div([ + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Delete Plate")), + dbc.ModalBody( + "Are you sure you want to delete this plate? This will remove all associated wells and their data. This action cannot be undone." + ), + dbc.ModalFooter([ + dbc.Button("Delete", id={"type": "confirm-delete-plate", "index": pid}, color="danger"), + dbc.Button("Cancel", id={"type": "cancel-delete-plate", "index": pid}, className="ms-auto"), + ]), + ], + id={"type": "modal-delete-plate", "index": pid}, + is_open=False, backdrop="static", + ) + ]) + dummy = html.Div(id={"type": "dummy-div", "index": pid}, style={"display": "none"}) return [table_wrap, plate_wrap, buttons_row, delete_plate_modal, flagging_modal, delete_rows_modal, dummy] \ No newline at end of file diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index a5dae5c..6c786be 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -5,7 +5,7 @@ from dash import html from models import Plate, PlateWell, Measurement, AppSpecificFile, Sample from app_state import AppState -from .api_call_utils import initialize_sample_attributes, safe_str +from .api_call_utils import populate_samples_by_identifier_lookup, safe_str from bfabric_web_apps.objects.BfabricInterface import bfabric_interface def find_and_populate_samples_by_sample_id( @@ -77,7 +77,7 @@ def handle_plate_upload(contents, filenames, app_state): app_state, plate = populate_sample_types(app_state, plate) if plate.plate_type in ("D1000", "D5000", "HSD1000"): - plate = initialize_sample_attributes(plate, app_state) + plate = populate_samples_by_identifier_lookup(plate, app_state) #print("intial app state sample_registry:", app_state.sample_registry) # print("intial app state plate_registry:", app_state.plate_registry) @@ -146,10 +146,6 @@ def update_app_state_from_ui(app_state, all_ui_tables): if sample: if sample.sample_type != sample_type: - if sample_type in ("Library", "Positive Control") and sample.sample_type in ("Standard", "Negative Control", "PhiX") and pw.well_note == "": - pw.well_note = "No sample found" - # Only update sample_type if both old and new are in ("Library", "Positive Control") - # This avoids overwriting "Negative Control" or "PhiX" with "Library" sample.sample_type = sample_type break From 5f4cc1ef15f41890ca04bea5777a42eee19e1583 Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Thu, 11 Sep 2025 16:28:06 +0200 Subject: [PATCH 10/13] changed line for simplicity --- index.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/index.py b/index.py index b837a4a..fa46416 100644 --- a/index.py +++ b/index.py @@ -527,10 +527,7 @@ def plate_mapper(plate_ids, objects): selected_well_ids = {p['customdata'][0] for p in selected_data['points']} for well in plate.plate_wells: if str(well.well_position_id) in selected_well_ids: - sample = well.get_sample(app_state) - sample.sample_type = sample_type - - + well.get_sample(app_state).sample_type = sample_type From 531748c096c802388a0cb0cf9b605eab11e2f02a Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Thu, 11 Sep 2025 16:32:18 +0200 Subject: [PATCH 11/13] removed old line --- utils/api_call_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/api_call_utils.py b/utils/api_call_utils.py index 8c45ef3..2de73f4 100644 --- a/utils/api_call_utils.py +++ b/utils/api_call_utils.py @@ -44,8 +44,6 @@ def populate_samples_by_identifier_lookup(plate: Plate, app_state): for well in plate.plate_wells: - sample_type = well.get_sample(app_state).sample_type - formatted_tube_id = format_tube_id(well.file_sample_identifier) if not formatted_tube_id: continue From 24009714b23b00d51ea2d9cf540218573edce774 Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Thu, 11 Sep 2025 16:58:06 +0200 Subject: [PATCH 12/13] fixed formating --- utils/plates_callback_utils.py | 40 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index 6c786be..8a3f55c 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -180,26 +180,26 @@ def populate_sample_types(app_state, plate): else: - for well in plate.plate_wells: - desc = well.sample_identifier - desc_lower = desc.lower() - - if ( - "negative control" in desc_lower - or "neg" in desc_lower - ): - sample_type = "Negative Control" - elif "2220" in desc_lower: - sample_type = "Positive Control" - elif any(x in desc_lower for x in ["phix", "phi x", "phi-x", "phi_x"]): - sample_type = "PhiX" - else: - sample_type = "Library" - - sample = well.get_sample(app_state) - sample.sample_type = sample_type - - return app_state, plate + for well in plate.plate_wells: + desc = well.sample_identifier + desc_lower = desc.lower() + + if ( + "negative control" in desc_lower + or "neg" in desc_lower + ): + sample_type = "Negative Control" + elif "2220" in desc_lower: + sample_type = "Positive Control" + elif any(x in desc_lower for x in ["phix", "phi x", "phi-x", "phi_x"]): + sample_type = "PhiX" + else: + sample_type = "Library" + + sample = well.get_sample(app_state) + sample.sample_type = sample_type + + return app_state, plate From c259953c31ee487fc03983c615f9e179c6f707cf Mon Sep 17 00:00:00 2001 From: Marc Zuber Date: Thu, 11 Sep 2025 17:01:15 +0200 Subject: [PATCH 13/13] changed phix to phi --- utils/plates_callback_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py index 8a3f55c..8501221 100644 --- a/utils/plates_callback_utils.py +++ b/utils/plates_callback_utils.py @@ -191,7 +191,7 @@ def populate_sample_types(app_state, plate): sample_type = "Negative Control" elif "2220" in desc_lower: sample_type = "Positive Control" - elif any(x in desc_lower for x in ["phix", "phi x", "phi-x", "phi_x"]): + elif "phi" in desc_lower: sample_type = "PhiX" else: sample_type = "Library"