CSV-Importer implementiert

This commit is contained in:
2021-09-01 19:01:04 +02:00
parent a14694aa13
commit 3687de6069
3 changed files with 119 additions and 7 deletions
+8 -3
View File
@@ -28,7 +28,7 @@ from openpyxl.utils.exceptions import InvalidFileException
from .auth import beratungslehrer_required, admin_required from .auth import beratungslehrer_required, admin_required
from .. import exporter, util 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 from ..models import db, Stufe, Klausur, Schueler
bp = Blueprint("fileio", __name__) bp = Blueprint("fileio", __name__)
@@ -98,8 +98,13 @@ def import_():
_del_plan(stufe) _del_plan(stufe)
if plan.filename.endswith(".xml"):
import_func = import_kurs42
else:
import_func = import_csv
try: try:
import_plan(plan, stufe, new_lehrer_password) import_func(plan, stufe, new_lehrer_password)
except (KeyError, ValueError, ExpatError, KlausurplanImportError) as exc: except (KeyError, ValueError, ExpatError, KlausurplanImportError) as exc:
db.session.rollback() db.session.rollback()
@@ -125,7 +130,7 @@ def import_():
ks_file.save(fp.name) ks_file.save(fp.name)
try: try:
ks_import_failures = excelimport(fp.name) ks_import_failures = import_excel_koop(fp.name)
except (InvalidFileException, KoopSchuelerImportError) as exc: except (InvalidFileException, KoopSchuelerImportError) as exc:
db.session.rollback() db.session.rollback()
+108 -3
View File
@@ -18,7 +18,8 @@
""" """
Enthält Funktionen zum Import von Daten. Enthält Funktionen zum Import von Daten.
""" """
import io
import re
from datetime import datetime from datetime import datetime
from typing import TextIO, SupportsInt, Union from typing import TextIO, SupportsInt, Union
@@ -50,7 +51,111 @@ def _nbit_int(x: Union[str, bytes, SupportsInt], base=10, maxbits=32):
return result 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. Importiert eine Klausurliste aus dem gegebenen XML-Klausurexport für die angegebene Stufe.
Registriert noch nicht registrierte Lehrer mit dem angegebenen Passwort. 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 # Von Johannes
def excelimport(filepath: str): def import_excel_koop(filepath: str):
# Initialisiert Startdaten # Initialisiert Startdaten
if Stufe.import_date is not None: if Stufe.import_date is not None:
workbook = load_workbook(filepath) workbook = load_workbook(filepath)
+3 -1
View File
@@ -54,7 +54,9 @@
{% for stufe in stufen_list %} {% for stufe in stufen_list %}
<div> <div>
<div><label for="plan_{{ stufe.name }}-input">Plan für {{ stufe.name }}:</label></div> <div><label for="plan_{{ stufe.name }}-input">Plan für {{ stufe.name }}:</label></div>
<div><input type="file" name="plan_{{ stufe.name }}" id="plan_{{ stufe.name }}-input" accept="text/xml"></div> <div>
<input type="file" name="plan_{{ stufe.name }}" id="plan_{{ stufe.name }}-input" accept="text/csv,text/xml">
</div>
<div> <div>
<input type="submit" name="del_{{ stufe.name }}" value="Plan löschen" <input type="submit" name="del_{{ stufe.name }}" value="Plan löschen"
onclick="return confirm(plan_delete_warn)" {% if stufe.import_date is none %}disabled{% endif %}> onclick="return confirm(plan_delete_warn)" {% if stufe.import_date is none %}disabled{% endif %}>