diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 86ecd54b..08154b94 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import Literal +import numpy as np import pandas as pd import streamlit as st @@ -447,7 +448,9 @@ def register_user_changes( columns: str, values: str, df_tab: pd.DataFrame, + df_orig: pd.DataFrame, key: str, + editor_key: str, ): """ Register all user changes in the session state variable "user_changes_df". @@ -456,13 +459,29 @@ def register_user_changes( """ # convert session state dict to dataframe: # Create a list of dictionaries - data_dict = st.session_state[key]["edited_rows"] + data_dict = st.session_state[editor_key]["edited_rows"] if any(data_dict): data_list = [] + rejected_changes = False for k, v in data_dict.items(): for c_name, value in v.items(): - data_list.append({index: k, columns: c_name, values: value}) + if np.isnan(df_orig.iloc[k, :][c_name]): + msg = ( + f":exclamation: Cannot modify empty value '{c_name}' " + f"for '{df_orig.index[k]}'" + ) + st.toast(msg) + rejected_changes = True + else: + data_list.append({index: k, columns: c_name, values: value}) + + if rejected_changes: + # modify key number + st.session_state[f"{key}_number"] += 1 + + if len(data_list) == 0: + return # Convert the list to a DataFrame res = pd.DataFrame(data_list) @@ -544,13 +563,28 @@ def display_and_edit_input_data( data type "conversion_processes" and "transportation_processes" which is not region specific. key : str - A key for the data editing streamlit widget. Need to be unique. + A key for the data editing layout element. Needs to be unique in the app. + Several session state variables are derived from this key:: + + - st.session_state[f"{key}_number"]: + This is initialized with 0 and incremented by 1 whenever any input + value got rejected by the callback function + :func:`register_user_changes`. This will trigger a re-rendering of the + data_editor widget and thus reset modifications on empty values. + - st.session_state[f"{key}_form"]: + the key for the form the editor lives in + - st_session_state[f"{key}_{st.session_state[f'{key}_number']}"]: + the name of this session state variable consists of the `key` and the + current `{key}_number`. It refers to the st.data_editor widget. + Whenever the key_number changes, the editor widget gets a new key and + is initialized from scratch. Returns ------- pd.DataFrame """ df = get_data_type_from_input_data(api, data_type=data_type, scope=scope) + df_orig = df.copy() if data_type in [ "electricity_generation", @@ -605,6 +639,10 @@ def display_and_edit_input_data( # if editing is enabled, store modifications in session_state: if st.session_state["edit_input_data"]: + if f"{key}_number" not in st.session_state: + st.session_state[f"{key}_number"] = 0 + editor_key = f"{key}_{st.session_state[f'{key}_number']}" + with st.form(key=f"{key}_form"): st.info( ( @@ -613,17 +651,10 @@ def display_and_edit_input_data( ) ) - if df.isna().any().any(): - # TODO: remove this warning when input data is fixed. - msg = ( - "Draft version: Do no edit empty data points (None), this will " - "throw an error. Missing data has to be filled before publishing." - ) - st.warning(msg) df = st.data_editor( df, use_container_width=True, - key=key, + key=editor_key, num_rows="fixed", disabled=[index], column_config=column_config, @@ -639,7 +670,9 @@ def display_and_edit_input_data( "columns": columns, "values": "value", "df_tab": df, + "df_orig": df_orig, "key": key, + "editor_key": editor_key, }, ) else: