Add support for importing moodle result csv files.
This commit is contained in:
parent
569b52159f
commit
c4f3b64138
9
RocketScience-Bewertungen.csv
Normal file
9
RocketScience-Bewertungen.csv
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
Nachname,Vorname,E-Mail-Adresse,Status,Begonnen,Beendet,Dauer,"Bewertung/73,0","F 1 /4,0","F 2 /6,0","F 3 /4,0","F 4 /4,0","F 5 /4,0","F 6 /7,0","F 7 /10,0","F 8 /5,0","F 9 /4,0","F 10 /9,0","F 11 /8,0","F 12 /8,0"
|
||||||
|
Hendrix,Jimi,bla@th-nuernberg.de,Beendet,"5. Februar 2026 16:30","5. Februar 2026 18:00","1 Stunde 30 Minuten","39,5","3,0","6,0","2,0","4,0","2,5","4,0","1,0","4,0","0,0","4,0","7,0","2,0"
|
||||||
|
Lukather,Steve,blub@th-nuernberg.de,Beendet,"5. Februar 2026 16:30","5. Februar 2026 18:00","1 Stunde 30 Minuten","26,5","3,0","6,0","2,0","2,0","1,5","2,0","3,0","1,0",-,"2,0","2,0","2,0"
|
||||||
|
Mayer,John,abc@th-nuernberg.de,Beendet,"5. Februar 2026 16:30","5. Februar 2026 18:11","1 Stunde 40 Minuten","62,0","3,0","6,0","3,0","4,0","3,0","5,0","6,0","5,0","4,0","7,0","8,0","8,0"
|
||||||
|
Page,Jimmy,def@th-nuernberg.de,Beendet,"5. Februar 2026 16:30","5. Februar 2026 18:00","1 Stunde 30 Minuten","30,5","3,0","4,0","1,0","1,0","2,0","0,5","0,0","5,0","0,0","8,0","4,0","2,0"
|
||||||
|
Townshend,Pete,hij@th-nuernberg.de,Beendet,"5. Februar 2026 16:30","5. Februar 2026 18:00","1 Stunde 30 Minuten","24,0","2,0","5,0","4,0","2,5","2,0","1,5","1,0","0,5","0,0","2,5","2,0","1,0"
|
||||||
|
Van Halen,Eddie,xyz@th-nuernberg.de,Beendet,"5. Februar 2026 16:30","5. Februar 2026 18:00","1 Stunde 30 Minuten","24,0","2,0","5,0","4,0","2,5","2,0","1,5","1,0","0,5","0,0","2,5","2,0","1,0"
|
||||||
|
Gruppendurchschnitt,,,,,,,"31,91","3,65","5,88","3,09","3,35","1,82","0,47","1,18","1,35","4,59","2,91","2,41","1,21"
|
||||||
|
Gesamtdurchschnitt,,,,,,,"31,32","3,07","5,59","3,04","3,02","1,68","0,51","1,15","1,17","4,55","3,30","2,83","1,41"
|
||||||
|
21
backend/helpers.py
Normal file
21
backend/helpers.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
reserved_columns = ['Nachname', 'Vorname', 'Punkte', 'Note']
|
||||||
|
|
||||||
|
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
|
||||||
47
backend/moodle_io.py
Normal file
47
backend/moodle_io.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
pandas_json_format = 'table'
|
||||||
|
|
||||||
|
def read_csv(table_str: StringIO) -> pd.DataFrame:
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(table_str, engine='python', skipfooter=2)
|
||||||
|
except:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
df = df.set_index(['Nachname', 'Vorname'])
|
||||||
|
|
||||||
|
|
||||||
|
# Rename columns to replace commas.
|
||||||
|
# Replace with _ as AggGrid does not seem to support columns with decimals in name
|
||||||
|
df = df.rename(columns=lambda x: x.replace(',','_'))
|
||||||
|
exercises = [col for col in list(df.columns) if 'F' in col]
|
||||||
|
df.update(df[exercises].map(lambda v: v.replace(',','.'))) # German floats -> international floats
|
||||||
|
df.update(df[exercises].map(lambda v: v.replace('-','0.0'))) # '-' -> 0
|
||||||
|
df = df.astype({k: 'float64' for k in exercises})
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def parse_header(df: pd.DataFrame) -> tuple[int, list[str]]:
|
||||||
|
max_points = [col for col in list(df.columns) if 'Bewertung' in col]
|
||||||
|
if len(max_points) != 1:
|
||||||
|
raise ValueError("Could not find column 'Bewertung' in table!")
|
||||||
|
max_points = float(max_points[0].split('/', 1)[1].replace('_','.'))
|
||||||
|
|
||||||
|
exercises = [col for col in list(df.columns) if 'F' in col]
|
||||||
|
if len(exercises) < 1:
|
||||||
|
raise ValueError("Table does not seem to contain exercise points!")
|
||||||
|
return max_points, exercises
|
||||||
|
|
||||||
|
|
||||||
|
def merge_points(points_df_hisio: pd.DataFrame, points_df_moodle: pd.DataFrame) -> tuple[pd.DataFrame, int]:
|
||||||
|
points_df_hisio = points_df_hisio.reset_index().set_index(['Nachname', 'Vorname'])
|
||||||
|
common_keys = points_df_hisio.index.intersection(points_df_moodle.index)
|
||||||
|
update_count = len(common_keys)
|
||||||
|
points_df_hisio.update(points_df_moodle)
|
||||||
|
points_df_hisio = points_df_hisio.reset_index().set_index('Matrikelnummer')
|
||||||
|
return points_df_hisio, update_count
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
import time
|
import time
|
||||||
|
from backend import moodle_io
|
||||||
from backend.db import DB, Session
|
from backend.db import DB, Session
|
||||||
|
from backend.helpers import update_columns
|
||||||
from backend.his_io import prepare_table, read_excel
|
from backend.his_io import prepare_table, read_excel
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +39,8 @@ def session_ui(db: DB, session: Session | None) -> Session | None:
|
|||||||
new_session_dialog(db)
|
new_session_dialog(db)
|
||||||
if st.button("Lösche Session", disabled=session is None):
|
if st.button("Lösche Session", disabled=session is None):
|
||||||
delete_session_dialog(db, session)
|
delete_session_dialog(db, session)
|
||||||
|
if st.button("Moodle Ergebnisse importieren", disabled=session is None):
|
||||||
|
upload_moodle_results(db, session)
|
||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
||||||
@ -62,6 +66,26 @@ def delete_session_dialog(db: DB, session: Session):
|
|||||||
st.success("Session gelöscht")
|
st.success("Session gelöscht")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
@st.dialog("Moodle Ergebnisse hochladen")
|
||||||
|
def upload_moodle_results(db: DB, session: Session):
|
||||||
|
moodle_file = st.file_uploader("Moodle Ergebnis CSV hochladen", type=['csv'], key='moodle_file_uploader')
|
||||||
|
|
||||||
|
if moodle_file:
|
||||||
|
moodle_points_df = moodle_io.read_csv(moodle_file)
|
||||||
|
max_points, exercise_columns = moodle_io.parse_header(moodle_points_df)
|
||||||
|
st.write(f'Moodle CSV enhält :blue[{len(moodle_points_df.index)}] Einträge')
|
||||||
|
st.write(f'Aufgaben: ```{", ".join(exercise_columns)}```')
|
||||||
|
st.write(f'Punktzahl: :blue[{max_points}]')
|
||||||
|
st.warning("Sind Sie sicher, dass Sie die Ergebnisse importieren möchten? Aktuelle Ergebnisse werden gelöscht!")
|
||||||
|
if st.button("Importieren :question:"):
|
||||||
|
session.points_df = update_columns(exercise_columns, session.points_df)
|
||||||
|
session.points_df, update_count = moodle_io.merge_points(session.points_df, moodle_points_df)
|
||||||
|
st.session_state.max_points = max_points
|
||||||
|
st.success(f"{update_count} Ergebnisse importiert")
|
||||||
|
db.save_session(session)
|
||||||
|
time.sleep(1)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
def create_new_session(db, upload_file) -> Session:
|
def create_new_session(db, upload_file) -> Session:
|
||||||
new_session = db.create_session()
|
new_session = db.create_session()
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from st_aggrid import AgGrid, GridUpdateMode, JsCode
|
|||||||
|
|
||||||
from backend.db import DB, Session
|
from backend.db import DB, Session
|
||||||
import backend.his_io as his_io
|
import backend.his_io as his_io
|
||||||
|
from backend.helpers import reserved_columns, update_columns
|
||||||
from session_ui import session_ui
|
from session_ui import session_ui
|
||||||
from stats_ui import stats_ui
|
from stats_ui import stats_ui
|
||||||
|
|
||||||
@ -59,29 +60,12 @@ if st.session_state.session.token != st.session_state.last_session_token:
|
|||||||
st.write(f':blue[{st.session_state.session.table_name}]')
|
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")}')
|
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)]
|
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:
|
if len(exercise_columns) == 0:
|
||||||
exercise_columns = ''
|
exercise_columns = ''
|
||||||
exercise_columns = st.text_input('Aufgaben', value=','.join(exercise_columns), placeholder='1,2,3,4,...')
|
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()]
|
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)
|
st.session_state.session.points_df = update_columns(exercise_columns, st.session_state.session.points_df)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
66
tests/test_moodleio.py
Normal file
66
tests/test_moodleio.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from io import StringIO, BytesIO
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from backend import his_io, moodle_io
|
||||||
|
|
||||||
|
|
||||||
|
class TestMoodleio(unittest.TestCase):
|
||||||
|
|
||||||
|
def check_csv_table(self, table):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
exercise_columns = ['F 1 /4_0', 'F 2 /6_0', 'F 3 /4_0', 'F 4 /4_0',
|
||||||
|
'F 5 /4_0', 'F 6 /7_0', 'F 7 /10_0', 'F 8 /5_0', 'F 9 /4_0',
|
||||||
|
'F 10 /9_0', 'F 11 /8_0', 'F 12 /8_0']
|
||||||
|
|
||||||
|
with open('RocketScience-Bewertungen.csv', 'r') as f:
|
||||||
|
self.table_str = StringIO(f.read())
|
||||||
|
|
||||||
|
with self.subTest('read'):
|
||||||
|
points_df_moodle = moodle_io.read_csv(self.table_str)
|
||||||
|
self.assertIsNotNone(points_df_moodle)
|
||||||
|
|
||||||
|
|
||||||
|
with self.subTest('check_table'):
|
||||||
|
self.assertEqual(list(points_df_moodle.columns),
|
||||||
|
['E-Mail-Adresse', 'Status', 'Begonnen', 'Beendet', 'Dauer', 'Bewertung/73_0'] + exercise_columns)
|
||||||
|
self.assertEqual(points_df_moodle.shape[0], 6)
|
||||||
|
self.assertEqual(points_df_moodle.index.names, ['Nachname', 'Vorname'])
|
||||||
|
self.assertEqual(list(points_df_moodle.index), [('Hendrix','Jimi'), ('Lukather','Steve') , ('Mayer','John'), ('Page','Jimmy'), ('Townshend','Pete'), ('Van Halen','Eddie')])
|
||||||
|
self.assertEqual(list(points_df_moodle[exercise_columns].dtypes), ['float64'] * len(list(points_df_moodle[exercise_columns].dtypes)))
|
||||||
|
|
||||||
|
with self.subTest('parse_header'):
|
||||||
|
max_points, exercises = moodle_io.parse_header(points_df_moodle)
|
||||||
|
self.assertEqual(max_points, 73)
|
||||||
|
self.assertEqual(exercises, exercise_columns)
|
||||||
|
|
||||||
|
with self.subTest('merge_tables'):
|
||||||
|
table_bytes = None
|
||||||
|
with open('5160-RocketScience-WiSe_2023.xlsx', 'rb') as f:
|
||||||
|
table_bytes = BytesIO(f.read())
|
||||||
|
|
||||||
|
students_table = None
|
||||||
|
students_table, _ = his_io.read_excel(table_bytes)
|
||||||
|
self.assertIsNotNone(students_table)
|
||||||
|
|
||||||
|
for col in exercise_columns:
|
||||||
|
students_table[col] = pd.Series(np.nan, index=students_table.index, dtype='float64')
|
||||||
|
|
||||||
|
students_table, merged_count = moodle_io.merge_points(students_table, points_df_moodle)
|
||||||
|
self.assertEqual(merged_count, 6)
|
||||||
|
self.assertEqual(students_table.index.name, 'Matrikelnummer')
|
||||||
|
|
||||||
|
self.assertEqual(list(students_table.loc[3434343][exercise_columns]), [3.0, 6.0, 2.0, 4.0, 2.5, 4.0, 1.0, 4.0, 0.0, 4.0, 7.0, 2.0])
|
||||||
|
self.assertEqual(list(students_table.loc[3535353][exercise_columns]), [3.0, 6.0, 2.0, 2.0, 1.5, 2.0, 3.0, 1.0, 0.0, 2.0, 2.0, 2.0])
|
||||||
|
self.assertEqual(list(students_table.loc[3131313][exercise_columns]), [3.0, 6.0, 3.0, 4.0, 3.0, 5.0, 6.0, 5.0, 4.0, 7.0, 8.0, 8.0])
|
||||||
|
self.assertEqual(list(students_table.loc[2323232][exercise_columns]), [3.0, 4.0, 1.0, 1.0, 2.0, 0.5, 0.0, 5.0, 0.0, 8.0, 4.0, 2.0])
|
||||||
|
self.assertEqual(list(students_table.loc[3737373][exercise_columns]), [2.0, 5.0, 4.0, 2.5, 2.0, 1.5, 1.0, 0.5, 0.0, 2.5, 2.0, 1.0])
|
||||||
|
self.assertEqual(list(students_table.loc[3939393][exercise_columns]), [2.0, 5.0, 4.0, 2.5, 2.0, 1.5, 1.0, 0.5, 0.0, 2.5, 2.0, 1.0])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Loading…
x
Reference in New Issue
Block a user