diff --git a/index.py b/index.py
index ceb699e..307a8f2 100644
--- a/index.py
+++ b/index.py
@@ -16,7 +16,7 @@
import re
import copy
-
+from utils.flagging_conditions import collect_flagging_from_all
from utils.plates_callback_utils import update_app_state_from_ui, handle_plate_delete, handle_plate_upload, find_and_populate_samples_by_sample_id
from utils.warning_utils import collect_missing_sample_warnings, build_warning_msg
from utils.plate_render_utils import (
@@ -117,6 +117,14 @@
id="plate-button-container",
className="small",
),
+ html.Div(
+ id="phix-control-dropdowns",
+ className="small",
+ ),
+ html.Div(
+ id="adjust-flagging-conditon-buttons",
+ className="small",
+ ),
],
className="p-2 small",
),
@@ -133,17 +141,11 @@
dbc.Alert("Error: More than one qPCR file uploaded. Please upload only one.", color="danger",id="alert-fade-too-many-qpcr", dismissable=False, is_open=False),
dbc.Alert(id="alert-find-libraries-fail", color="danger", is_open=False, dismissable=True, children=""),
dbc.Alert(id="alert-fade-duplicate-file", color="danger", is_open=False, dismissable=True, children=""),
- dbc.Alert(
- id="alert-tapestation-sampleid",
- color="warning",
- is_open=False,
- dismissable=False,
- children="",
- ),
+ dbc.Alert(id="alert-tapestation-sampleid", color="warning", is_open=False, dismissable=False, children=""),
+ dbc.Alert(id="alert-flagging-conditions", color="warning", is_open=False, dismissable=False, children=""),
dbc.Alert(id="alert-find-libraries-success", color="success", is_open=False, dismissable=True, children=""),
-
-
- ], style={"margin": "20px"}
+ ],
+ style={"margin": "20px"}
)
def render_main_content():
@@ -195,7 +197,9 @@ def render_main_content():
),
width=9,
),
- dcc.Store(id="app_data_state", storage_type="memory")
+ dcc.Store(id="app_data_state", storage_type="memory"),
+ dcc.Store(id="flagging-conditions-storage", storage_type="memory"),
+ dcc.Store(id="flagging-conditions-storage-prune-missing-tabs", storage_type="memory"),
],
style={"margin-top": "0px", "min-height": "40vh"}
)
@@ -277,6 +281,7 @@ def update_ui(token_data, entity_data):
Input({"type": "Standard", "index": ALL}, "n_clicks"),
Input({"type": "PhiX", "index": ALL}, "n_clicks"),
Input({"type": "Library", "index": ALL}, "n_clicks"),
+ Input("flagging-conditions-storage", "data"),
],
[
State({"type": "plate-graph", "index": ALL}, "selectedData"),
@@ -293,7 +298,7 @@ def update_ui(token_data, entity_data):
State({"type": "datatable-plate", "index": ALL}, "id"),
State({"type": "confirm-delete-rows", "index": ALL}, "id"),
State({"type":"plate-graph", "index":ALL}, "id"),
- State("view-mode-switch", "value")
+ State("view-mode-switch", "value"),
],
prevent_initial_call=True,
)
@@ -307,6 +312,7 @@ def manipulate_plates(
standard,
phix,
library,
+ flagging_conditions_storage,
plate_graph_selections,
filenames,
app_data_state_json,
@@ -317,7 +323,7 @@ def manipulate_plates(
datatable_ids,
delete_rows_button_ids,
plate_graph_ids,
- table_view
+ table_view,
):
"""
Core callback for all plate management actions (upload, delete, edit, sample lookup).
@@ -339,7 +345,7 @@ def manipulate_plates(
"Positive Control",
"Standard",
"PhiX",
- "Library"
+ "Library",
}
@@ -347,7 +353,6 @@ def manipulate_plates(
app_state = AppState.from_json(app_data_state_json) if app_data_state_json else AppState()
print("manipulate_plates triggered by:", triggered)
-
def plate_mapper(plate_ids, objects):
"""
This takes in a list of dash component ids (which we've passed above as states)
@@ -430,6 +435,14 @@ def plate_mapper(plate_ids, objects):
False, "", False, "", None, False, "", [], no_update
)
+ elif triggered == "flagging-conditions-storage":
+ update_app_state_from_ui(app_state, all_ui_tables)
+ warnings = collect_missing_sample_warnings(app_state)
+ show_warning = bool(warnings)
+ warning_msg = build_warning_msg(warnings)
+ return False, "", app_state.to_json(), show_warning, warning_msg, False, "", False, "", None, False, "", no_update, []
+
+
# --- Check that we're not getting any unexpected triggers... this shouldn't happen, but better to raise an error rather than hide it ---
elif type(triggered) != dash._utils.AttributeDict or triggered.get("type") not in POSSIBLE_TRIGGERS:
raise Exception("ERROR RAISED INTENTIONALLY - No relevant trigger found.")
@@ -548,6 +561,11 @@ def update_standard_curve(app_state, row_data, plate_id):
else:
app_state = AppState.from_json(app_state)
plate = app_state.plate_by_id.get(plate_id.get("index"))
+
+ if plate is None:
+ return no_update, no_update, no_update
+
+
df = plate.dataset(app_state, tidy=True).replace(['None', 'none', 'NaN', 'nan', ''], np.nan).infer_objects(copy=False)
# grab only samples which are defined as standards
@@ -576,23 +594,81 @@ def update_standard_curve(app_state, row_data, plate_id):
]
return pcr_fig, pcr_table_children, store_data
+
+
+
+@app.callback(
+ Output("flagging-conditions-storage", "data"),
+ [
+ Input({"type": "input-stddev", "index": ALL}, "value"),
+ Input({"type": "input-min-nm", "index": ALL}, "value"),
+ Input({"type": "input-max-nm", "index": ALL}, "value"),
+ Input({"type": "input-min-frag", "index": ALL}, "value"),
+ Input({"type": "input-max-frag", "index": ALL}, "value"),
+ Input({"type": "input-min-mol", "index": ALL}, "value"),
+ Input({"type": "input-max-mol", "index": ALL}, "value"),
+ Input({"type": "phix-control-dropdown", "index": ALL}, "value"),
+ ],
+ [
+ State({"type": "input-stddev", "index": ALL}, "id"),
+ State({"type": "input-min-nm", "index": ALL}, "id"),
+ State({"type": "input-max-nm", "index": ALL}, "id"),
+ State({"type": "input-min-frag", "index": ALL}, "id"),
+ State({"type": "input-max-frag", "index": ALL}, "id"),
+ State({"type": "input-min-mol", "index": ALL}, "id"),
+ State({"type": "input-max-mol", "index": ALL}, "id"),
+ State({"type": "phix-control-dropdown", "index": ALL}, "id"),
+ ],
+ prevent_initial_call=True
+)
+
+def collect_flagging_conditions(stddev_vals, min_nm_vals, max_nm_vals,
+ min_frag_vals, max_frag_vals, min_mol_vals, max_mol_vals,
+ phix_vals, stddev_ids, min_nm_ids, max_nm_ids,
+ min_frag_ids, max_frag_ids, min_mol_ids, max_mol_ids, phix_ids ):
+
+ def add(values, ids, key, out):
+ for value, id_dict in zip(values, ids):
+ pid = id_dict.get("index")
+ if pid not in out:
+ out[pid] = {}
+ out[pid][key] = value
+
+ print("Collecting flagging conditions...", phix_vals)
+
+ flagging_conditions = {}
+ add(stddev_vals, stddev_ids, "stddev", flagging_conditions)
+ add(min_nm_vals, min_nm_ids, "min_nm", flagging_conditions)
+ add(max_nm_vals, max_nm_ids, "max_nm", flagging_conditions)
+ add(min_frag_vals, min_frag_ids, "min_frag", flagging_conditions)
+ add(max_frag_vals, max_frag_ids, "max_frag", flagging_conditions)
+ add(min_mol_vals, min_mol_ids, "min_mol", flagging_conditions)
+ add(max_mol_vals, max_mol_ids, "max_mol", flagging_conditions)
+ add(phix_vals, phix_ids, "phix_control_well_position", flagging_conditions)
+
+ print("Collected flagging conditions:", flagging_conditions)
+ return flagging_conditions
+
+
@app.callback(
[
Output("dynamic-tabs", "children", allow_duplicate=True),
Output("dynamic-tabs", "active_tab", allow_duplicate=True),
- Output("table-creation-trigger", "children"),
+ Output("table-creation-trigger", "children"),
+ Output("flagging-conditions-storage-prune-missing-tabs", "data"),
],
# TRIGGERS AFTER EVERY "UPLOAD OR DELETE" OPERATION
# Upload / Delete from mega function --> table-deletion-trigger --> table-creation-trigger
Input("table-deletion-trigger", "children"),
State("dynamic-tabs", "children"),
State("dynamic-tabs", "active_tab"),
+ State("flagging-conditions-storage", "data"),
prevent_initial_call=True
)
-def prune_missing_tabs(app_data_json, children, active_tab):
+def prune_missing_tabs(app_data_json, children, active_tabs, flagging_conditions_storage):
children = children or []
app = AppState.from_json(app_data_json) if app_data_json else None
@@ -621,13 +697,19 @@ def prune_missing_tabs(app_data_json, children, active_tab):
else:
new_children, next_active = [], None
- return new_children, next_active, ""
+ # Build a per-plate flagging snapshot from the ALL inputs
+
+ return new_children, next_active, "", flagging_conditions_storage
@app.callback(
[
Output("dynamic-tabs", "children"),
- Output("dynamic-tabs", "active_tab")
+ Output("dynamic-tabs", "active_tab"),
+ Output("adjust-flagging-conditon-buttons", "children"),
+ Output("phix-control-dropdowns", "children"),
+ Output("alert-flagging-conditions", "is_open", allow_duplicate=True),
+ Output("alert-flagging-conditions", "children", allow_duplicate=True),
],
# TRIGGERS AFTER EVERY "UPLOAD OR DELETE" OPERATION
# Upload / Delete from mega function --> table-deletion-trigger --> table-creation-trigger
@@ -635,15 +717,17 @@ def prune_missing_tabs(app_data_json, children, active_tab):
[
State("app_data_state", "data"),
State("dynamic-tabs", "children"),
- State("dynamic-tabs", "active_tab")
+ State("dynamic-tabs", "active_tab"),
+ State("flagging-conditions-storage-prune-missing-tabs", "data")
],
prevent_initial_call=True
)
-def append_new_tabs(_, app_data_json, existing_tabs, active_tab):
-
+def append_new_tabs(_trigger, app_data_json, existing_tabs, active_tab, flagging_conditions):
# RETURNS an EXACT COPY of existing tabs, plus any new ones, generated
# the same as before from the function in plate_render_utils.
+ #print("app_state for tab append before:", app_data_json)
+
existing_tabs = existing_tabs or []
app = AppState.from_json(app_data_json)
desired_ids = [str(p.plate_id) for p in app.plate_registry]
@@ -655,10 +739,34 @@ def append_new_tabs(_, app_data_json, existing_tabs, active_tab):
to_add = [pid for pid in desired_ids if pid not in mounted_ids]
if not to_add:
# nothing to add; keep everything untouched
- return existing_tabs, (active_tab if active_tab in desired_ids else (desired_ids[-1] if desired_ids else None))
+ return existing_tabs, (active_tab if active_tab in desired_ids else (desired_ids[-1] if desired_ids else None)), no_update
+
plates_by_id = {str(p.plate_id): p for p in app.plate_registry}
- new_tabs = [make_tab(plates_by_id[pid], app) for pid in to_add]
+ new_tabs = []
+ flag_buttons = []
+ phix_dropdowns = []
+ aggregated_flagging_warnings = []
+ for pid in to_add:
+ if flagging_conditions:
+ flagging_per_plate = flagging_conditions.get(pid)
+ else:
+ flagging_per_plate = {}
+
+ tab, flag_button, phix_dropdown, plate_flagging_warnings = make_tab(
+ plates_by_id[pid],
+ app,
+ current_flagging=flagging_per_plate
+ )
+
+ new_tabs.append(tab)
+ flag_buttons.append(flag_button)
+ phix_dropdowns.append(phix_dropdown)
+
+ if plate_flagging_warnings:
+ aggregated_flagging_warnings.extend(plate_flagging_warnings)
+
+
# preserve order of existing; append new at the end (no reordering of mounted)
next_children = existing_tabs + new_tabs
@@ -681,7 +789,43 @@ def _id_of(tab_like):
else:
next_active = None
- return next_children, next_active
+ # Before returning, set visibility style on each button
+ for btn in flag_buttons:
+ if btn.id["index"] == next_active:
+ # Make the active one visible
+ btn.style = {}
+ else:
+ # Hide all others
+ btn.style = {"display": "none"}
+
+ #print("app_state for tab append after:", app_data_json)
+
+ if aggregated_flagging_warnings:
+ # turn warnings into
list items
+ list_items = [html.Li(msg) for msg in aggregated_flagging_warnings]
+
+ # add the instruction at the bottom, same styling pattern as build_warning_msg
+ list_items.append(
+ html.Li(
+ "To resolve these warnings: Check wells highlighted in red. Adjust your flagging thresholds (Adjust Flagging Condition) or review those wells manually.",
+ style={"listStyleType": "none", "marginTop": "10px"}
+ )
+ )
+
+ alert_is_open = True
+ alert_children = html.Ul(list_items)
+ else:
+ alert_is_open = False
+ alert_children = ""
+
+ return (
+ next_children,
+ next_active,
+ flag_buttons,
+ phix_dropdowns,
+ alert_is_open,
+ alert_children,
+ )
@@ -762,6 +906,17 @@ def sync_plate_style(cell_evt, row_data, virtual_row_data, fig):
else:
error_wells, warn_wells = set(), set()
+ # Add flagging_condition-based error wells
+ flag_error_wells = set()
+ if {"flagging_conditions", "well_position_id"}.issubset(df_flags.columns):
+ flag_error_wells = set(
+ df_flags.loc[
+ df_flags["flagging_conditions"].notna() & (df_flags["flagging_conditions"].astype(str) != ""),
+ "well_position_id"
+ ].astype(str)
+ )
+ error_wells |= flag_error_wells
+
# sample types
sample_type_map = {}
if {"sample_type","well_position_id"}.issubset(df_flags.columns):
@@ -812,6 +967,8 @@ def sync_plate_style(cell_evt, row_data, virtual_row_data, fig):
+
+
@app.callback(
Output({"type": "datatable-plate", "index": MATCH}, "rowData"),
Input("update-rows-trigger-div", "children"), # broadcast trigger
@@ -819,10 +976,11 @@ def sync_plate_style(cell_evt, row_data, virtual_row_data, fig):
Input({"type": "pcr-curve-store", "index": MATCH}, "data"), # for qPCR plates, to show efficiency etc
# Input({"type": "phix-control-dropdown", "index": MATCH}, "value"), # to highlight the selected phix control value
State({"type": "datatable-plate", "index": MATCH}, "id"), # gives us the plate_id (via id["index"])
+ State("flagging-conditions-storage", "data"), # to apply flagging conditions
prevent_initial_call=True
)
# def update_rows_callback(_, app_data_state_json, pcr_store, phix_control_value, table_id):
-def update_rows_callback(_, app_data_state_json, pcr_store, table_id):
+def update_rows_callback(_, app_data_state_json, pcr_store, table_id, current_flagging):
if not app_data_state_json or not table_id:
raise PreventUpdate
@@ -848,14 +1006,19 @@ def update_rows_callback(_, app_data_state_json, pcr_store, table_id):
is_qpcr = (plate.plate_type == "qPCR")
df = plate.dataset(app_state, tidy=False).replace(['None', 'none', 'NaN', 'nan', ''], np.nan).infer_objects(copy=False)
- df = build_computed_molarity(app_state, plate, slope, intercept, phix_correction_factor)
+ df = build_computed_molarity(app_state, plate, slope, intercept, phix_correction_factor)
+ per_plate_flagging = current_flagging.get(plate_id, {})
+ df = collect_flagging_from_all(df, per_plate_flagging)
+
+ if "__error__" not in df.columns:
+ df["__error__"] = False
# 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")
else:
# drop all-NaN columns except 'sample_id'
- drop_cols = [c for c in df.columns if c != "sample_id" and df[c].isna().all()]
+ drop_cols = [c for c in df.columns if c not in {"sample_id","flagging_conditions","well_note"} and df[c].isna().all()]
if drop_cols:
df = df.drop(columns=drop_cols)
@@ -866,172 +1029,255 @@ def update_rows_callback(_, app_data_state_json, pcr_store, table_id):
df["__error__"] = False
df["__warning__"] = False
+ if "flagging_conditions" in df.columns:
+ has_flag_issue = df["flagging_conditions"].astype(str).str.strip() != ""
+ df["__error__"] = df["__error__"] | has_flag_issue
+
return df.to_dict("records")
+
+
+
@app.callback(
- Output("plate-button-container", "children"),
- [Input("app_data_state", "data"),
- Input("dynamic-tabs", "active_tab")],
- prevent_initial_call=True
+ [
+ Output("alert-flagging-conditions", "is_open", allow_duplicate=True),
+ Output("alert-flagging-conditions", "children", allow_duplicate=True),
+ ],
+ # We re-run this alert logic whenever rowData changes
+ Input({"type": "datatable-plate", "index": ALL}, "rowData"),
+ State({"type": "datatable-plate", "index": ALL}, "id"),
+ prevent_initial_call=True,
)
-def update_plate_buttons(app_data_state_json, selected_tab):
-
- if selected_tab is None or app_data_state_json is None:
- return
- else:
- plate_id = selected_tab
- app_state = AppState.from_json(app_data_state_json)
- plate = app_state.plate_by_id.get(plate_id)
- df = plate.dataset(app_state, tidy=False)
-
- # Only consider rows where sample_type == "PhiX"
- if "sample_type" in df.columns:
- phix_rows = df[df["sample_type"] == "PhiX"]
- else:
- phix_rows = pd.DataFrame()
-
- # Prepare dropdown options
- dropdown_options = []
- plate_type = plate.plate_type
- for _, row in phix_rows.iterrows():
- well = row.get("well_position_id")
- if plate_type.lower() == "qpcr":
- colum_value = row.get("Cq")
- plate_type_display = "qPCR"
- else:
- # Finds the col for Region Molarity dynamically no mater what the unit is in the ending
- region_molarity_col = next((col for col in row.index if col.startswith("Region Molarity")))
- colum_value = row.get(region_molarity_col)
- plate_type_display = "TS"
- label = f"{plate_type_display} - PhiX - {well} - {colum_value}"
- value = str(colum_value)
- dropdown_options.append({"label": label, "value": value})
-
- return html.Div(
- [
- # --- Control by PhiX Dropdown ---
- dbc.Label(
- "Control by PhiX",
- className="mt-2 mb-1 text-muted fw-semibold",
- ),
- dcc.Dropdown(
- id={"type": "phix-control-dropdown", "index": plate_id},
- options=dropdown_options,
- placeholder="Select or enter control value...",
- className="mb-2 small", # reduce dropdown text size
- ),
+def update_flagging_alert(all_rowdata_per_plate, all_table_ids):
+ """
+ Build the global 'flagging conditions' alert based on the CURRENT contents
+ of every plate table.
- # --- Adjust Flagging Button ---
- dbc.Button(
- "Adjust Flagging Condition",
- id={"type": "adjust-flagging-btn", "index": plate_id},
- color="secondary",
- className="w-100 mb-2 py-2 fw-semibold small shadow-sm",
- ),
- html.Div(
- id={"type": "dummy-flagging-output", "index": plate_id},
- style={"display": "none"},
- ),
- ],
- className="p-2 bg-light rounded shadow-sm", # optional card-like background
+ all_rowdata_per_plate: list[ list[ rowDict, ... ] ], one entry per plate table
+ all_table_ids: list[ {"type":"datatable-plate","index":}, ... ],
+ parallel to all_rowdata_per_plate
+ """
+
+ messages = []
+
+ # pair up each table's id with its rows
+ for table_id_obj, rows in zip(all_table_ids, all_rowdata_per_plate):
+ plate_id = str(table_id_obj.get("index"))
+
+ if not rows:
+ continue
+
+ # Find wells that currently violate flagging_conditions on this plate
+ violating_wells = []
+ for r in rows:
+ # r is each row dict (the records from df.to_dict("records"))
+ flag_text = str(r.get("flagging_conditions") or "").strip()
+ sample_type = r.get("sample_type")
+ well_id = r.get("well_position_id")
+
+ if (
+ flag_text != "" and
+ sample_type in ("Library", "Positive Control")
+ ):
+ violating_wells.append(str(well_id))
+
+ if violating_wells:
+ # unique & sorted for pretty output
+ wells_str = ", ".join(sorted(set(violating_wells)))
+ messages.append(
+ f"Plate {plate_id}: wells {wells_str} exceed the defined flagging thresholds."
+ )
+
+ # no violations → hide the alert
+ if not messages:
+ return False, ""
+
+ # build a nice bullet list
+ items = [html.Li(msg) for msg in messages]
+ items.append(
+ html.Li(
+ "To resolve these warnings: Adjust 'Flagging Condition' thresholds or inspect those wells.",
+ style={"listStyleType": "none", "marginTop": "10px"}
+ )
)
+ alert_children = html.Ul(items)
+
+ return True, alert_children
+
+
+
+
@app.callback(
- Output({"type": "dummy-flagging-output", "index": MATCH}, "children"),
- Input({"type": "adjust-flagging-btn", "index": MATCH}, "n_clicks"),
- prevent_initial_call=True
+ Output({"type": "adjust-flagging-btn", "index": ALL}, "style"),
+ Input("dynamic-tabs", "active_tab"),
+ State({"type": "adjust-flagging-btn", "index": ALL}, "id"),
)
-def on_adjust_flagging_clicked(n_clicks):
- plate_id = ctx.triggered_id["index"]
- print(f"Adjust flagging condition button is pressed for plate {plate_id}")
- return ""
+def show_only_active_flagging_button(active_tab, all_ids):
+ return [
+ {} if btn_id["index"] == active_tab else {"display": "none"}
+ for btn_id in all_ids
+ ]
+
+
+
+@app.callback(
+ Output({"type": "phix-control-wrapper", "index": ALL}, "style"),
+ Input("dynamic-tabs", "active_tab"),
+ State({"type": "phix-control-wrapper", "index": ALL}, "id"),
+)
+def show_only_active_phix_drop(active_tab, all_ids):
+ return [
+ {} if btn_id["index"] == active_tab else {"display": "none"}
+ for btn_id in all_ids
+ ]
+
+
+
+@app.callback(
+ Output({"type": "phix-control-dropdown", "index": MATCH}, "options"),
+ [
+ Input("app_data_state", "data"),
+ Input("dynamic-tabs", "active_tab"),
+ ],
+ State({"type": "phix-control-dropdown", "index": MATCH}, "id"),
+ prevent_initial_call=True,
+)
+def refresh_phix_dropdown_options(app_data_state_json, active_tab, this_dropdown_id):
+ """
+ Keeps the PhiX dropdown OPTIONS up to date for each plate.
+ This runs separately from build_tab_body(), so content stays fresh.
+ """
+
+ if app_data_state_json is None:
+ return []
+
+ app_state = AppState.from_json(app_data_state_json)
+
+ # which plate does THIS dropdown belong to?
+ pid_for_this_dropdown = this_dropdown_id["index"]
+ plate = app_state.plate_by_id.get(pid_for_this_dropdown)
+ if plate is None:
+ return []
+
+ is_qpcr = (plate.plate_type == "qPCR")
+
+ df = plate.dataset(app_state, tidy=False)
+
+ if "sample_type" in df.columns:
+ phix_rows = df[df["sample_type"] == "PhiX"]
+ else:
+ phix_rows = pd.DataFrame()
+
+ dropdown_options = []
+ plate_type_display = "qPCR" if is_qpcr else "TS"
+
+ for _, row in phix_rows.iterrows():
+ well = row.get("well_position_id")
+
+ if is_qpcr:
+ colum_value = row.get("Cq")
+ else:
+ # dynamically find "Region Molarity ..." column
+ region_molarity_col = next(
+ (col for col in row.index if col.startswith("Region Molarity")),
+ None
+ )
+ colum_value = row.get(region_molarity_col) if region_molarity_col else None
+
+ label = f"{plate_type_display} - PhiX - {well} - {colum_value}"
+ value = "" if colum_value is None else str(colum_value)
+
+ dropdown_options.append({"label": label, "value": well})
+
+ return dropdown_options
+
+
# @app.callback(
-# [Output({"type": "flagging-modal", "index": MATCH}, "is_open"),
-# Output({"type": "flagging-modal-body", "index": MATCH}, "children")],
-# Input({"type": "adjust-flagging-btn", "index": MATCH}, "n_clicks"),
-# State("app_data_state", "data"),
-# prevent_initial_call=True
-# )
-# @app.callback(
-# [Output("flagging-modal", "is_open"),
-# Output("flagging-modal-body", "children")],
-# Input({"type": "adjust-flagging-btn", "index": ALL}, "n_clicks"),
-# State("app_data_state", "data"),
+# Output("plate-button-container", "children"),
+# [Input("app_data_state", "data"),
+# Input("dynamic-tabs", "active_tab")],
# prevent_initial_call=True
# )
-# def show_flagging_modal(n_clicks_list, app_data_json):
-# triggered = ctx.triggered_id
-# if not isinstance(triggered, dict) or triggered.get("type") != "adjust-flagging-btn":
-# raise PreventUpdate
-
-# plate_id = triggered["index"]
-# btn_index = next(
-# (i for i, btn in enumerate(ctx.inputs_list[0]) if btn["id"]["index"] == plate_id),
-# None
+# def update_plate_buttons(app_data_state_json, selected_tab):
+
+# if selected_tab is None or app_data_state_json is None:
+# return
+# else:
+# plate_id = selected_tab
+# app_state = AppState.from_json(app_data_state_json)
+# plate = app_state.plate_by_id.get(plate_id)
+# df = plate.dataset(app_state, tidy=False)
+
+# # Only consider rows where sample_type == "PhiX"
+# if "sample_type" in df.columns:
+# phix_rows = df[df["sample_type"] == "PhiX"]
+# else:
+# phix_rows = pd.DataFrame()
+
+# # Prepare dropdown options
+# dropdown_options = []
+# plate_type = plate.plate_type
+# for _, row in phix_rows.iterrows():
+# well = row.get("well_position_id")
+# if plate_type.lower() == "qpcr":
+# colum_value = row.get("Cq")
+# plate_type_display = "qPCR"
+# else:
+# # Finds the col for Region Molarity dynamically no mater what the unit is in the ending
+# region_molarity_col = next((col for col in row.index if col.startswith("Region Molarity")))
+# colum_value = row.get(region_molarity_col)
+# plate_type_display = "TS"
+# label = f"{plate_type_display} - PhiX - {well} - {colum_value}"
+# value = str(colum_value)
+# dropdown_options.append({"label": label, "value": value})
+
+# return html.Div(
+# [
+# # --- Control by PhiX Dropdown ---
+# dbc.Label(
+# "Control by PhiX",
+# className="mt-2 mb-1 text-muted fw-semibold",
+# ),
+# dcc.Dropdown(
+# id={"type": "phix-control-dropdown", "index": plate_id},
+# options=dropdown_options,
+# placeholder="Select or enter control value...",
+# className="mb-2 small", # reduce dropdown text size
+# ),
+# ],
+# className="p-2 bg-light rounded shadow-sm", # optional card-like background
# )
-# if btn_index is None or n_clicks_list[btn_index] is None or n_clicks_list[btn_index] == 0:
-# raise PreventUpdate
-
-# app_state = AppState.from_json(app_data_json)
-# plate = next((p for p in app_state.plate_registry if p.plate_id == plate_id), None)
-# if plate is None:
-# raise PreventUpdate
-
-# # Plate type specific inputs
-# if plate.plate_type == "qPCR":
-# inputs = html.Div([
-# html.Label("Std. Dev:"),
-# dcc.Input(type="number", value=0.2, step=0.01, id="qpcr-std-dev", className="form-control"),
-# html.Br(), html.Label("Min nM:"),
-# dcc.Input(type="number", value=0.8, step=0.01, id="qpcr-min-nm", className="form-control"),
-# html.Br(), html.Label("Max nM:"),
-# dcc.Input(type="number", value=30, step=0.1, id="qpcr-max-nm", className="form-control"),
-# ])
-# elif plate.plate_type == "HSD1000":
-# min_frag, max_frag = 180, 900
-# min_molarity, max_molarity = 200, 20000 # in pM
-# molarity_unit = "pM"
-# molarity_step = 10
-# elif plate.plate_type == "D1000":
-# min_frag, max_frag = 180, 900
-# min_molarity, max_molarity = 2, 60 # in nM
-# molarity_unit = "nM"
-# molarity_step = 0.1
-# elif plate.plate_type == "D5000":
-# min_frag, max_frag = 250, 2500
-# min_molarity, max_molarity = 2, 60 # in nM
-# molarity_unit = "nM"
-# molarity_step = 0.1
-
-# if plate.plate_type != "qPCR":
-# inputs = html.Div([
-# html.Label("Min Frag length:"),
-# dcc.Input(type="number", value=min_frag, step=1, id="ts-min-frag", className="form-control"),
-# html.Br(), html.Label("Max Frag length:"),
-# dcc.Input(type="number", value=max_frag, step=1, id="ts-max-frag", className="form-control"),
-# html.Br(), html.Label(f"Min Molarity ({molarity_unit}):"),
-# dcc.Input(type="number", value=min_molarity, step=molarity_step, id="ts-min-nm", className="form-control"),
-# html.Br(), html.Label(f"Max Molarity ({molarity_unit}):"),
-# dcc.Input(type="number", value=max_molarity, step=molarity_step, id="ts-max-nm", className="form-control"),
-# ])
-
-# return True, inputs
+
@app.callback(
Output({"type": "flagging-modal", "index": MATCH}, "is_open"),
- [
- Input({"type": "flagging-modal-close", "index": MATCH}, "n_clicks"),
- Input({"type": "flagging-modal-save", "index": MATCH}, "n_clicks"),
- ],
- prevent_initial_call=True
+ Input({"type": "adjust-flagging-btn", "index": MATCH}, "n_clicks"),
+ Input({"type": "flagging-modal-close", "index": MATCH}, "n_clicks"),
+ State({"type": "flagging-modal", "index": MATCH}, "is_open"),
+ State({"type": "input-stddev", "index": ALL}, "value"),
+ State({"type": "input-stddev", "index": ALL}, "id"),
+ prevent_initial_call=True,
)
-def close_modal_matched(n_close, n_save):
- return False
+def toggle_flagging_modal(adjust_btn, close_btn, is_open, stddev_vals, stddev_ids):
+ # Coalesce None -> False so we always return a boolean
+ is_open = bool(is_open)
+ tid = getattr(ctx, "triggered_id", None)
+
+ if isinstance(tid, dict) and tid.get("type") == "adjust-flagging-btn" and (adjust_btn or 0) > 0:
+ return True
+ if isinstance(tid, dict) and tid.get("type") == "flagging-modal-close" and (close_btn or 0) > 0:
+ return False
+
+ # First render or unrelated trigger -> keep current state
+ return is_open
+
+
@app.callback(
diff --git a/models.py b/models.py
index e89a72d..e1fa873 100644
--- a/models.py
+++ b/models.py
@@ -103,6 +103,7 @@ class AppSpecificPlateWell(BaseModel):
replicate_group: Optional[str] = "" # qPCR
# sample_description: Optional[str] = "" # Tube ID
well_note: Optional[str] = "" # sample found or not
+ flagging_conditions: Optional[str] = ""
dilution_factor: Optional[float] = 1
diff --git a/utils/flagging_conditions.py b/utils/flagging_conditions.py
new file mode 100644
index 0000000..2ab599c
--- /dev/null
+++ b/utils/flagging_conditions.py
@@ -0,0 +1,102 @@
+# utils/flagging_conditions.py
+from typing import Dict, Any
+import pandas as pd
+import re
+
+def safe_float(v):
+ """Convert v to float safely — ignores units like 'nM' or 'pM'."""
+ if v in [" - ", "-", None, ""]:
+ return None
+
+ if isinstance(v, (int, float)):
+ return float(v)
+
+ if isinstance(v, str):
+ s = v.strip().replace(",", ".")
+ parts = s.split()
+ return float(parts[0])
+
+ return None
+
+
+
+
+
+def first_col_startswith(df, prefix):
+ """Return the first column whose name starts with the given prefix."""
+ pat = re.compile(rf"^{re.escape(prefix)}\b", flags=re.IGNORECASE)
+ for col in df.columns:
+ if pat.search(str(col).strip()):
+ return col
+ return None
+
+
+def evaluate_flagging_for_df(df: pd.DataFrame, conditions: Dict[str, Any]) -> pd.Series:
+ """
+ Evaluate flagging for one DataFrame and return a Series with text flags.
+ """
+ conditions = conditions or {}
+
+ # thresholds
+ min_frag = conditions.get("min_frag")
+ max_frag = conditions.get("max_frag")
+ min_mol_plate = conditions.get("min_mol")
+ max_mol_plate = conditions.get("max_mol")
+ stddev = conditions.get("stddev")
+ min_nm = conditions.get("min_nm")
+ max_nm = conditions.get("max_nm")
+
+ avg_col = first_col_startswith(df, "Average Size")
+ reg_mol_col = first_col_startswith(df, "Region Molarity")
+
+ flags = []
+
+ for _, row in df.iterrows():
+ parts = []
+
+ # Tapestation: Average Size
+ if avg_col is not None:
+ avg = safe_float(row.get(avg_col))
+ if avg is not None:
+ if min_frag is not None and avg < min_frag:
+ parts.append("average size too low")
+ if max_frag is not None and avg > max_frag:
+ parts.append("average size too high")
+
+ # Tapestation: Region Molarity
+ if reg_mol_col is not None:
+ rmol = safe_float(row.get(reg_mol_col))
+ if rmol is not None:
+ if min_mol_plate is not None and rmol < min_mol_plate:
+ parts.append("region molarity too low")
+ if max_mol_plate is not None and rmol > max_mol_plate:
+ parts.append("region molarity too high")
+
+ # qPCR: standard error threshold
+ if "computed_standard_error" in df.columns and stddev is not None:
+ se = safe_float(row.get("computed_standard_error"))
+ if se is not None and se > stddev:
+ parts.append("standard error too high")
+
+ # qPCR: computed molarity in [min_nm, max_nm]
+ if "computed_molarity" in df.columns:
+ cm = safe_float(row.get("computed_molarity"))
+ if cm is not None:
+ if min_nm is not None and cm < min_nm:
+ parts.append("computed molarity too low")
+ if max_nm is not None and cm > max_nm:
+ parts.append("computed molarity too high")
+
+ flags.append(", ".join(parts))
+
+ return pd.Series(flags, index=df.index)
+
+
+def collect_flagging_from_all(df: pd.DataFrame, current_flagging: Dict[str, Any]) -> pd.DataFrame:
+ """
+ Single-DF variant used in your tab builder.
+ Returns a COPY of df with a 'flagging_conditions' column attached.
+ """
+ df_out = df.copy()
+ df_out["flagging_conditions"] = evaluate_flagging_for_df(df_out, current_flagging)
+ return df_out
diff --git a/utils/plate_render_utils.py b/utils/plate_render_utils.py
index aad9fe5..193494e 100644
--- a/utils/plate_render_utils.py
+++ b/utils/plate_render_utils.py
@@ -7,6 +7,7 @@
from app_state import AppState
import plotly.graph_objects as go
import datetime
+from utils.flagging_conditions import collect_flagging_from_all
pd.set_option('future.no_silent_downcasting', True)
@@ -47,6 +48,7 @@ def is_numeric(val):
'Concentration',
'Concentration Error',
'Concentration Mean',
+ 'Color',
'Cq Error',
'EPF',
'Slope',
@@ -68,17 +70,16 @@ def is_numeric(val):
QPCR_SAMPLE_DILUTION_COEFFICIENT = 10000
-
-def make_tab(plate, app_state):
+def make_tab(plate, app_state, current_flagging):
pid = str(plate.plate_id)
- from utils.plate_render_utils import build_tab_body
+ tab_components, flag_button, phix_dropdown, flagging_warnings = build_tab_body(plate, app_state, current_flagging)
return dbc.Tab(
label=plate.plate_type if plate.plate_type != "qPCR" else "qPCR",
tab_id=pid,
id={"type": "tab", "index": pid},
- children=build_tab_body(plate, app_state),
+ children=tab_components,
style={"paddingRight": "0px"},
- )
+ ), flag_button, phix_dropdown, flagging_warnings
def get_tab_id(tab_like):
# works for Component or serialized dict from State
@@ -269,35 +270,12 @@ def _build_plate_figure_for_plate(plate, app_state):
if w in df["well"].values and st:
df.loc[df["well"] == w, "group"] = st
- # --- error/warning border mapping from plate dataset (mirror AG Grid logic) ---
- error_wells, warn_wells = set(), set()
-
- df_table = plate.dataset(app_state, tidy=False)
- # Only non-qPCR have well_note-based flags in your grid code
- if getattr(plate, "plate_type", "") != "qPCR" and ("well_note" in df_table.columns):
- error_wells = set(df_table.loc[
- df_table["well_note"].isin(["No sample found", "Multiple samples found"]),
- "well_position_id"
- ].astype(str))
- warn_wells = set(df_table.loc[
- df_table["well_note"] == "Unable to Update",
- "well_position_id"
- ].astype(str))
-
# --- colors: fill + per-well border ---
def fill_color(g):
return SAMPLE_TYPE_COLORS.get(g, UNASSIGNED_COLOR) if g != "Unassigned" else UNASSIGNED_COLOR
marker_colors = [fill_color(g) for g in df["group"]]
-
line_colors = []
line_widths = []
- for w in df["well"]:
- 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)
- else:
- line_colors.append("#222"); line_widths.append(BORDER_PX)
# --- figure ---
fig = go.Figure(
@@ -336,7 +314,134 @@ def fill_color(g):
return fig
-def build_tab_body(plate, app_state):
+def build_flagging_body(plate_type: str, index: str, current_flagging):
+ """
+ Build the modal inputs for this plate, prefilling with previously used values if available.
+
+ Returns:
+ (updated_flagging: dict, body_div: html.Div)
+
+ updated_flagging = the effective flagging values that the user sees.
+ This guarantees that if current_flagging was empty or missing keys,
+ we still pass back the defaults.
+ """
+ current_flagging = current_flagging or {}
+
+ if plate_type == "qPCR":
+ # defaults
+ stddev_default = 0.2
+ min_nm_default = 0.8
+ max_nm_default = 30
+
+ # use saved values if present, otherwise defaults
+ stddev_val = current_flagging.get("stddev", stddev_default)
+ min_nm_val = current_flagging.get("min_nm", min_nm_default)
+ max_nm_val = current_flagging.get("max_nm", max_nm_default)
+
+ # this is now the authoritative config for this plate
+ updated_flagging = {
+ "stddev": stddev_val,
+ "min_nm": min_nm_val,
+ "max_nm": max_nm_val,
+ }
+
+ body_div = html.Div([
+ dbc.Label("Std. Dev:"),
+ dcc.Input(
+ id={"type": "input-stddev", "index": index},
+ type="number",
+ value=stddev_val,
+ step=0.01,
+ className="form-control",
+ ),
+ html.Br(), dbc.Label("Min nM:"),
+ dcc.Input(
+ id={"type": "input-min-nm", "index": index},
+ type="number",
+ value=min_nm_val,
+ step=0.01,
+ className="form-control",
+ ),
+ html.Br(), dbc.Label("Max nM:"),
+ dcc.Input(
+ id={"type": "input-max-nm", "index": index},
+ type="number",
+ value=max_nm_val,
+ step=0.1,
+ className="form-control",
+ ),
+ ], style={"minWidth": 240})
+
+ return updated_flagging, body_div
+
+
+ elif plate_type in {"HSD1000", "D1000", "D5000"}:
+ # plate-type defaults
+ if plate_type == "HSD1000":
+ molarity_unit, molarity_step = "pM", 10
+ min_frag_default, max_frag_default, min_mol_default, max_mol_default = 180, 900, 200, 20000
+ elif plate_type == "D5000":
+ molarity_unit, molarity_step = "nM", 0.1
+ min_frag_default, max_frag_default, min_mol_default, max_mol_default = 250, 2500, 2, 60
+ else: # D1000
+ molarity_unit, molarity_step = "nM", 0.1
+ min_frag_default, max_frag_default, min_mol_default, max_mol_default = 180, 900, 2, 60
+
+ # use saved values if present, otherwise defaults
+ min_frag_val = current_flagging.get("min_frag", min_frag_default)
+ max_frag_val = current_flagging.get("max_frag", max_frag_default)
+ min_mol_val = current_flagging.get("min_mol", min_mol_default)
+ max_mol_val = current_flagging.get("max_mol", max_mol_default)
+
+ # this becomes the authoritative config for this plate
+ updated_flagging = {
+ "min_frag": min_frag_val,
+ "max_frag": max_frag_val,
+ "min_mol": min_mol_val,
+ "max_mol": max_mol_val,
+ }
+
+ body_div = html.Div([
+ dbc.Label("Min Frag length:"),
+ dcc.Input(
+ id={"type": "input-min-frag", "index": index},
+ type="number",
+ value=min_frag_val,
+ step=1,
+ className="form-control",
+ ),
+ html.Br(), dbc.Label("Max Frag length:"),
+ dcc.Input(
+ id={"type": "input-max-frag", "index": index},
+ type="number",
+ value=max_frag_val,
+ step=1,
+ className="form-control",
+ ),
+ html.Br(), dbc.Label(f"Min Molarity ({molarity_unit}):"),
+ dcc.Input(
+ id={"type": "input-min-mol", "index": index},
+ type="number",
+ value=min_mol_val,
+ step=molarity_step,
+ className="form-control",
+ ),
+ html.Br(), dbc.Label(f"Max Molarity ({molarity_unit}):"),
+ dcc.Input(
+ id={"type": "input-max-mol", "index": index},
+ type="number",
+ value=max_mol_val,
+ step=molarity_step,
+ className="form-control",
+ ),
+ ], style={"minWidth": 240})
+
+ return updated_flagging, body_div
+
+
+
+
+def build_tab_body(plate, app_state, current_flagging):
"""
Build the tab *children* for one plate. Returns a Python list of components.
Nothing here creates a dbc.Tab — that's done by make_tab().
@@ -418,11 +523,24 @@ def build_tab_body(plate, app_state):
df = build_computed_molarity(app_state, plate, slope, intercept)
if is_qpcr:
- df = df.dropna(axis=1, how="all")
+ keep = {"flagging_conditions"}
+ null_only = [c for c in df.columns if c not in keep and df[c].isna().all()]
+ df = df.drop(columns=null_only)
else:
- drop_cols = [c for c in df.columns if c != "sample_id" and df[c].isna().all()]
+ drop_cols = [c for c in df.columns if c not in {"sample_id","flagging_conditions","well_note"} and df[c].isna().all()]
if drop_cols:
df = df.drop(columns=drop_cols)
+
+ # get the phix value for this plate from current_flagging before we loose it in build_flagging_body
+ phix_value = (current_flagging or {}).get("phix_control_well_position", "")
+
+ current_flagging, flagging_body = build_flagging_body(plate.plate_type, pid, current_flagging)
+
+ #print("df", df.columns)
+ #print("current_flagging", current_flagging)
+
+ # Adds the flagging conditoins to the df. based on this we do the coloring and warning.
+ df = collect_flagging_from_all(df, current_flagging)
# error/warn flags for Tapestation-like plates
if (not is_qpcr) and ("well_note" in df.columns):
@@ -432,6 +550,21 @@ def build_tab_body(plate, app_state):
df["__error__"] = False
df["__warning__"] = False
+ flagging_warning_rows = []
+
+ if "flagging_conditions" in df.columns:
+ has_flag_issue = df["flagging_conditions"].astype(str).str.strip() != ""
+ df["__error__"] = df["__error__"] | has_flag_issue
+
+ # collect human-readable warnings for this plate
+ if has_flag_issue.any():
+ # we keep it simple: 1 warning per plate, like:
+ # "Plate : One or more wells violate the flagging thresholds."
+ flagging_warning_rows = [
+ f"Plate {plate.plate_id}: One or more wells exceed the defined flagging thresholds."
+ ]
+
+
getRowStyle = {
"styleConditions": [
{
@@ -591,6 +724,68 @@ def _is_num(s):
# --- Buttons row ---
initial_display = "none" if len(df) <= 1 else "inline-flex"
+
+
+
+ flag_button = dbc.Button(
+ "Adjust Flagging Condition",
+ id={"type": "adjust-flagging-btn", "index": pid},
+ color="secondary",
+ className="ms-2 mb-2 py-2 fw-semibold small shadow-sm",
+ )
+
+ # ------------------------------------------------------------------
+ # Build the PhiX dropdown
+ # ------------------------------------------------------------------
+
+ # Find rows where sample_type == "PhiX"
+ if "sample_type" in df.columns:
+ phix_rows = df[df["sample_type"] == "PhiX"]
+ else:
+ phix_rows = pd.DataFrame()
+
+ dropdown_options = []
+ plate_type_display = "qPCR" if is_qpcr else "TS"
+
+ for _, row in phix_rows.iterrows():
+ well = row.get("well_position_id")
+
+ if is_qpcr:
+ colum_value = row.get("Cq")
+ else:
+ # dynamically find "Region Molarity ..." column
+ region_molarity_col = next(
+ (col for col in row.index if col.startswith("Region Molarity")),
+ None
+ )
+ colum_value = row.get(region_molarity_col) if region_molarity_col else None
+
+ label = f"{plate_type_display} - PhiX - {well} - {colum_value}"
+ value = "" if colum_value is None else str(colum_value)
+
+ dropdown_options.append({"label": label, "value": well})
+
+ phix_dropdown = html.Div(
+ [
+ dbc.Label(
+ "Control by PhiX",
+ className="mt-2 mb-1 text-muted fw-semibold",
+ ),
+ dcc.Dropdown(
+ id={"type": "phix-control-dropdown", "index": pid},
+ options=dropdown_options,
+ value=phix_value,
+ placeholder="Select or enter control value...",
+ className="mb-2 small",
+ ),
+ ],
+ id={"type": "phix-control-wrapper", "index": pid},
+ className="p-2 bg-light rounded shadow-sm",
+ )
+
+
+
+
buttons_row = html.Div(
[
dbc.Button(
@@ -623,7 +818,7 @@ def _is_num(s):
],
style={"gap": "0.5rem", "alignItems": "center",
"paddingInline": "0.75rem", "fontWeight": "500"},
- ),
+ ),
],
style={"display": "flex", "gap": "10px", "marginTop": "10px"},
)
@@ -700,18 +895,19 @@ def _is_num(s):
flagging_modal = html.Div([
dbc.Modal(
[
- dbc.ModalHeader(dbc.ModalTitle("Adjust Flagging Conditions")),
- dbc.ModalBody(id={"type": "flagging-modal-body", "index": pid}),
+ dbc.ModalHeader("Adjust Flagging"),
+ dbc.ModalBody(flagging_body, id={"type": "flagging-modal-body", "index": pid}),
dbc.ModalFooter([
- dbc.Button("Save", id={"type": "flagging-modal-save", "index": pid}, color="primary"),
- dbc.Button("Close", id={"type": "flagging-modal-close", "index": pid}, className="ms-auto"),
+ dbc.Button("Close", id={"type": "flagging-modal-close", "index": pid}),
]),
],
- id={"type": "modal-flagging", "index": pid},
- is_open=False, backdrop="static",
+ id={"type": "flagging-modal", "index": pid},
+ is_open=False,
)
])
+
+
delete_rows_modal = html.Div([
dbc.Modal(
[
@@ -747,4 +943,5 @@ def _is_num(s):
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
+ tab_components = [table_wrap, plate_wrap, buttons_row, delete_plate_modal, flagging_modal, delete_rows_modal, dummy]
+ return tab_components, flag_button, phix_dropdown, flagging_warning_rows
\ No newline at end of file
diff --git a/utils/plates_callback_utils.py b/utils/plates_callback_utils.py
index 6bdd985..4f8f477 100644
--- a/utils/plates_callback_utils.py
+++ b/utils/plates_callback_utils.py
@@ -147,6 +147,10 @@ def update_app_state_from_ui(app_state, all_ui_tables):
if pw:
+ # flagging_conditions back into the PlateWell
+ if "flagging_conditions" in df_ui.columns:
+ pw.flagging_conditions = row.get("flagging_conditions", "")
+
if "dilution_factor" in df_ui.columns:
dilution_factor = row["dilution_factor"]
pw.dilution_factor = dilution_factor
diff --git a/utils/warning_utils.py b/utils/warning_utils.py
index 9650035..3973341 100644
--- a/utils/warning_utils.py
+++ b/utils/warning_utils.py
@@ -15,6 +15,8 @@ def collect_missing_sample_warnings(app_state):
missing = []
for w in plate.plate_wells:
+ #print("flagging condition:", w.flagging_conditions)
+ #print("Well note:", w.well_note)
sample = w.get_sample(app_state)
sample_type = getattr(sample, "sample_type", None)
if (