noteneingabetool/streamlit_app.py
2025-07-25 18:54:33 +02:00

232 lines
9.7 KiB
Python

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()