Initial commit.

This commit is contained in:
enricoschroeder
2025-07-25 18:54:33 +02:00
parent 0d38b5d5ca
commit 8f66e48cea
16 changed files with 782 additions and 2 deletions
View File
+93
View File
@@ -0,0 +1,93 @@
from datetime import datetime
from io import StringIO
from pathlib import Path
from tinydb import TinyDB, Query
import secrets
import pandas as pd
from pydantic import BaseModel, field_serializer
from .his_io import dtypes
pandas_json_format = 'split'
class Session(BaseModel):
token: str
table_name: str = ''
students_df: pd.DataFrame
points_df: pd.DataFrame
his_header: pd.DataFrame
grade_1_0: float = 0.92
grade_4_0: float = 0.4
max_points: int = 100
created: datetime = datetime.now()
edited: datetime = datetime.now()
def __eq__(self, other: object) -> bool:
return isinstance(other, Session) and \
self.token == other.token and \
self.table_name == other.table_name and \
self.students_df.equals(other.students_df) and \
self.points_df.equals(other.points_df) and \
self.his_header.equals(other.his_header) and \
self.grade_1_0 == other.grade_1_0 and \
self.grade_4_0 == other.grade_4_0 and \
self.max_points == other.max_points and \
self.created == other.created and \
self.edited == other.edited
@field_serializer('students_df', 'points_df')
def serialize_df(self, df: pd.DataFrame):
return df.reset_index().to_json(orient=pandas_json_format)
@field_serializer('his_header')
def serialize_his_header(self, df):
return df.to_json(orient=pandas_json_format)
@field_serializer('created', 'edited')
def serialize_datetime(self, dt):
return dt.isoformat()
class Config:
arbitrary_types_allowed = True
class DB:
def __init__(self, db_path='db.json'):
self.db_path = Path(db_path)
self.db = TinyDB(self.db_path, access_mode='r+')
def create_session(self):
token = secrets.token_urlsafe(16)
self.db.insert({'token': token})
return Session(**{
'token': token,
'students_df': pd.DataFrame(),
'points_df': pd.DataFrame(),
'his_header': pd.DataFrame(),
})
def get_session(self, token: str) -> Session:
results = self.db.search(Query().token == token)
if len(results) == 0:
raise ValueError(f'Session with token {token} not found')
session = results[0].copy()
session['points_df'] = pd.read_json(StringIO(session['points_df']), dtype=dtypes.students_table, orient=pandas_json_format).set_index('Matrikelnummer')
session['students_df'] = pd.read_json(StringIO(session['students_df']), dtype=dtypes.students_table, orient=pandas_json_format).set_index('Matrikelnummer')
session['his_header'] = pd.read_json(StringIO(session['his_header']), dtype=dtypes.his_header, orient=pandas_json_format)
return Session(**session)
def save_session(self, session: Session):
write_session = session.model_dump()
self.db.upsert(write_session, Query().token == write_session['token'])
def delete_session(self, session: Session):
self.db.remove(Query().token == session.token)
def list_tokens(self):
return [session['token'] for session in self.db.all()]
+81
View File
@@ -0,0 +1,81 @@
import pandas as pd
import numpy as np
from io import BytesIO
pandas_json_format = 'table'
class dtypes:
students_table = {'Examplan.id': 'int64', 'PrüfungsNr.': 'int64', 'Titel': 'O', 'Nachname': 'O', 'Vorname': 'O', 'Leistung': 'O', 'Versuch': 'int64', 'Status': 'O', 'ECTS': 'O', 'Semester': 'O', 'Jahr': 'int64', 'Prüfungsperiode': 'int64', 'Vermerk': 'O', 'Thema': 'O', 'Beginn': 'O', 'Gepl. Ende': 'O', 'Tatsächl. Ende': 'O', 'Prüfungsart': 'O', 'Prüfungsform': 'O', 'LockVersion': 'O'}
his_header = 'O'
def read_excel(bytes: BytesIO) -> tuple[pd.DataFrame, pd.DataFrame]:
try:
excelfile = pd.ExcelFile(bytes, engine='openpyxl')
except:
return pd.DataFrame()
full_table = pd.read_excel(excelfile, header=None)
# Finde Indizes der Zeilen mit den Werten "startHISsheet" und "endHISsheet"
def find_row_with_value(df, value):
mask = full_table.iloc[:, 0] == value
if not mask.any():
raise ValueError(f'{value} nicht gefunden')
return mask.idxmax()
start_idx = find_row_with_value(full_table, 'startHISsheet')
end_idx = find_row_with_value(full_table, 'endHISsheet')
# Lade den HISinOne Header (die ersten start_idx+1 Zeilen)
his_header = pd.read_excel(excelfile, nrows=start_idx+1, header=None)
# Lade Studentendaten (zwischen start_idx+1 und end_idx)
students_table = pd.read_excel(excelfile,
header=start_idx + 1,
nrows=end_idx - start_idx - 2,
index_col='Matrikelnummer',
dtype=dtypes.students_table)
students_table = students_table.replace({np.nan: None})
his_header = his_header.replace({np.nan: None})
return students_table, his_header
def prepare_table(students_table: pd.DataFrame) -> pd.DataFrame:
# Erzeuge Tabelle zur Punkteeingabe
points_table = students_table
new_columns = {'Punkte': np.nan, 'Note': np.nan}
points_table = points_table.assign(**new_columns)
return points_table
def export(edited_df: pd.DataFrame, his_header: pd.DataFrame, exercise_columns: list[str], grades_table: pd.DataFrame) -> BytesIO:
edited_df['Leistung'] = edited_df['Note'].replace({np.nan: 'NT'})
export_students_table = edited_df.drop(columns=['Punkte', 'Note']+exercise_columns).reset_index()
# Examplan.id muss erste Spalte sein (HISinOne prüft Spaltennamen nicht)
export_students_table = export_students_table.reindex(columns=['Examplan.id'] + [col for col in export_students_table.columns if col != 'Examplan.id'])
export_full_table = pd.DataFrame(
np.vstack([
his_header.values, # HISinOne Header
np.array(list(export_students_table)), # Tabellenheader (Matrikelnummer, Nachname, Vorname, ...)
export_students_table.values, # Daten
np.array(['endHISsheet'] + [''] * (len(list(export_students_table))-1)) # HISinOne Footer
]),
columns=export_students_table.columns)
# Exportiere Tabelle
bytesio = BytesIO()
with pd.ExcelWriter(bytesio, engine='openpyxl') as writer:
export_full_table.to_excel(writer, sheet_name='First Sheet', header=False, index=False) # HISinOne Tabelle
edited_df[['Nachname', 'Vorname', 'Punkte', 'Note']+exercise_columns].to_excel(writer, sheet_name='Aufgaben') # Punktergebnisse als extra Sheet
grades_table.to_excel(writer, sheet_name='Notenschlüssel', header=False, index=False) # Notenberechnung als extra Sheet
return bytesio