232 lines
9.7 KiB
Python
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() |