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 .. 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()
+108 -3
View File
@@ -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)
+3 -1
View File
@@ -54,7 +54,9 @@
{% for stufe in stufen_list %}
<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>
<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 %}>