# NateMan – Nachschreibtermin-Manager
# blueprints/klausuren.py
# Copyright © 2020 Niklas Elsbrock
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""
Klausurlisten-Blueprint
"""
from datetime import datetime
from flask import Blueprint, abort, current_app, flash, g, redirect, render_template, request, url_for, Response
from .auth import login_required, beratungslehrer_required
from ..config_manager import config
from ..models import Klausur, Klausurteilnahme, Lehrer, Schueler, db, get_next_new_schueler_id, Stufe
bp = Blueprint("klausuren", __name__, url_prefix="/klausuren")
@bp.route("/")
@login_required
def mine():
""" Ansicht der eigenen Klausuren (Seite *Meine Klausuren*) """
klausur_query = Klausur.query.filter(Klausur.lehrer == g.lehrer).order_by(Klausur.date)
klausuren_anstehend = klausur_query.filter(Klausur.date > datetime.now().date()).all()
klausuren_vergangen = klausur_query.filter(Klausur.date <= datetime.now().date()).all()
return render_template("klausuren/mine.html.j2", klausuren_anstehend=klausuren_anstehend,
klausuren_vergangen=klausuren_vergangen)
@bp.route("/schueler", endpoint="schueler")
def schueler_():
schueler_id = request.args.get("id")
schueler_nachname = request.args.get("nachname")
if schueler_id is None or schueler_nachname is None:
flash("Fehlende Schülerdaten.", "error")
return redirect(url_for("index.index"), code=303)
schueler = Schueler.query.filter_by(id=schueler_id, nachname=schueler_nachname).first()
if schueler is None:
flash("Ungültige Schülerdaten.", "error")
return redirect(url_for("index.index"), code=303)
kt_query = Klausurteilnahme.query.join(Schueler).join(Klausur) \
.filter(Schueler.id == schueler_id) \
.order_by(Klausur.date)
kt_anstehend = kt_query.filter(Klausur.date > datetime.now().date()).all()
kt_vergangen = kt_query.filter(Klausur.date <= datetime.now().date()).all()
return render_template("klausuren/schueler.html.j2", schueler=schueler, kt_vergangen=kt_vergangen,
kt_anstehend=kt_anstehend)
@bp.route("//", endpoint="stufe")
def stufe_(stufe_name):
""" Ansicht der Klausuren einer Stufe (Seite *Klausuren [Stufe]*) """
stufe = Stufe.query.filter_by(name=stufe_name).first()
if stufe is None:
abort(404)
return
desc_order = request.args.get("order") == "desc"
if desc_order:
order = Klausur.date.desc()
else:
order = Klausur.date.asc()
dates_query = db.session.query(Klausur.date).filter_by(stufe=stufe).order_by(order)
dates_anstehend = [t.date for t in dates_query.filter(
(Klausur.date > datetime.now().date()) if desc_order else (Klausur.date >= datetime.now().date())
).distinct()]
dates_vergangen = [t.date for t in dates_query.filter(
(Klausur.date <= datetime.now().date()) if desc_order else (Klausur.date < datetime.now().date())
).distinct()]
return render_template("klausuren/stufe.html.j2", stufe=stufe, dates_anstehend=dates_anstehend,
dates_vergangen=dates_vergangen, desc_order=desc_order)
@bp.route("//add", methods=("GET", "POST"))
@beratungslehrer_required
def add(stufe_name):
""" Klausurhinzufügung """
reload_resp = redirect(url_for(".add", stufe_name=stufe_name), code=303)
stufe = Stufe.query.filter_by(name=stufe_name).first()
if stufe is None:
abort(404)
return
if not g.lehrer.can_access(stufe):
abort(403)
return
if request.method != "POST":
return render_template("klausuren/add.html.j2", stufe=stufe)
# ELSE
kursname = request.form["kursname"]
lehrer_id = request.form["lehrer"]
date_str = request.form["date"]
try:
startperiod = int(request.form["startperiod"])
endperiod = int(request.form["endperiod"])
except ValueError:
abort(400)
return
if startperiod < 0 or startperiod > endperiod or endperiod.bit_length() > 32:
flash("Ungültiger Zeitraum", "error")
return reload_resp
if not kursname:
flash("Bitte geben Sie einen Kursnamen an.", "error")
return reload_resp
try:
date = datetime.strptime(date_str, "%Y-%m-%d").date()
except ValueError:
abort(400)
return
lehrer = Lehrer.query.filter_by(id=lehrer_id).first()
if lehrer is None:
abort(400)
return
new_klausur = Klausur(kursname=kursname, date=date, startperiod=startperiod, endperiod=endperiod, stufe=stufe,
lehrer=lehrer)
db.session.add(new_klausur)
db.session.commit()
flash("Die Klausur wurde erfolgreich hinzugefügt.", "success")
current_app.logger.info(f"{g.lehrer} hat die Klausur {new_klausur} hinzugefügt.")
return redirect(url_for(".edit", klausur_id=new_klausur.id), code=303)
@bp.route("/", methods=("GET", "POST"))
@login_required
def edit(klausur_id):
""" Klausurbearbeitung """
klausur = Klausur.query.filter_by(id=klausur_id).first()
if klausur is None:
abort(404)
return
if not g.lehrer.can_access(klausur.stufe) and klausur.lehrer != g.lehrer:
abort(403)
return
# ELSE
if request.method != "POST":
kt_list = Klausurteilnahme.query \
.filter_by(klausur=klausur) \
.join(Schueler) \
.order_by(Schueler.nachname, Schueler.vorname) \
.all()
not_in_klausur_list = Schueler.query \
.filter_by(stufe=klausur.stufe) \
.filter(Schueler.id.notin_(db.session.query(Klausurteilnahme.schueler_id).filter_by(klausur=klausur))) \
.order_by(Schueler.nachname, Schueler.vorname) \
.all()
return render_template("klausuren/edit.html.j2", klausur=klausur, kt_list=kt_list,
not_in_klausur_list=not_in_klausur_list)
# ELSE
reload_resp = redirect(url_for(".edit", klausur_id=klausur_id), code=303)
exit_resp: Response
if klausur.lehrer == g.lehrer:
exit_resp = redirect(url_for(".mine"), code=303)
else:
exit_resp = redirect(url_for(".stufe", stufe_name=klausur.stufe.name), code=303)
if klausur.edited and not g.lehrer.can_access(klausur.stufe):
abort(400)
return
action = request.form["action"]
if action == "edit":
lehrer_can_access = g.lehrer.can_access(klausur.stufe)
annotation = request.form["annotation"].strip()
flag_as_edited = ("flag-as-edited" in request.form)
klausur_laenge_str = request.form.get("laenge", None)
if lehrer_can_access and not klausur_laenge_str:
klausur_laenge = None
else:
try:
klausur_laenge = int(klausur_laenge_str)
except ValueError:
abort(400)
return
klausur.laenge = klausur_laenge
for kt in Klausurteilnahme.query.filter_by(klausur=klausur).all():
kt.versaeumt = (f"s_{kt.schueler.id}" in request.form)
if annotation:
if len(annotation) > config["klausuren"]["max-annotation-length"]:
abort(400)
return
klausur.annotation = annotation
else:
klausur.annotation = None
if lehrer_can_access:
klausur.edited = flag_as_edited
else:
klausur.edited = True
current_app.logger.info(f"{g.lehrer} hat die Klausur {klausur} bearbeitet.")
flash("Klausur erfolgreich bearbeitet.", "success")
db.session.commit()
return exit_resp
elif action == "edit-advanced":
if not g.lehrer.can_access(klausur.stufe):
abort(400)
return
new_lehrer_id = request.form["new-lehrer"]
new_date_str = request.form["new-date"]
try:
new_startperiod = int(request.form["new-startperiod"])
new_endperiod = int(request.form["new-endperiod"])
except ValueError:
abort(400)
return
if new_startperiod < 0 or new_startperiod > new_endperiod or new_endperiod.bit_length() > 32:
flash("Ungültiger Zeitraum", "error")
return reload_resp
try:
new_date = datetime.strptime(new_date_str, "%Y-%m-%d").date()
except ValueError:
abort(400)
return
new_lehrer = Lehrer.query.filter_by(id=new_lehrer_id).first()
if new_lehrer is None:
abort(400)
return
klausur.lehrer = new_lehrer
klausur.date = new_date
klausur.startperiod = new_startperiod
klausur.endperiod = new_endperiod
current_app.logger.info(f"{g.lehrer} hat die Klausur {klausur} erweitert bearbeitet.")
flash("Klausur erfolgreich bearbeitet.", "success")
db.session.commit()
return reload_resp
elif action == "delete":
if not g.lehrer.can_access(klausur.stufe):
abort(400)
return
current_app.logger.info(f"{g.lehrer} hat die Klausur {klausur} gelöscht.")
flash("Die Klausur wurde erfolgreich gelöscht.", "success")
db.session.delete(klausur)
db.session.commit()
return exit_resp
elif action == "add-schueler":
schueler_id = request.form.get("added-schueler", None)
add_to = request.form["add-to"]
if schueler_id is None:
flash("Bitte wählen Sie einen Schüler zum Hinzufügen aus.", "error")
return reload_resp
is_new = False
if schueler_id == "new-schueler":
is_new = True
new_schueler_nachname = request.form["new-schueler-nachname"].strip()
new_schueler_vorname = request.form["new-schueler-vorname"].strip()
new_schueler_koop = ("new-schueler-koop" in request.form)
if not new_schueler_nachname or not new_schueler_vorname:
flash("Bitte geben Sie einen vollständigen Namen für den neuen Schüler an.", "error")
return reload_resp
schueler = Schueler(id=get_next_new_schueler_id(), nachname=new_schueler_nachname,
vorname=new_schueler_vorname, stufe=klausur.stufe,
koop=new_schueler_koop)
db.session.add(schueler)
else:
schueler = Schueler.query.filter_by(id=schueler_id).first()
if schueler is None:
abort(400)
return
if Klausurteilnahme.query.filter_by(klausur=klausur, schueler=schueler).first() is not None:
abort(400)
return
if add_to == "klausur":
klausuren = [klausur]
elif add_to == "kurs":
klausuren = Klausur.query.filter_by(stufe=klausur.stufe).filter_by(kursname=klausur.kursname).all()
else:
abort(400)
return
for k in klausuren:
kt_entity = Klausurteilnahme(klausur=k, schueler=schueler)
db.session.add(kt_entity)
current_app.logger.info(f"{g.lehrer} hat den{' neuen' if is_new else ''} Schüler {schueler} "
f"zur Klausur {klausur} hinzugefügt.")
db.session.commit()
return reload_resp
elif action == "remove-schueler":
for kt in Klausurteilnahme.query.join(Klausur)\
.filter_by(stufe=klausur.stufe)\
.filter_by(kursname=klausur.kursname)\
.all():
if f"r_{kt.schueler.id}" in request.form:
db.session.delete(kt)
current_app.logger.info(f"{g.lehrer} hat den Schüler {kt.schueler} aus der Klausur "
f"{kt.klausur} entfernt.")
db.session.commit()
return reload_resp
else:
# Bad Request, wenn ungültige action
abort(400)