from datetime import datetime import pandas as pd import numpy as np import streamlit as st from st_aggrid import AgGrid, GridUpdateMode, JsCode from backend.db import DB, Session import backend.his_io as his_io from session_ui import session_ui from stats_ui import stats_ui st.set_page_config(layout='wide') # Open database try: db = DB() except Exception as e: st.error('Datenbank konnte nicht geladen werden.') st.exception(e) st.stop() # Init session state if 'session' not in st.session_state: st.session_state.session = None if 'last_session_token' not in st.session_state: st.session_state.last_session_token = None # Load session def load_session() -> Session: session = session_ui(db, st.session_state.session) return session def store_session(session: Session): session.edited = datetime.now() db.save_session(session) st.session_state.session = load_session() if st.session_state.session is None: st.stop() # Save and export @st.cache_data def export(points_df: pd.DataFrame, his_header: pd.DataFrame, exercise_columns: list[str], grades_table: pd.DataFrame): return his_io.export(points_df, his_header, exercise_columns, grades_table) # Set some default UI values from the session if st.session_state.session.token != st.session_state.last_session_token: st.session_state.grade_range = (st.session_state.session.grade_4_0, st.session_state.session.grade_1_0) st.session_state.max_points = st.session_state.session.max_points st.session_state.last_session_token = st.session_state.session.token #export.clear() st.write(f':blue[{st.session_state.session.table_name}]') st.write(f':gray[Erstellt:] {st.session_state.session.created.strftime("%d.%m.%Y %H:%M:%S")} :gray[Zuletzt bearbeitet:] {st.session_state.session.edited.strftime("%d.%m.%Y %H:%M:%S")}') reserved_columns = ['Nachname', 'Vorname', 'Punkte', 'Note'] exercise_columns = [col for col in st.session_state.session.points_df.columns if col not in reserved_columns+list(st.session_state.session.students_df.columns)] if len(exercise_columns) == 0: exercise_columns = '' exercise_columns = st.text_input('Aufgaben', value=','.join(exercise_columns), placeholder='1,2,3,4,...') exercise_columns = [col.strip() for col in exercise_columns.split(',') if col.strip()] def update_columns(new_columns, table): if len(table) > 0: # Drop table columns that are not in the list of columns for column in table.columns: if column not in new_columns and column not in reserved_columns+list(st.session_state.session.students_df.columns): table.drop(column, axis=1, inplace=True) # Add columns that are in the list of columns but not in the table for column in new_columns: if column not in table.columns: table[column] = pd.Series(np.nan, index=table.index, dtype='float64') # Reorder columns before_reorder = table.columns original_columns = [col for col in before_reorder if col not in new_columns] table = table[original_columns + new_columns] return table st.session_state.session.points_df = update_columns(exercise_columns, st.session_state.session.points_df) # Grade calculation with st.expander('Notenberechnung'): grades = ['1,0', '1,3', '1,7', '2,0', '2,3', '2,7', '3,0', '3,3', '3,7', '4,0', '5,0'] left, right = st.columns(2, gap='large') with left: grade_4_0, grade_1_0 = st.slider('Notenbereich (4,0-Grenze / 1,0-Grenze)', min_value=0., max_value=1., key='grade_range') max_points = st.slider('Maximalpunktzahl', min_value=1, max_value=100, key='max_points') if st.session_state.session.grade_4_0 != grade_4_0 or st.session_state.session.grade_1_0 != grade_1_0 or st.session_state.session.max_points != max_points: st.session_state.session.grade_4_0 = grade_4_0 st.session_state.session.grade_1_0 = grade_1_0 st.session_state.session.max_points = max_points def calculate_grades_table(grade_1_0, grade_4_0): grade_interval = (grade_1_0 - grade_4_0) / 9 grade_intervals = [grade_1_0 - i * grade_interval for i in range(10)] grade_intervals.append(0) lower = np.array([i * st.session_state.session.max_points for i in grade_intervals]) lower = np.floor(lower * 2) / 2 upper = np.roll(lower, 1)-0.5 upper[0] = st.session_state.session.max_points return pd.DataFrame({'Note': grades, 'Prozent': grade_intervals, 'Untergrenze': lower, 'Obergrenze': upper}) with right: grades_table = calculate_grades_table(st.session_state.session.grade_1_0, st.session_state.session.grade_4_0) st.dataframe(grades_table, hide_index=True, column_config={ 'Note': st.column_config.TextColumn(), 'Prozent': st.column_config.NumberColumn(format='%.2f'), 'Untergrenze': st.column_config.NumberColumn(format='%.1f'), 'Obergrenze': st.column_config.NumberColumn(format='%.1f')}) def calculate_points(session: Session): points = session.points_df[exercise_columns].sum(axis=1, skipna=False, min_count=1) session.points_df['Punkte'] = points session.points_df['Note'] = session.points_df['Punkte'].apply(lambda x: np.nan if np.isnan(x) else grades_table.iloc[np.digitize(x, grades_table['Untergrenze'])]['Note']) calculate_points(st.session_state.session) # Editable points table def color_try_column(col): return ['color: red' if val > 2 else 'color: orange' if val > 1 else '' for val in col] def color_grade_column(col): return ['color: red' if val in ['5,0'] else '' for val in col] attemptCellStyle = JsCode( r""" function(params) { if (params.value == 2) return {'color': 'gold'}; else if (params.value > 2) return {'color': 'red'}; return {}; } """) gradeCellStyle = JsCode( r""" function(params) { if (params.value === '5,0') return {'color': 'red'}; return {}; } """) def points_table(session: Session, exercise_columns: list[str]): filterText = st.text_input('Filter', placeholder='Filter...', label_visibility='hidden') grid_options = { "columnDefs":[ {"headerName":"Matrikelnummer","field":"Matrikelnummer","checkboxSelection": True, "cellDataType": "Number"}, {"headerName":"Nachname","field":"Nachname", "cellDataType": "String"}, {"headerName":"Vorname","field":"Vorname", "cellDataType": "String"}, {"headerName":"Versuch","field":"Versuch", "cellDataType": "Number", "cellStyle": attemptCellStyle}, {"headerName":"Punkte","field":"Punkte", "cellDataType": "Number"}, {"headerName":"Note","field":"Note", "cellDataType": "Number", "cellStyle": gradeCellStyle}, *[{ "headerName": col, "field": col, "cellDataType": "Number", "width": 40} for col in exercise_columns] ], "autoSizeStrategy": {"type":"fitCellContents"}, "rowSelection":"single", "rowMultiSelectWithClick": True, "suppressRowDeselection": True, "suppressRowClickSelection": True, "quickFilterText": filterText } event = AgGrid(data=session.points_df.reset_index(), gridOptions=grid_options, key='grid1', update_mode=GridUpdateMode.SELECTION_CHANGED, allow_unsafe_jscode=True) return event def points_editor(row_df: pd.DataFrame, exercise_columns: list[str]): grid_options = { "columnDefs":[ {"headerName":"Matrikelnummer","field":"Matrikelnummer", "cellDataType": "Number"}, {"headerName":"Nachname","field":"Nachname","cellDataType": "String"}, {"headerName":"Vorname","field":"Vorname", "cellDataType": "String"}, *[{ "headerName": col, "field": col, "cellDataType": "Number", "width": 40, "editable": True} for col in exercise_columns] ], "autoSizeStrategy": {"type":"fitCellContents"}, } editor_event = AgGrid(data=row_df, gridOptions=grid_options, height=100, key='grid2'+str(row_df['Matrikelnummer']), update_mode=GridUpdateMode.VALUE_CHANGED) return editor_event with st.expander('Punkteeingabe', expanded=True): event = points_table(st.session_state.session, exercise_columns) if event.selected_rows is not None: selected_row = event.selected_rows.astype(st.session_state.session.points_df.dtypes) selected_row_idx = int(event.selected_rows_id[0]) with st.form('edit_points'): editor_event = points_editor(selected_row, exercise_columns) submit = st.form_submit_button('Submit') if submit: # Replace all commas with decimal points in editor_event.data data = editor_event.data data[exercise_columns] = data[exercise_columns].replace('\\,', '.', regex=True) # Submit updated row updated_row = data.astype(st.session_state.session.points_df.dtypes).set_index('Matrikelnummer') st.session_state.session.points_df.update(updated_row) st.session_state.request_rerun = True # Statistics stats_ui(st.session_state.session, grades_table, grades) # Download button st.download_button('Export nach :blue[HISinOne]', data=export(st.session_state.session.points_df, st.session_state.session.his_header, exercise_columns,grades_table), file_name=f'{st.session_state.session.table_name}_Noten.xlsx', mime='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') # Store session calculate_points(st.session_state.session) store_session(st.session_state.session) # Rerun if requested if 'request_rerun' in st.session_state: del st.session_state['request_rerun'] st.rerun()