From 3687de6069e515117f9f7036879bf0d54974235e Mon Sep 17 00:00:00 2001 From: Niklas Elsbrock Date: Wed, 1 Sep 2021 19:01:04 +0200 Subject: [PATCH] CSV-Importer implementiert --- nateman/blueprints/fileio.py | 11 ++- nateman/importer.py | 111 +++++++++++++++++++++++- nateman/templates/fileio/import.html.j2 | 4 +- 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/nateman/blueprints/fileio.py b/nateman/blueprints/fileio.py index 07de6a7..8deba39 100644 --- a/nateman/blueprints/fileio.py +++ b/nateman/blueprints/fileio.py @@ -28,7 +28,7 @@ from openpyxl.utils.exceptions import InvalidFileException from .auth import beratungslehrer_required, admin_required from .. import exporter, util -from ..importer import KlausurplanImportError, import_plan, excelimport, KoopSchuelerImportError +from ..importer import KlausurplanImportError, import_kurs42, import_excel_koop, KoopSchuelerImportError, import_csv from ..models import db, Stufe, Klausur, Schueler bp = Blueprint("fileio", __name__) @@ -98,8 +98,13 @@ def import_(): _del_plan(stufe) + if plan.filename.endswith(".xml"): + import_func = import_kurs42 + else: + import_func = import_csv + try: - import_plan(plan, stufe, new_lehrer_password) + import_func(plan, stufe, new_lehrer_password) except (KeyError, ValueError, ExpatError, KlausurplanImportError) as exc: db.session.rollback() @@ -125,7 +130,7 @@ def import_(): ks_file.save(fp.name) try: - ks_import_failures = excelimport(fp.name) + ks_import_failures = import_excel_koop(fp.name) except (InvalidFileException, KoopSchuelerImportError) as exc: db.session.rollback() diff --git a/nateman/importer.py b/nateman/importer.py index b5ce06d..d6861cc 100644 --- a/nateman/importer.py +++ b/nateman/importer.py @@ -18,7 +18,8 @@ """ Enthält Funktionen zum Import von Daten. """ - +import io +import re from datetime import datetime from typing import TextIO, SupportsInt, Union @@ -50,7 +51,111 @@ def _nbit_int(x: Union[str, bytes, SupportsInt], base=10, maxbits=32): return result -def import_plan(xml_file: TextIO, stufe: Stufe, new_lehrer_pwd: str) -> None: +def import_csv(file: TextIO, stufe: Stufe, new_lehrer_pwd: str) -> None: + """ + Importiert eine Klausurliste aus der gegebenen CSV-Klausurplandatei für die angegebene Stufe. + Registriert noch nicht registrierte Lehrer mit dem angegebenen Passwort. + von Niklas Elsbrock + + :param file: zu importierende XML-Datei + :param stufe: Stufe, für die der Plan importiert werden soll + :param new_lehrer_pwd: Passwort für neu registrierte Lehrerkonten + """ + col_kursname = 0 + col_lehrer_kuerzel = 1 + col_date = 2 + col_startperiod = 3 + col_endperiod = 4 + col_teilnehmer = 5 + + row_len = 6 + + delim_regex = r"[,;]" + + assert util.validate_bcrypt_password(new_lehrer_pwd) + + # Importdatum setzen + stufe.import_date = datetime.now().date() + + new_lehrer_pwd_hash = bcrypt.hashpw(new_lehrer_pwd.encode("utf-8"), bcrypt.gensalt()) + + rows = [tuple(re.split(delim_regex, row)) for row in io.TextIOWrapper(file).read().splitlines()] + + for rownum, row in enumerate(rows[1:], start=2): + if len(row) != row_len: + raise KlausurplanImportError(f"Zu importierender Plan für die Stufe {stufe.name} enthält in Zeile {rownum} " + f"nur {len(row)} statt der erwarteten {row_len} Werte.") + + kursname = row[col_kursname] + lehrer_kuerzel = row[col_lehrer_kuerzel] + date = datetime.strptime(row[col_date], "%Y%m%d").date() + startperiod = _nbit_int(row[col_startperiod]) + endperiod = _nbit_int(row[col_endperiod]) + + if startperiod < 0 or startperiod > endperiod: + raise KlausurplanImportError(f"Zu importierender Plan für die Stufe {stufe.name} enthält ungültigen " + f"Zeitraum (Zeile: {rownum}; VonStd: {startperiod} BisStd: {endperiod})") + + # eventuell bereits registrierten Lehrer bekommen + kurs_lehrer_entity = Lehrer.query.filter_by(kuerzel=lehrer_kuerzel).first() + + # Falls der Lehrer noch nicht registriert ist, registrieren + if kurs_lehrer_entity is None: + kurs_lehrer_entity = Lehrer(kuerzel=lehrer_kuerzel, is_confirmed=True, pwd_hash=new_lehrer_pwd_hash) + # Falls ein E-Mailadressenformat konfiguriert ist, dieses nutzen. + email_address_format = config.get("email-address-format", None) + if email_address_format is not None: + kurs_lehrer_entity.set_default_email_address(email_address_format) + + db.session.add(kurs_lehrer_entity) + + klausur_entity = Klausur(kursname=kursname, date=date, startperiod=startperiod, endperiod=endperiod, + stufe=stufe, lehrer=kurs_lehrer_entity) + db.session.add(klausur_entity) + + for schueler_entry in row[col_teilnehmer].split('~'): + schueler_data = schueler_entry.rsplit('_', maxsplit=2) + + if len(schueler_data) != 3: + schueler_entity = Schueler(id=get_next_new_schueler_id(), nachname=schueler_entry, vorname="???", + stufe=stufe, koop=True) + else: + (nachname, vorname, birthdate) = schueler_data + birthdate = int(birthdate) + for rownum in range(100): + test_id = birthdate * 100 + rownum + test_schueler = Schueler.query.filter_by(id=test_id).first() + if test_schueler is None: + schueler_entity = Schueler(id=test_id, nachname=nachname, vorname=vorname, stufe=stufe) + break + elif test_schueler.nachname == nachname and test_schueler.vorname == vorname: + # Ist dieser Schüler bereits für eine andere Stufe eingetragen? -> Fehler + if test_schueler.stufe != stufe: + raise KlausurplanImportError(f"Zu importierender Plan für die {stufe.name} enthält Schüler " + f"({vorname} {nachname}, {birthdate}), der bereits für die " + f"{test_schueler.stufe.name} eingetragen ist " + f"(Zeile: {rownum}).") + schueler_entity = test_schueler + break + else: + raise KlausurplanImportError(f"Zu importierender Plan für Stufe {stufe.name} enthält Schüler " + f"({vorname} {nachname}, {birthdate}; Zeile: {rownum}), dem keine ID " + f"gegeben werden konnte.") + + # Ist derselbe Schüler bereits in dieser Klausur? -> Fehler + if db.session.query(exists().where(and_(Klausurteilnahme.schueler == schueler_entity, + Klausurteilnahme.klausur == klausur_entity))).scalar(): + raise KlausurplanImportError(f"Zu importierender Plan für die {stufe.name} enthält selben " + f"Schüler ({schueler_entity.vorname} {schueler_entity.nachname}, " + f"{schueler_entity.birthdate}, Zeile: {rownum}) mehrfach in derselben " + f"Klausur ({kursname}).") + + # Schüler zur Klausur hinzufügen + klausurteilnahme = Klausurteilnahme(klausur=klausur_entity, schueler=schueler_entity) + db.session.add(klausurteilnahme) + + +def import_kurs42(xml_file: TextIO, stufe: Stufe, new_lehrer_pwd: str) -> None: """ Importiert eine Klausurliste aus dem gegebenen XML-Klausurexport für die angegebene Stufe. Registriert noch nicht registrierte Lehrer mit dem angegebenen Passwort. @@ -146,7 +251,7 @@ def import_plan(xml_file: TextIO, stufe: Stufe, new_lehrer_pwd: str) -> None: # Von Johannes -def excelimport(filepath: str): +def import_excel_koop(filepath: str): # Initialisiert Startdaten if Stufe.import_date is not None: workbook = load_workbook(filepath) diff --git a/nateman/templates/fileio/import.html.j2 b/nateman/templates/fileio/import.html.j2 index 9c77fad..c096a9b 100644 --- a/nateman/templates/fileio/import.html.j2 +++ b/nateman/templates/fileio/import.html.j2 @@ -54,7 +54,9 @@ {% for stufe in stufen_list %}
-
+
+ +